Custom face material on ExtrudeGeometry

Hi there,

in one of my apps i use ExtrudeGeometries heavily. I know that there is a way to give the caps and the faces created by the extrusion different materials with material groups 1 and 2. Today i thought if the following would be possible:

If i create a shape like this:

is there some way to add a material group 2 to the existing geometry’s material group 1 (basically the contour of the shape when extruded). lets say the 5th lineTo and the absArc should have another group so i can give them another material. Any hint would be helpful has anyone have ever done this? i could not find anything about it.

Greetings Tom

short answer, not really out of the box. ExtrudeGeometry only splits materials into caps and side walls, it doesn’t keep track of individual edges or segments from your original Shape once it builds the side faces.

what you’re asking would mean tagging specific contour segments like that 5th lineTo or the absArc and carrying that info all the way through the extrusion so they end up in a separate material group. three.js just doesn’t store that mapping when it triangulates and builds the geometry.

you’ve got a couple of workarounds though

one is to post-process the geometry and rebuild groups yourself. you can iterate over the faces in the side wall, look at their UVs or positions, and decide which ones belong to the region you want, then assign a different material index. it’s a bit hacky but works if your shape is predictable

another is to split the shape before extruding. basically build two shapes, one for the segment you want different and one for the rest, extrude them separately, then merge or just render them together with different materials. this is usually the cleanest approach

last option is going lower level and writing your own extrusion logic or modifying ExtrudeGeometry so it emits groups per path segment. that’s more effort but gives full control if you need it a lot

so yeah, no built-in way, but splitting the shape ahead of time is probably your best bet unless you want to dive into custom geometry generation

1 Like

P.S. this is what i mean in “3D”:

Hey @Umbawa_Sarosong ,

thanks for the answer. I like the first idea of post-processing a lot that is exactly where i wanted to go for.

The second approach would definitely the fallback solution and i do this already in my app.

The third approach of defining a custom geometry sounds scary to me but of course i could give it a try with the help of AI why not.

1 Like

It’s not that hard once you give it some thought.

I started with simple geometric shapes and quickly moved on to much more complex ones.
https://discourse.threejs.org/t/profiledcontourgeometry-multimaterial/5801it’s along the lines of what you obviously need.

