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>