From bdec4590d2fd0ee782ac5f1504eacb7dd20b6c15 Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Thu, 4 Jun 2026 11:40:55 +0200 Subject: [PATCH] Add table shader diagnostics and candle shadow model --- public/js/webgl-book-lab.js | 135 +++++++++++++++++++++++++++--------- 1 file changed, 101 insertions(+), 34 deletions(-) diff --git a/public/js/webgl-book-lab.js b/public/js/webgl-book-lab.js index 8176518..f5f127d 100644 --- a/public/js/webgl-book-lab.js +++ b/public/js/webgl-book-lab.js @@ -2,6 +2,23 @@ import * as THREE from 'https://esm.sh/three@0.165.0'; const canvas = document.getElementById('scene'); canvas.style.cursor = 'grab'; +const tableDebugModes = { + none: 0, + shadow: 1, + dust: 2, + normal: 3, + room: 4, + scene: 5, + mask: 6, + ao: 7 +}; +const urlParams = new URLSearchParams(window.location.search); +const tableDebugName = urlParams.get('tableDebug') || 'none'; +const tableDebugMode = tableDebugModes[tableDebugName] ?? tableDebugModes.none; +const labStatus = document.getElementById('lab_status'); +if (labStatus && tableDebugMode !== tableDebugModes.none) { + labStatus.textContent = `table debug: ${tableDebugName}`; +} const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false }); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.outputColorSpace = THREE.SRGBColorSpace; @@ -59,6 +76,11 @@ const cameraRig = { pointerY: 0, keys: new Set() }; +if (urlParams.get('view') === 'wide') { + cameraRig.target.set(0, 0.05, 0); + cameraRig.pitch = 0.96; + cameraRig.radius = 7.8; +} updateCameraRig(0); const clock = new THREE.Clock(); @@ -162,11 +184,11 @@ function buildTable() { color: 0x8a4c22, map: tableTexture, normalMap: tableNormal, - normalScale: new THREE.Vector2(0.07, 0.07), - roughness: 0.31, + normalScale: new THREE.Vector2(0.22, 0.18), + roughness: 0.38, metalness: 0, clearcoat: 0.54, - clearcoatRoughness: 0.42, + clearcoatRoughness: 0.48, reflectivity: 0.34, envMapIntensity: 0 }); @@ -238,7 +260,14 @@ function addCandle(x, y, z, intensity, height) { const baseLightIntensity = intensity * 7.4; const light = new THREE.PointLight(0xff9f45, baseLightIntensity, 4.35, 1.86); light.position.copy(flame.position); - light.castShadow = false; + light.castShadow = true; + light.shadow.mapSize.set(1024, 1024); + light.shadow.bias = -0.00004; + light.shadow.normalBias = 0.018; + light.shadow.radius = 5; + light.shadow.blurSamples = 12; + light.shadow.camera.near = 0.04; + light.shadow.camera.far = 5.0; candle.add(light); candle.userData = { @@ -486,6 +515,7 @@ function configureTableShader(material) { shader.uniforms.candleBodyData = { value: [new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2()] }; + shader.uniforms.tableDebugMode = { value: tableDebugMode }; shader.vertexShader = shader.vertexShader .replace( @@ -508,6 +538,7 @@ function configureTableShader(material) { uniform vec3 candleBodyPositions[3]; uniform vec3 candleFlamePositions[3]; uniform vec2 candleBodyData[3]; + uniform int tableDebugMode; varying vec3 vTableWorldPosition; varying vec4 vSceneReflectionCoord; @@ -536,41 +567,68 @@ function configureTableShader(material) { sampleRoomReflection(dir - bitangent * 0.026) * 0.11; } - float candleBodyOcclusion(vec3 point, vec3 flame, vec3 body, vec2 bodyData) { - vec3 ray = point - flame; - float rayLen = max(length(ray), 0.0001); - vec3 dir = ray / rayLen; - float t = clamp(dot(body - flame, dir), 0.0, rayLen); - vec3 closest = flame + dir * t; - float lateral = length((body - closest).xz); + float candleBodyOcclusion(vec3 point, vec3 flame, vec3 body, vec2 bodyData, float selfLight) { + vec3 segment = point - flame; + vec2 segmentXZ = segment.xz; + vec2 flameToBody = flame.xz - body.xz; + float radius = bodyData.x; + float a = max(dot(segmentXZ, segmentXZ), 0.000001); + float b = 2.0 * dot(flameToBody, segmentXZ); + float c = dot(flameToBody, flameToBody) - radius * radius; + float discriminant = b * b - 4.0 * a * c; + float nearestT = clamp(-dot(flameToBody, segmentXZ) / a, 0.0, 1.0); + vec2 nearestXZ = flameToBody + segmentXZ * nearestT; + float nearestDistance = length(nearestXZ); + float hitT = nearestT; + float cylinderHit = 0.0; + if (discriminant > 0.0) { + float sqrtDisc = sqrt(discriminant); + float t0 = (-b - sqrtDisc) / (2.0 * a); + float t1 = (-b + sqrtDisc) / (2.0 * a); + float validT0 = step(0.0, t0) * step(t0, 1.0); + float validT1 = step(0.0, t1) * step(t1, 1.0); + hitT = mix(hitT, t0, validT0); + hitT = mix(hitT, t1, (1.0 - validT0) * validT1); + cylinderHit = max(validT0, validT1); + } + float closestY = flame.y + segment.y * hitT; float bodyTop = body.y + bodyData.y; - float vertical = smoothstep(body.y - 0.05, body.y + 0.14, closest.y) * - (1.0 - smoothstep(bodyTop - 0.12, bodyTop + 0.08, closest.y)); - float sourceDistance = length(flame - body); - float penumbra = bodyData.x * (2.2 + sourceDistance * 1.65 + rayLen * 0.34); - float umbra = 1.0 - smoothstep(bodyData.x * 0.45, bodyData.x * 1.16, lateral); - float softEdge = 1.0 - smoothstep(bodyData.x * 1.05, penumbra, lateral); - float travelFade = 1.0 - smoothstep(1.65, 4.2, rayLen); - float waxTransmission = 0.38 + 0.28 * smoothstep(bodyTop - 0.32, bodyTop + 0.05, closest.y); - return clamp((umbra * 0.34 + softEdge * 0.2) * vertical * travelFade * waxTransmission, 0.0, 0.38); + float vertical = smoothstep(body.y - 0.045, body.y + 0.08, closestY) * + (1.0 - smoothstep(bodyTop - 0.08, bodyTop + 0.045, closestY)); + float segmentLength = length(segment); + float penumbraWidth = radius * (0.45 + segmentLength * 0.12); + float exactHit = cylinderHit; + float softHit = 1.0 - smoothstep(radius, radius + penumbraWidth, nearestDistance); + float selfShadowLimiter = mix(1.0, 0.06, selfLight); + float waxExitHeight = smoothstep(body.y, bodyTop, closestY); + float waxTransmission = 0.48 + 0.34 * waxExitHeight; + float bodyOpacity = 1.0 - waxTransmission * mix(0.62, 0.86, selfLight); + return clamp(max(exactHit, softHit * 0.72) * vertical * selfShadowLimiter * bodyOpacity, 0.0, 0.42); } float candleContactOcclusion(vec3 point, vec3 body, vec2 bodyData) { vec2 delta = point.xz - body.xz; float base = 1.0 - smoothstep(bodyData.x * 0.72, bodyData.x * 2.55, length(delta)); - return base * 0.18; + return base * 0.32; } - float candleOcclusionField(vec3 point) { + float candleContactField(vec3 point) { float contact = 0.0; - float projectedShadow = 0.0; for (int bodyIndex = 0; bodyIndex < 3; bodyIndex++) { contact = max(contact, candleContactOcclusion(point, candleBodyPositions[bodyIndex], candleBodyData[bodyIndex])); + } + return clamp(contact, 0.0, 0.36); + } + + float candleProjectedShadowField(vec3 point) { + float projectedShadow = 0.0; + for (int bodyIndex = 0; bodyIndex < 3; bodyIndex++) { for (int flameIndex = 0; flameIndex < 3; flameIndex++) { - projectedShadow = max(projectedShadow, candleBodyOcclusion(point, candleFlamePositions[flameIndex], candleBodyPositions[bodyIndex], candleBodyData[bodyIndex])); + float selfLight = bodyIndex == flameIndex ? 1.0 : 0.0; + projectedShadow = max(projectedShadow, candleBodyOcclusion(point, candleFlamePositions[flameIndex], candleBodyPositions[bodyIndex], candleBodyData[bodyIndex], selfLight)); } } - return clamp(contact + projectedShadow, 0.0, 0.46); + return clamp(projectedShadow, 0.0, 0.46); }` ) .replace( @@ -588,25 +646,34 @@ function configureTableShader(material) { smoothstep(0.0, 0.08, sceneReflectionUv.y) * smoothstep(0.0, 0.08, 1.0 - sceneReflectionUv.x) * smoothstep(0.0, 0.08, 1.0 - sceneReflectionUv.y); - vec2 roughReflectionUv = sceneReflectionUv + normal.xz * 0.012; + vec2 roughReflectionUv = sceneReflectionUv + normal.xz * 0.024; vec3 sceneReflection = texture2D(sceneReflectionMap, roughReflectionUv).rgb; sceneReflection = pow(max(sceneReflection, vec3(0.0)), vec3(0.88)) * sceneReflectionInBounds * sceneReflectionEdge; vec2 tableDustUv = clamp(vec2(vTableWorldPosition.x / 9.8 + 0.5, 0.5 - vTableWorldPosition.z / 6.6), vec2(0.0), vec2(1.0)); float dust = texture2D(tableDustMap, tableDustUv).r; - float reflectionCleanliness = 1.0 - dust * 0.52; - vec3 combinedReflection = (roomReflection * 0.22 + sceneReflection * 0.32) * reflectionCleanliness; + float reflectionCleanliness = 1.0 - dust * 0.62; + vec3 combinedReflection = (roomReflection * 0.18 + sceneReflection * 0.5) * reflectionCleanliness; float fresnel = pow(1.0 - max(dot(viewDirWorld, tableNormalWorld), 0.0), 1.85); float tableReflectionMask = smoothstep(-0.095, -0.025, vTableWorldPosition.y); vec3 reflectedSurface = combinedReflection * (0.56 + fresnel * 0.44); - vec3 reflectionLift = max(reflectedSurface - outgoingLight * 0.18, vec3(0.0)); float contactAo = 1.0 - smoothstep(0.0, 0.9, length(vTableWorldPosition.xz * vec2(0.34, 0.58))) * 0.16; float dustDulling = dust * tableReflectionMask; - float candleOcclusion = candleOcclusionField(vTableWorldPosition) * tableReflectionMask; + float candleContact = candleContactField(vTableWorldPosition) * tableReflectionMask; + float candleProjectedShadow = candleProjectedShadowField(vTableWorldPosition) * tableReflectionMask; + float candleOcclusion = clamp(candleContact + candleProjectedShadow * 0.42, 0.0, 0.48); + vec3 normalDebug = normalize(normal) * 0.5 + 0.5; outgoingLight *= mix(1.0, contactAo, tableReflectionMask * 0.55); - outgoingLight += tableReflectionMask * reflectionLift * (0.12 + fresnel * 0.2) * reflectionCleanliness; - outgoingLight += tableReflectionMask * combinedReflection * 0.012; - outgoingLight = mix(outgoingLight, outgoingLight * vec3(0.78, 0.74, 0.66) + vec3(0.035, 0.028, 0.02), dustDulling * 0.18); - outgoingLight *= mix(vec3(1.0), vec3(0.62, 0.52, 0.42), candleOcclusion); + outgoingLight = mix(outgoingLight, reflectedSurface, tableReflectionMask * (0.07 + fresnel * 0.14) * reflectionCleanliness); + outgoingLight += tableReflectionMask * roomReflection * 0.008 * reflectionCleanliness; + outgoingLight = mix(outgoingLight, outgoingLight * vec3(0.72, 0.68, 0.6) + vec3(0.045, 0.036, 0.025), dustDulling * 0.32); + outgoingLight *= mix(vec3(1.0), vec3(0.5, 0.4, 0.31), candleOcclusion); + if (tableDebugMode == 1) outgoingLight = vec3(candleProjectedShadow); + if (tableDebugMode == 2) outgoingLight = vec3(dust); + if (tableDebugMode == 3) outgoingLight = normalDebug; + if (tableDebugMode == 4) outgoingLight = roomReflection; + if (tableDebugMode == 5) outgoingLight = sceneReflection; + if (tableDebugMode == 6) outgoingLight = vec3(tableReflectionMask); + if (tableDebugMode == 7) outgoingLight = vec3(candleContact); #include ` ); };