ExtrudeShape Caps issue

I had no idea we had this tool in the documentation :slight_smile:

I think it’s because of all the downsides you listed (no scaling, no rotation, and potentially weird joins at very sharp angles). It’s better to have it as an external code snippet if it has so many pitfalls.

1 Like

Feel free to add new features if you think they might be useful! The existing code is an external contribution, so we (the team members) are not really experts in this area.

1 Like

Quick update, I started to explore the generalization of your idea @Evgeni_Popov but it was too complex and maybe not solvable. First, angles over 90° need a scale that’s not uniform. Second, with large shapes and short path segments, the positioning of the extra points is problematic.

I ended up using the mitre extrusion technique, and dropping scale/rotation capabilities when this approach is used, which is consistent with the choices made in BJS itself. Sharing here in case anyone else goes through the same thought process.

Also, I’ve converted the code to Typescript and added cap option, sharing below in case it’s useful to anyone:

import { Ray } from '@babylonjs/core/Culling/ray'
import { Vector3 } from '@babylonjs/core/Maths/math'
import { Plane } from '@babylonjs/core/Maths/math.plane'
import { RibbonBuilder } from '@babylonjs/core/Meshes/Builders/ribbonBuilder'
import { Mesh } from '@babylonjs/core/Meshes/mesh'
import { Scene } from '@babylonjs/core/scene'

interface MiterExtrudeOptions {
  shape: Vector3[]
  path: Vector3[]
  scale: number
  cap: boolean
}

export const miterExtrude = (name: string, options: MiterExtrudeOptions, scene: Scene): Mesh => {
  const { shape: originalShape, path, scale, cap } = options
  // Apply scale to shape
  const shape = originalShape.map((pt) => new Vector3(pt.x * scale, pt.y * scale, 0))
  const nbPoints = path.length
  const line = Vector3.Zero()
  const nextLine = Vector3.Zero()
  let axisX = Vector3.Zero()
  let axisY = Vector3.Zero()
  let axisZ = Vector3.Zero()
  const nextAxisX = Vector3.Zero()
  const nextAxisY = Vector3.Zero()
  const nextAxisZ = Vector3.Zero()
  let startPoint = Vector3.Zero()
  const nextStartPoint = Vector3.Zero()
  const bisector = Vector3.Zero()
  let planeParallel: Vector3
  let planeNormal: Vector3
  let plane: Plane
  let ray: Ray
  let distance: number
  const allPaths: Vector3[][] = []
  const tempPaths: Vector3[][] = []
  // Initial camera up vector (use world up as default)
  const cameraUp = scene.activeCamera?.position || Vector3.Up()
  for (let s = 0; s < shape.length; s++) {
    path[1].subtractToRef(path[0], line)
    axisZ = line.clone().normalize()
    axisX = Vector3.Cross(cameraUp, axisZ).normalize()
    axisY = Vector3.Cross(axisZ, axisX)
    startPoint = path[0].add(axisX.scale(shape[s].x)).add(axisY.scale(shape[s].y))
    const ribbonPath = [startPoint.clone()]
    for (let p = 0; p < nbPoints - 2; p++) {
      path[p + 2].subtractToRef(path[p + 1], nextLine)
      nextAxisZ.copyFrom(nextLine.normalize())
      Vector3.CrossToRef(cameraUp, nextAxisZ, nextAxisX)
      nextAxisX.normalize()
      Vector3.CrossToRef(nextAxisZ, nextAxisX, nextAxisY)
      nextAxisZ.subtractToRef(axisZ, bisector)
      planeParallel = Vector3.Cross(nextAxisZ, axisZ)
      planeNormal = Vector3.Cross(planeParallel, bisector)
      plane = Plane.FromPositionAndNormal(path[p + 1], planeNormal)
      ray = new Ray(startPoint, axisZ)
      distance = ray.intersectsPlane(plane) || 0
      startPoint.addToRef(axisZ.scale(distance), nextStartPoint)
      ribbonPath.push(nextStartPoint.clone())
      axisX = nextAxisX.clone()
      axisY = nextAxisY.clone()
      axisZ = nextAxisZ.clone()
      startPoint = nextStartPoint.clone()
    }
    // Last Point
    planeNormal = axisZ
    plane = Plane.FromPositionAndNormal(path[nbPoints - 1], planeNormal)
    ray = new Ray(startPoint, axisZ)
    distance = ray.intersectsPlane(plane) || 0
    startPoint.addToRef(axisZ.scale(distance), nextStartPoint)
    ribbonPath.push(nextStartPoint.clone())
    tempPaths.push(ribbonPath)
  }

  // If caps are enabled, add center points at start and end
  if (cap) {
    // Calculate center point at start
    const startCapPoints = tempPaths.map((p) => p[0])
    const startCenter = startCapPoints.reduce((sum, pt) => sum.add(pt), Vector3.Zero()).scale(1 / startCapPoints.length)

    // Calculate center point at end
    const endCapPoints = tempPaths.map((p) => p[p.length - 1])
    const endCenter = endCapPoints.reduce((sum, pt) => sum.add(pt), Vector3.Zero()).scale(1 / endCapPoints.length)

    // Add center points to each ribbon path
    for (const ribbonPath of tempPaths) {
      allPaths.push([startCenter, ...ribbonPath, endCenter])
    }
  } else {
    allPaths.push(...tempPaths)
  }

  return RibbonBuilder.CreateRibbon(
    name,
    {
      pathArray: allPaths,
      sideOrientation: Mesh.DOUBLESIDE,
      closeArray: false,
      closePath: false,
    },
    scene
  )
}
2 Likes