A very simple example: Prism
(see Implement the different color for each face of the shape which is made by extrude geometry - #6 by hofk)

This is a good way to get a basic understanding of the issue.
RoundedRectangle + Squircle
Round-edged box flat

See also Construction of frames with contour/profile

2 Likes

That gave me the idea to try it out that way, too. Until recently, you always had to handle all the little details yourself.

For a manageable task like this, it works quite well if you define the conditions clearly enough.

In the end, it works, but it’s a far cry from my usual programming style, and it remains problematic when integrating such things into larger projects.

While it saves time when coding, you first have to figure out that haphazardly cobbled-together code, which is sometimes more exhausting than doing it your own way.

The result: angleprofile

Code:

<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/custom-face-material-on-extrudegeometry/90550/6 -->
<head>
	<title> angleprofile </title>
	<meta charset="utf-8" />
<style>
		body{
		overflow: hidden;
		margin: 0;
		}
	</style>
</head>

<body></body>

<script type="module">

// @author hofk + AI

import * as THREE from "../jsm/three.module.182.js";
import { OrbitControls } from "../jsm/OrbitControls.182.js";

const scene = new THREE.Scene( ); 
scene.background = new THREE.Color( 0xdedede );
const camera = new THREE.PerspectiveCamera( 55, innerWidth / innerHeight, 0.01, 1000 );
camera.position.set( 8, 12, 16 );
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( innerWidth, innerHeight );
 
document.body.appendChild(renderer.domElement);

//const light = new THREE.AmbientLight( 0xffffff, 1.0 );
//scene.add( light );
 
const controls = new OrbitControls( camera, renderer.domElement );

scene.add( new THREE.GridHelper( 20, 20 ) );
 
const materials = [

   new THREE.MeshBasicMaterial({ color: 0x000000 }),
   new THREE.MeshBasicMaterial({ color: 0xff0000 }),
   new THREE.MeshBasicMaterial({ color: 0xff00ff }),
   new THREE.MeshBasicMaterial({ color: 0xffff00 }),
   new THREE.MeshBasicMaterial({ color: 0x00ffff }),
   new THREE.MeshBasicMaterial({ color: 0xffffff }),
   new THREE.MeshBasicMaterial({ color: 0xa0b0c0 }),
   new THREE.MeshBasicMaterial({ color: 0x999900 }),
   new THREE.MeshBasicMaterial({ color: 0x990099 })
   
]
 
const geometry = angleprofileGeometry( 5, 0.6, 3 , 3.8, 1, 8 );
const profile = new THREE.Mesh( geometry, materials );
scene.add( profile );

animate( );

function animate( ) {

	requestAnimationFrame( animate );	
	renderer.render( scene, camera );
	
}

function angleprofileGeometry( ylength, thick, xlength , zlength, radius, smooth ) {

  const r = Math.max(0, Math.min(radius, thick));
  const arcSegments = Math.max(2, Math.floor(smooth));
 
  const xOuter = thick + r + xlength;
  const zOuter = thick + r + zlength;

  const positions = [];
  const normals = [];
  const indices = [];
  const groups = [];

  let vertexCount = 0;

  function pushVertex(x, y, z, nx, ny, nz) {
    positions.push(x, y, z);
    normals.push(nx, ny, nz);
    return vertexCount++;
  }

  function pushIndices(a, b, c) {
    indices.push(a, b, c);
  }

  function addGroup(startIndex, endIndex, materialIndex) {
    groups.push({
      start: startIndex,
      count: endIndex - startIndex,
      materialIndex,
    });
  }

  function normalize2(dx, dz) {
    const len = Math.hypot(dx, dz);
    if (len < 1e-12) return { x: 0, z: 0 };
    return { x: dx / len, z: dz / len };
  }

  function normal2D(p0, p1) {
    const dx = p1.x - p0.x;
    const dz = p1.z - p0.z;
    const n = normalize2(dz, -dx);
    return n;
  }

  // quarter circle  
  const center = { x: thick + r, z: thick + r };

  // Arc points, including endpoints:
  // D = (thick + r, thick)   -> angle -90°
  // E = (thick, thick + r)   -> angle -180°
  const arcPts = [];
  if (r > 1e-8) {
    for (let i = 0; i <= arcSegments; i++) {
      const t = i / arcSegments;
      const a = -Math.PI / 2 - t * (Math.PI / 2); // -90° .. -180°
      arcPts.push({
        x: center.x + r * Math.cos(a),
        z: center.z + r * Math.sin(a),
      });
    }
  }

  // Contour points  for the end faces
  //  radius > 0:
  // A -> B -> C -> D -> arc ... -> E -> F -> G
  // radius = 0:
  // A -> B -> C -> K -> F -> G
  const contour = [];

  const A = { x: 0, z: 0 };
  const B = { x: xOuter, z: 0 };
  const C = { x: xOuter, z: thick };
  const K = { x: thick, z: thick };
  const F = { x: thick, z: zOuter };
  const G = { x: 0, z: zOuter };

  contour.push(A, B, C);

  let D = null;
  let E = null;

  if (r > 1e-8) {
    D = arcPts[0];
    E = arcPts[arcPts.length - 1];
    contour.push(...arcPts);
    contour.push(F, G);
  } else {
    contour.push(K, F, G);
  }

  // Front and back surfaces:
  // Front  y=0, normale -Y
  // Back   y=ylength, normal +Y
  function addCap(y, ny, materialIndex) {
    const start = indices.length;
    const capVertexIndices = contour.map((p) => {
      return pushVertex(p.x, y, p.z, 0, ny, 0);
    });

    const contour2D = contour.map((p) => new THREE.Vector2(p.x, p.z));
    const tris = THREE.ShapeUtils.triangulateShape(contour2D, []);

    for (const tri of tris) {
      let [a, b, c] = tri;

      // contour  ( CCW ) => The normal of the front face at y=0 is -Y.
      // For the back side, the triangles are flipped over.
      if (ny < 0) {
        pushIndices(capVertexIndices[a], capVertexIndices[b], capVertexIndices[c]);
      } else {
        pushIndices(capVertexIndices[c], capVertexIndices[b], capVertexIndices[a]);
      }
    }

    addGroup(start, indices.length, materialIndex);
  }

  addCap(0, -1, 0);
  addCap(ylength, +1, 1);

  // Auxiliary function: a straight side face as a quad
  function addStraightFace(p0, p1, materialIndex) {
  
    const start = indices.length;
    const n2 = normal2D(p0, p1);

    const v0 = pushVertex(p0.x, 0, p0.z, n2.x, 0, n2.z);
    const v1 = pushVertex(p1.x, 0, p1.z, n2.x, 0, n2.z);
    const v2 = pushVertex(p1.x, ylength, p1.z, n2.x, 0, n2.z);
    const v3 = pushVertex(p0.x, ylength, p0.z, n2.x, 0, n2.z);

    // Winding chosen so that the outer normal points in the correct direction.
    pushIndices(v0, v3, v2);
    pushIndices(v0, v2, v1);

    addGroup(start, indices.length, materialIndex);
  }

  // Help function: Fillet as a quarter cylinder
  function addFilletFace(points, materialIndex) {
  
    const start = indices.length;

    const front = [];
    const back = [];

    for (const p of points) {
      const nx = p.x - center.x;
      const nz = p.z - center.z;
      const n = normalize2(nx, nz);

      front.push(pushVertex(p.x, 0, p.z, n.x, 0, n.z));
      back.push(pushVertex(p.x, ylength, p.z, n.x, 0, n.z));
    }

    for (let i = 0; i < points.length - 1; i++) {
      const v0 = front[i];
      const v1 = front[i + 1];
      const v2 = back[i + 1];
      const v3 = back[i];

      pushIndices(v0, v3, v2);
      pushIndices(v0, v2, v1);
    }

    addGroup(start, indices.length, materialIndex);
  }

  // Side panels in the desired group order
  addStraightFace(A, B, 2); // outer upper surface
  addStraightFace(B, C, 3); // outer right surface

  if (r > 1e-8) {
    addStraightFace(C, D, 4);                // inner upper straight line
    addFilletFace(arcPts, 5);                // rounding
    addStraightFace(E, F, 6);                // inner left straight
    addStraightFace(F, G, 7);                // outer lower surface
    addStraightFace(G, A, 8);                // outer left surface
  } else {
    // sharp corner without a rounding
    addStraightFace(C, K, 4);
    addStraightFace(K, F, 6);
    addStraightFace(F, G, 7);
    addStraightFace(G, A, 8);
  }

  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
  geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
  geometry.setIndex(indices);
  geometry.clearGroups();

  for (const g of groups) {
    geometry.addGroup(g.start, g.count, g.materialIndex);
  }

  geometry.computeBoundingBox();
  geometry.computeBoundingSphere();

  return geometry;
}
</script>

</html>
2 Likes

Hi @hofk ,

thanks for your code. I marked this as the solution although i am going the other way of reassigning the faces in a postprocessing method after the creation of the geometry. I know the start and the endpoint of the shapes in between the material should be reassigned that works for a lot of cases for me!

  1. Each shape defines a zone via two 2D coordinates (start and end point on its contour)
  2. Extrude the shape
  3. Post-processing function reads the geometry’s vertex buffer, checks which side-wall triangles have both their vertices within that zone
  4. Reassigns those to a separate material group
  5. Attach a different material to each group

This works well because i always know whats the start-/endpoint where i want to reassign the material in between.

It might be interesting for the community to compare both solutions.

Can you post the code here?

Then I can include both versions in the
Collection of examples from discourse.threejs.org .

I’m not sure if you can mark two posts as solutions here. But feel free to mark your own solution as such; “mine” is mostly just AI.

In my project it works slightly different but here is an abstracted version for the simple example like the L-Shape:

L-Shape creation, in this case i mark the inner rounding as material group 2:

import * as THREE from 'three';

// Simple L-shape with a rounded inner 90° corner (absarc)
export function createDemoLShape() {
  const shape = new THREE.Shape();

  const outerW = 2.0;
  const outerH = 2.0;
  const leg = 0.7;
  const r = 0.25;

  const cx = leg + r;
  const cy = leg + r;

  shape.moveTo(0, 0);
  shape.lineTo(outerW, 0);
  shape.lineTo(outerW, leg);
  shape.lineTo(cx, leg);

  // inner corner radius (quarter arc)
  shape.absarc(cx, cy, r, -Math.PI / 2, Math.PI, false);

  shape.lineTo(leg, outerH);
  shape.lineTo(0, outerH);
  shape.lineTo(0, 0);

  return {
    shape,
    materialSegments: [
      {
        // segment along the rounded inner corner
        startPoint: [cx, leg],
        endPoint: [leg, cy],
        materialIndex: 2,
      },
    ],
  };
}

Here is the post processing method that looks for the faces that needs to be reassigned, what it does:

  1. Find the side-face group of the ExtrudeGeometry (usually materialIndex = 1).
  2. Read the 2D contour points from the source shape and the vertex positions from the generated geometry.
  3. Define the target zone by locating the predefined start and end points on the contour. All contour points between them become the zone.
  4. Check each side-wall triangle: determine its 2 unique 2D (x, y) positions. If both belong to the target zone, reassign that triangle to materialIndex = 2.
  5. Rebuild the geometry groups so Three.js can render the reassigned faces with the second material.

Disclaimer: the code is fully generated aswell

const EPS = 1e-6; // small tollerance if values are not exact caused by calculations in js.

// Reassigns side-wall triangles to custom material groups based on shape segments
export function applyMaterialSegments(geometry, shape, materialSegments) {
  if (!materialSegments?.length) return;

  const sideGroupIdx = geometry.groups.findIndex((g) => g.materialIndex === 1);
  if (sideGroupIdx === -1) return;

  const sideGroup = geometry.groups[sideGroupIdx];
  const pts = shape.extractPoints().shape;
  const pos = geometry.attributes.position.array;
  const indexArr = geometry.index?.array ?? null;

  const triCount = sideGroup.count / 3;
  const triMaterials = new Array(triCount).fill(1);

  for (const { startPoint, endPoint, materialIndex } of materialSegments) {
    let startIdx = -1;
    let endIdx = -1;

    for (let i = 0; i < pts.length; i++) {
      if (startIdx === -1 && Math.abs(pts[i].x - startPoint[0]) < EPS && Math.abs(pts[i].y - startPoint[1]) < EPS) {
        startIdx = i;
      }
      if (Math.abs(pts[i].x - endPoint[0]) < EPS && Math.abs(pts[i].y - endPoint[1]) < EPS) {
        endIdx = i;
      }
    }

    if (startIdx === -1 || endIdx === -1 || startIdx >= endIdx) continue;

    const zonePoints = [];
    for (let i = startIdx; i <= endIdx; i++) zonePoints.push(pts[i]);

    for (let t = 0; t < triCount; t++) {
      const base = sideGroup.start + t * 3;
      const i0 = indexArr ? indexArr[base] : base;
      const i1 = indexArr ? indexArr[base + 1] : base + 1;
      const i2 = indexArr ? indexArr[base + 2] : base + 2;

      const v0x = pos[i0 * 3], v0y = pos[i0 * 3 + 1];
      const v1x = pos[i1 * 3], v1y = pos[i1 * 3 + 1];
      const v2x = pos[i2 * 3], v2y = pos[i2 * 3 + 1];

      const unique = [[v0x, v0y]];
      if (Math.abs(v1x - v0x) > EPS || Math.abs(v1y - v0y) > EPS) unique.push([v1x, v1y]);
      if (unique.length < 2 && (Math.abs(v2x - v0x) > EPS || Math.abs(v2y - v0y) > EPS)) unique.push([v2x, v2y]);
      if (unique.length !== 2) continue;

      const m0 = zonePoints.some((p) => Math.abs(p.x - unique[0][0]) < EPS && Math.abs(p.y - unique[0][1]) < EPS);
      const m1 = zonePoints.some((p) => Math.abs(p.x - unique[1][0]) < EPS && Math.abs(p.y - unique[1][1]) < EPS);

      if (m0 && m1) triMaterials[t] = materialIndex;
    }
  }

  const otherGroups = geometry.groups.filter((_, i) => i !== sideGroupIdx);
  const newGroups = [...otherGroups];

  let runStart = 0;
  let runMat = triMaterials[0];
  for (let i = 1; i <= triCount; i++) {
    if (i === triCount || triMaterials[i] !== runMat) {
      newGroups.push({
        start: sideGroup.start + runStart * 3,
        count: (i - runStart) * 3,
        materialIndex: runMat,
      });
      if (i < triCount) {
        runStart = i;
        runMat = triMaterials[i];
      }
    }
  }

  geometry.groups = newGroups;
}

Usage:

const { shape, materialSegments } = createDemoLShape();

const geom = new THREE.ExtrudeGeometry(shape, {
  depth: 0.1,
  bevelEnabled: false,
  steps: 1,
});

applyMaterialSegments(geom, shape, materialSegments);

const mesh = new THREE.Mesh(geom, [
  new THREE.MeshStandardMaterial({ color: '#c79d6d' }), // material-0 caps
  new THREE.MeshStandardMaterial({ color: '#cccccc' }), // material-1 default side
  new THREE.MeshStandardMaterial({ color: '#ff5533' }), // material-2 selected segment
]);
scene.add(mesh);
1 Like

Combining the two solutions: faceMaterial

Here is the combined code for both solutions:


<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/custom-face-material-on-extrudegeometry/90550/6 ... 9 -->
<head>
	<title> faceMaterial </title>
	<meta charset="utf-8" />
<style>
		body{
		overflow: hidden;
		margin: 0;
		}
	</style>
</head>

<body></body>

<script type="module">

// @author kalabedo + hofk + AI

import * as THREE from "../jsm/three.module.182.js";
import { OrbitControls } from "../jsm/OrbitControls.182.js";

const scene = new THREE.Scene( ); 
scene.background = new THREE.Color( 0xdedede );
const camera = new THREE.PerspectiveCamera( 55, innerWidth / innerHeight, 0.01, 1000 );
camera.position.set( 4, 4, 4 );
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( innerWidth, innerHeight );
 
document.body.appendChild(renderer.domElement);

const light = new THREE.AmbientLight( 0xffffff, 1.0 );
scene.add( light );
 
const controls = new OrbitControls( camera, renderer.domElement );

scene.add( new THREE.GridHelper( 10, 10 ) );

//..................................................
 
const EPS = 1e-6; // small tollerance if values are not exact caused by calculations in js. 

const { shape, materialSegments } = createDemoLShape();

const geom = new THREE.ExtrudeGeometry(shape, {
  depth: 0.1,
  bevelEnabled: false,
  steps: 1,
});

applyMaterialSegments(geom, shape, materialSegments);

const mesh = new THREE.Mesh(geom, [
  new THREE.MeshStandardMaterial({ color: '#c79d6d' }), // material-0 caps
  new THREE.MeshStandardMaterial({ color: '#cccccc' }), // material-1 default side
  new THREE.MeshStandardMaterial({ color: '#ff5533' }), // material-2 selected segment
]);
scene.add(mesh); 

//.................................................

const materials = [

   new THREE.MeshBasicMaterial({ color: 0x000000 }),
   new THREE.MeshBasicMaterial({ color: 0xff0000 }),
   new THREE.MeshBasicMaterial({ color: 0xff00ff }),
   new THREE.MeshBasicMaterial({ color: 0xffff00 }),
   new THREE.MeshBasicMaterial({ color: 0x00ffff }),
   new THREE.MeshBasicMaterial({ color: 0xffffff }),
   new THREE.MeshBasicMaterial({ color: 0xa0b0c0 }),
   new THREE.MeshBasicMaterial({ color: 0x999900 }),
   new THREE.MeshBasicMaterial({ color: 0x990099 })
   
]
 
const geometry = angleprofileGeometry( 2.1, 0.15, 1.1 , 1.4, 0.5, 8 );
const profile = new THREE.Mesh( geometry, materials );

profile.position.z = -3;
scene.add( profile );

//..................................................

animate( );

function animate( ) {

	requestAnimationFrame( animate );	
	renderer.render( scene, camera );
	
}

// Simple L-shape with a rounded inner 90° corner (absarc)
function createDemoLShape() {
  const shape = new THREE.Shape();

  const outerW = 2.0;
  const outerH = 2.0;
  const leg = 0.7;
  const r = 0.25;

  const cx = leg + r;
  const cy = leg + r;

  shape.moveTo(0, 0);
  shape.lineTo(outerW, 0);
  shape.lineTo(outerW, leg);
  shape.lineTo(cx, leg);

  // inner corner radius (quarter arc)
  shape.absarc(cx, cy, r, -Math.PI / 2, Math.PI, true);

  shape.lineTo(leg, outerH);
  shape.lineTo(0, outerH);
  shape.lineTo(0, 0);

  return {
    shape,
    materialSegments: [
      {
        // segment along the rounded inner corner
        startPoint: [cx, leg],
        endPoint: [leg, cy],
        materialIndex: 2,
      },
    ],
  };
}


// Reassigns side-wall triangles to custom material groups based on shape segments
function applyMaterialSegments(geometry, shape, materialSegments) {
  if (!materialSegments?.length) return;

  const sideGroupIdx = geometry.groups.findIndex((g) => g.materialIndex === 1);
  if (sideGroupIdx === -1) return;

  const sideGroup = geometry.groups[sideGroupIdx];
  const pts = shape.extractPoints().shape;
  const pos = geometry.attributes.position.array;
  const indexArr = geometry.index?.array ?? null;

  const triCount = sideGroup.count / 3;
  const triMaterials = new Array(triCount).fill(1);

  for (const { startPoint, endPoint, materialIndex } of materialSegments) {
    let startIdx = -1;
    let endIdx = -1;

    for (let i = 0; i < pts.length; i++) {
      if (startIdx === -1 && Math.abs(pts[i].x - startPoint[0]) < EPS && Math.abs(pts[i].y - startPoint[1]) < EPS) {
        startIdx = i;
      }
      if (Math.abs(pts[i].x - endPoint[0]) < EPS && Math.abs(pts[i].y - endPoint[1]) < EPS) {
        endIdx = i;
      }
    }

    if (startIdx === -1 || endIdx === -1 || startIdx >= endIdx) continue;

    const zonePoints = [];
    for (let i = startIdx; i <= endIdx; i++) zonePoints.push(pts[i]);

    for (let t = 0; t < triCount; t++) {
      const base = sideGroup.start + t * 3;
      const i0 = indexArr ? indexArr[base] : base;
      const i1 = indexArr ? indexArr[base + 1] : base + 1;
      const i2 = indexArr ? indexArr[base + 2] : base + 2;

      const v0x = pos[i0 * 3], v0y = pos[i0 * 3 + 1];
      const v1x = pos[i1 * 3], v1y = pos[i1 * 3 + 1];
      const v2x = pos[i2 * 3], v2y = pos[i2 * 3 + 1];

      const unique = [[v0x, v0y]];
      if (Math.abs(v1x - v0x) > EPS || Math.abs(v1y - v0y) > EPS) unique.push([v1x, v1y]);
      if (unique.length < 2 && (Math.abs(v2x - v0x) > EPS || Math.abs(v2y - v0y) > EPS)) unique.push([v2x, v2y]);
      if (unique.length !== 2) continue;

      const m0 = zonePoints.some((p) => Math.abs(p.x - unique[0][0]) < EPS && Math.abs(p.y - unique[0][1]) < EPS);
      const m1 = zonePoints.some((p) => Math.abs(p.x - unique[1][0]) < EPS && Math.abs(p.y - unique[1][1]) < EPS);

      if (m0 && m1) triMaterials[t] = materialIndex;
    }
  }

  const otherGroups = geometry.groups.filter((_, i) => i !== sideGroupIdx);
  const newGroups = [...otherGroups];

  let runStart = 0;
  let runMat = triMaterials[0];
  for (let i = 1; i <= triCount; i++) {
    if (i === triCount || triMaterials[i] !== runMat) {
      newGroups.push({
        start: sideGroup.start + runStart * 3,
        count: (i - runStart) * 3,
        materialIndex: runMat,
      });
      if (i < triCount) {
        runStart = i;
        runMat = triMaterials[i];
      }
    }
  }

  geometry.groups = newGroups;
}

//...............................................

function angleprofileGeometry( ylength, thick, xlength , zlength, radius, smooth ) {

  const r = Math.max(0, Math.min(radius, thick));
  const arcSegments = Math.max(2, Math.floor(smooth));
 
  const xOuter = thick + r + xlength;
  const zOuter = thick + r + zlength;

  const positions = [];
  const normals = [];
  const indices = [];
  const groups = [];

  let vertexCount = 0;

  function pushVertex(x, y, z, nx, ny, nz) {
    positions.push(x, y, z);
    normals.push(nx, ny, nz);
    return vertexCount++;
  }

  function pushIndices(a, b, c) {
    indices.push(a, b, c);
  }

  function addGroup(startIndex, endIndex, materialIndex) {
    groups.push({
      start: startIndex,
      count: endIndex - startIndex,
      materialIndex,
    });
  }

  function normalize2(dx, dz) {
    const len = Math.hypot(dx, dz);
    if (len < 1e-12) return { x: 0, z: 0 };
    return { x: dx / len, z: dz / len };
  }

  function normal2D(p0, p1) {
    const dx = p1.x - p0.x;
    const dz = p1.z - p0.z;
    const n = normalize2(dz, -dx);
    return n;
  }

  // quarter circle  
  const center = { x: thick + r, z: thick + r };

  // Arc points, including endpoints:
  // D = (thick + r, thick)   -> angle -90°
  // E = (thick, thick + r)   -> angle -180°
  const arcPts = [];
  if (r > 1e-8) {
    for (let i = 0; i <= arcSegments; i++) {
      const t = i / arcSegments;
      const a = -Math.PI / 2 - t * (Math.PI / 2); // -90° .. -180°
      arcPts.push({
        x: center.x + r * Math.cos(a),
        z: center.z + r * Math.sin(a),
      });
    }
  }

  // Contour points  for the end faces
  //  radius > 0:
  // A -> B -> C -> D -> arc ... -> E -> F -> G
  // radius = 0:
  // A -> B -> C -> K -> F -> G
  const contour = [];

  const A = { x: 0, z: 0 };
  const B = { x: xOuter, z: 0 };
  const C = { x: xOuter, z: thick };
  const K = { x: thick, z: thick };
  const F = { x: thick, z: zOuter };
  const G = { x: 0, z: zOuter };

  contour.push(A, B, C);

  let D = null;
  let E = null;

  if (r > 1e-8) {
    D = arcPts[0];
    E = arcPts[arcPts.length - 1];
    contour.push(...arcPts);
    contour.push(F, G);
  } else {
    contour.push(K, F, G);
  }

  // Front and back surfaces:
  // Front  y=0, normale -Y
  // Back   y=ylength, normal +Y
  function addCap(y, ny, materialIndex) {
    const start = indices.length;
    const capVertexIndices = contour.map((p) => {
      return pushVertex(p.x, y, p.z, 0, ny, 0);
    });

    const contour2D = contour.map((p) => new THREE.Vector2(p.x, p.z));
    const tris = THREE.ShapeUtils.triangulateShape(contour2D, []);

    for (const tri of tris) {
      let [a, b, c] = tri;

      // contour  ( CCW ) => The normal of the front face at y=0 is -Y.
      // For the back side, the triangles are flipped over.
      if (ny < 0) {
        pushIndices(capVertexIndices[a], capVertexIndices[b], capVertexIndices[c]);
      } else {
        pushIndices(capVertexIndices[c], capVertexIndices[b], capVertexIndices[a]);
      }
    }

    addGroup(start, indices.length, materialIndex);
  }

  addCap(0, -1, 0);
  addCap(ylength, +1, 1);

  // Auxiliary function: a straight side face as a quad
  function addStraightFace(p0, p1, materialIndex) {
  
    const start = indices.length;
    const n2 = normal2D(p0, p1);

    const v0 = pushVertex(p0.x, 0, p0.z, n2.x, 0, n2.z);
    const v1 = pushVertex(p1.x, 0, p1.z, n2.x, 0, n2.z);
    const v2 = pushVertex(p1.x, ylength, p1.z, n2.x, 0, n2.z);
    const v3 = pushVertex(p0.x, ylength, p0.z, n2.x, 0, n2.z);

    // Winding chosen so that the outer normal points in the correct direction.
    pushIndices(v0, v3, v2);
    pushIndices(v0, v2, v1);

    addGroup(start, indices.length, materialIndex);
  }

  // Help function: Fillet as a quarter cylinder
  function addFilletFace(points, materialIndex) {
  
    const start = indices.length;

    const front = [];
    const back = [];

    for (const p of points) {
      const nx = p.x - center.x;
      const nz = p.z - center.z;
      const n = normalize2(nx, nz);

      front.push(pushVertex(p.x, 0, p.z, n.x, 0, n.z));
      back.push(pushVertex(p.x, ylength, p.z, n.x, 0, n.z));
    }

    for (let i = 0; i < points.length - 1; i++) {
      const v0 = front[i];
      const v1 = front[i + 1];
      const v2 = back[i + 1];
      const v3 = back[i];

      pushIndices(v0, v3, v2);
      pushIndices(v0, v2, v1);
    }

    addGroup(start, indices.length, materialIndex);
  }

  // Side panels in the desired group order
  addStraightFace(A, B, 2); // outer upper surface
  addStraightFace(B, C, 3); // outer right surface

  if (r > 1e-8) {
    addStraightFace(C, D, 4);                // inner upper straight line
    addFilletFace(arcPts, 5);                // rounding
    addStraightFace(E, F, 6);                // inner left straight
    addStraightFace(F, G, 7);                // outer lower surface
    addStraightFace(G, A, 8);                // outer left surface
  } else {
    // sharp corner without a rounding
    addStraightFace(C, K, 4);
    addStraightFace(K, F, 6);
    addStraightFace(F, G, 7);
    addStraightFace(G, A, 8);
  }

  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
  geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
  geometry.setIndex(indices);
  geometry.clearGroups();

  for (const g of groups) {
    geometry.addGroup(g.start, g.count, g.materialIndex);
  }

  geometry.computeBoundingBox();
  geometry.computeBoundingSphere();

  return geometry;
}
</script>

</html>
1 Like

This is awsome! Thanks for helping me solve this!