// Crease & Outline — Non-photorealistic 3D rendering
//
// Two rendering passes:
// 1. Flat-shaded filled mesh (low contrast fill color)
// 2. Inverted-hull outline: geometry enlarged slightly with BackSide +
// solid ink material → thick silhouette outline
//
// Crease lines: EdgesGeometry extracts edges where the dihedral angle
// between adjacent face normals exceeds creaseAngle degrees.
//
// Rebuilt from scratch when shape or creaseAngle changes (rebuildOnChange).
import * as THREE from 'three';
export const PARAMS = {
shape: { value: "Torus Knot", options: ["Torus Knot", "Icosahedron", "Octahedron", "Torus"], label: "Shape", folder: "Structure", rebuildOnChange: true },
outlineThick: { value: 0.04, min: 0, max: 0.15, step: 0.005, label: "Outline Thickness", folder: "Appearance" },
creaseAngle: { value: 25, min: 5, max: 90, step: 1, label: "Crease Angle (°)", folder: "Appearance", rebuildOnChange: true },
fillColor: { value: "#e0e8f0", type: "color", label: "Fill", folder: "Appearance" },
inkColor: { value: "#0a0a0a", type: "color", label: "Ink", folder: "Appearance" },
bg: { value: "#fafafa", type: "color", label: "Background", folder: "Appearance" },
rotateSpeed: { value: 0.3, min: 0, max: 2, step: 0.01, label: "Rotation Speed", folder: "Behavior" },
};
function buildGeometry(THREE, shape) {
switch (shape) {
case "Icosahedron": return new THREE.IcosahedronGeometry(1.3, 1);
case "Octahedron": return new THREE.OctahedronGeometry(1.5, 1);
case "Torus": return new THREE.TorusGeometry(1.1, 0.4, 16, 48);
default: return new THREE.TorusKnotGeometry(1, 0.35, 128, 16);
}
}
function buildScene(THREE, scene, params) {
// Clear previous objects
while (scene.children.length > 0) scene.remove(scene.children[0]);
scene.background = new THREE.Color(params.bg);
// Lighting — a subtle directional light for flat-shaded look
const ambLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.4);
dirLight.position.set(3, 5, 4);
scene.add(dirLight);
const geo = buildGeometry(THREE, params.shape);
// Fill mesh — flat shading gives faceted look, low contrast
const fillMat = new THREE.MeshLambertMaterial({
color: new THREE.Color(params.fillColor),
flatShading: true,
});
const fillMesh = new THREE.Mesh(geo, fillMat);
scene.add(fillMesh);
// Outline mesh — inverted hull: push vertices along normals with BackSide
const outlineMat = new THREE.ShaderMaterial({
uniforms: {
u_thickness: { value: params.outlineThick },
u_color: { value: new THREE.Color(params.inkColor) },
},
vertexShader: `
uniform float u_thickness;
void main() {
vec3 pos = position + normal * u_thickness;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
uniform vec3 u_color;
void main() {
gl_FragColor = vec4(u_color, 1.0);
}
`,
side: THREE.BackSide,
});
const outlineMesh = new THREE.Mesh(geo, outlineMat);
scene.add(outlineMesh);
// Crease lines — EdgesGeometry with threshold angle
const edgesGeo = new THREE.EdgesGeometry(geo, params.creaseAngle);
const edgesMat = new THREE.LineBasicMaterial({
color: new THREE.Color(params.inkColor),
});
const creaseLines = new THREE.LineSegments(edgesGeo, edgesMat);
// Render on top of fill mesh
creaseLines.renderOrder = 1;
scene.add(creaseLines);
return { fillMesh, outlineMesh, outlineMat, fillMat, creaseLines, edgesMat };
}
export function sceneSetup(THREE, scene, camera, renderer, params, seed) {
camera.position.set(0, 0, 4);
camera.lookAt(0, 0, 0);
const state = buildScene(THREE, scene, params);
return state;
}
export function sceneAnimate(THREE, scene, camera, state, params, time, delta) {
const { fillMesh, outlineMesh, outlineMat, fillMat, creaseLines, edgesMat } = state;
// Rotate all objects together
const angle = time * params.rotateSpeed;
fillMesh.rotation.y = angle;
fillMesh.rotation.x = angle * 0.4;
outlineMesh.rotation.y = angle;
outlineMesh.rotation.x = angle * 0.4;
creaseLines.rotation.y = angle;
creaseLines.rotation.x = angle * 0.4;
// Live color updates (no rebuild needed)
fillMat.color.set(params.fillColor);
const inkCol = new THREE.Color(params.inkColor);
outlineMat.uniforms.u_color.value.copy(inkCol);
outlineMat.uniforms.u_thickness.value = params.outlineThick;
edgesMat.color.copy(inkCol);
scene.background.set(params.bg);
}