About this technique →
crease-outline/sketch.js
// 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);
}