// Hatching — NdotL-driven procedural ink hatching
//
// Custom ShaderMaterial that:
// 1. Computes a lighting term (NdotL) in the vertex shader, interpolated
// via vTone to the fragment shader.
// 2. Uses object-space position coordinates to build hatching UV so the
// lines stay fixed on the surface and don't swim with rotation.
// 3. One primary hatch layer always present; a perpendicular cross-hatch
// layer added in darker (shadowed) regions for deeper tone.
//
// Rebuilt when shape changes (rebuildOnChange).
import * as THREE from 'three';
export const PARAMS = {
shape: { value: "Torus Knot", options: ["Torus Knot", "Icosahedron", "Sphere", "Torus"], label: "Shape", folder: "Structure", rebuildOnChange: true },
hatchScale: { value: 80, min: 10, max: 300, step: 1, label: "Hatch Density (lines/unit)", folder: "Appearance" },
hatchAngle: { value: 0.78, min: 0, max: 3.14, step: 0.01, label: "Hatch Angle (rad)", folder: "Appearance" },
bands: { value: 4, min: 2, max: 6, step: 1, label: "Tone Bands", folder: "Appearance" },
inkColor: { value: "#0a0a0a", type: "color", label: "Ink", folder: "Appearance" },
paperColor: { value: "#f5efe1", type: "color", label: "Paper", folder: "Appearance" },
rotateSpeed: { value: 0.3, min: 0, max: 2, step: 0.01, label: "Rotation Speed", folder: "Behavior" },
};
const vertexShader = `
varying vec3 vNormal;
varying vec3 vPos;
void main() {
// Pass object-space data to fragment shader
vPos = position;
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform vec3 inkColor;
uniform vec3 paperColor;
uniform float hatchScale;
uniform float hatchAngle;
uniform float bands;
varying vec3 vNormal;
varying vec3 vPos;
float hatchPattern(vec2 uv, float angle, float scale) {
float s = uv.x * cos(angle) + uv.y * sin(angle);
return step(0.5, fract(s * scale));
}
void main() {
// Fixed light direction (view space — always same position relative to camera)
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.6));
float tone = max(dot(normalize(vNormal), lightDir), 0.0);
// Quantize tone into discrete bands (toon-like stepping for hatching density)
float bandedTone = floor(tone * bands) / bands;
// Hatching UV derived from object-space position
// Projecting onto a 2D plane so lines don't swim
vec2 hatchUV = vec2(vPos.x + vPos.z, vPos.y - vPos.z * 0.5);
float h1 = hatchPattern(hatchUV, hatchAngle, hatchScale);
// Cross-hatch at 90° offset
float h2 = hatchPattern(hatchUV, hatchAngle + 1.5708, hatchScale);
// Primary hatch: drawn everywhere; more opaque on darker areas
// Cross-hatch: drawn only in shadows (tone < 0.4)
float ink = (1.0 - h1) * (1.0 - bandedTone)
+ (tone < 0.4 ? (1.0 - h2) * 0.7 : 0.0);
ink = clamp(ink, 0.0, 1.0);
vec3 col = mix(paperColor, inkColor, ink * 0.85);
gl_FragColor = vec4(col, 1.0);
}
`;
function buildGeometry(THREE, shape) {
switch (shape) {
case "Icosahedron": return new THREE.IcosahedronGeometry(1.3, 3);
case "Sphere": return new THREE.SphereGeometry(1.4, 64, 32);
case "Torus": return new THREE.TorusGeometry(1.1, 0.4, 32, 96);
default: return new THREE.TorusKnotGeometry(1, 0.35, 128, 32);
}
}
export function sceneSetup(THREE, scene, camera, renderer, params, seed) {
camera.position.set(0, 0, 4);
camera.lookAt(0, 0, 0);
scene.background = new THREE.Color(params.paperColor);
const geo = buildGeometry(THREE, params.shape);
const mat = new THREE.ShaderMaterial({
uniforms: {
inkColor: { value: new THREE.Color(params.inkColor) },
paperColor: { value: new THREE.Color(params.paperColor) },
hatchScale: { value: params.hatchScale },
hatchAngle: { value: params.hatchAngle },
bands: { value: params.bands },
},
vertexShader,
fragmentShader,
});
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);
return { mesh, mat };
}
export function sceneAnimate(THREE, scene, camera, state, params, time, delta) {
const { mesh, mat } = state;
// Rotate
mesh.rotation.y = time * params.rotateSpeed;
mesh.rotation.x = time * params.rotateSpeed * 0.4;
// Update uniforms live
mat.uniforms.inkColor.value.set(params.inkColor);
mat.uniforms.paperColor.value.set(params.paperColor);
mat.uniforms.hatchScale.value = params.hatchScale;
mat.uniforms.hatchAngle.value = params.hatchAngle;
mat.uniforms.bands.value = params.bands;
scene.background.set(params.paperColor);
}