Add table shader diagnostics and candle shadow model

This commit is contained in:
2026-06-04 11:40:55 +02:00
parent 199462442c
commit bdec4590d2
+101 -34
View File
@@ -2,6 +2,23 @@ import * as THREE from 'https://esm.sh/three@0.165.0';
const canvas = document.getElementById('scene'); const canvas = document.getElementById('scene');
canvas.style.cursor = 'grab'; 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 }); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.outputColorSpace = THREE.SRGBColorSpace;
@@ -59,6 +76,11 @@ const cameraRig = {
pointerY: 0, pointerY: 0,
keys: new Set() 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); updateCameraRig(0);
const clock = new THREE.Clock(); const clock = new THREE.Clock();
@@ -162,11 +184,11 @@ function buildTable() {
color: 0x8a4c22, color: 0x8a4c22,
map: tableTexture, map: tableTexture,
normalMap: tableNormal, normalMap: tableNormal,
normalScale: new THREE.Vector2(0.07, 0.07), normalScale: new THREE.Vector2(0.22, 0.18),
roughness: 0.31, roughness: 0.38,
metalness: 0, metalness: 0,
clearcoat: 0.54, clearcoat: 0.54,
clearcoatRoughness: 0.42, clearcoatRoughness: 0.48,
reflectivity: 0.34, reflectivity: 0.34,
envMapIntensity: 0 envMapIntensity: 0
}); });
@@ -238,7 +260,14 @@ function addCandle(x, y, z, intensity, height) {
const baseLightIntensity = intensity * 7.4; const baseLightIntensity = intensity * 7.4;
const light = new THREE.PointLight(0xff9f45, baseLightIntensity, 4.35, 1.86); const light = new THREE.PointLight(0xff9f45, baseLightIntensity, 4.35, 1.86);
light.position.copy(flame.position); 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.add(light);
candle.userData = { candle.userData = {
@@ -486,6 +515,7 @@ function configureTableShader(material) {
shader.uniforms.candleBodyData = { shader.uniforms.candleBodyData = {
value: [new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2()] value: [new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2()]
}; };
shader.uniforms.tableDebugMode = { value: tableDebugMode };
shader.vertexShader = shader.vertexShader shader.vertexShader = shader.vertexShader
.replace( .replace(
@@ -508,6 +538,7 @@ function configureTableShader(material) {
uniform vec3 candleBodyPositions[3]; uniform vec3 candleBodyPositions[3];
uniform vec3 candleFlamePositions[3]; uniform vec3 candleFlamePositions[3];
uniform vec2 candleBodyData[3]; uniform vec2 candleBodyData[3];
uniform int tableDebugMode;
varying vec3 vTableWorldPosition; varying vec3 vTableWorldPosition;
varying vec4 vSceneReflectionCoord; varying vec4 vSceneReflectionCoord;
@@ -536,41 +567,68 @@ function configureTableShader(material) {
sampleRoomReflection(dir - bitangent * 0.026) * 0.11; sampleRoomReflection(dir - bitangent * 0.026) * 0.11;
} }
float candleBodyOcclusion(vec3 point, vec3 flame, vec3 body, vec2 bodyData) { float candleBodyOcclusion(vec3 point, vec3 flame, vec3 body, vec2 bodyData, float selfLight) {
vec3 ray = point - flame; vec3 segment = point - flame;
float rayLen = max(length(ray), 0.0001); vec2 segmentXZ = segment.xz;
vec3 dir = ray / rayLen; vec2 flameToBody = flame.xz - body.xz;
float t = clamp(dot(body - flame, dir), 0.0, rayLen); float radius = bodyData.x;
vec3 closest = flame + dir * t; float a = max(dot(segmentXZ, segmentXZ), 0.000001);
float lateral = length((body - closest).xz); 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 bodyTop = body.y + bodyData.y;
float vertical = smoothstep(body.y - 0.05, body.y + 0.14, closest.y) * float vertical = smoothstep(body.y - 0.045, body.y + 0.08, closestY) *
(1.0 - smoothstep(bodyTop - 0.12, bodyTop + 0.08, closest.y)); (1.0 - smoothstep(bodyTop - 0.08, bodyTop + 0.045, closestY));
float sourceDistance = length(flame - body); float segmentLength = length(segment);
float penumbra = bodyData.x * (2.2 + sourceDistance * 1.65 + rayLen * 0.34); float penumbraWidth = radius * (0.45 + segmentLength * 0.12);
float umbra = 1.0 - smoothstep(bodyData.x * 0.45, bodyData.x * 1.16, lateral); float exactHit = cylinderHit;
float softEdge = 1.0 - smoothstep(bodyData.x * 1.05, penumbra, lateral); float softHit = 1.0 - smoothstep(radius, radius + penumbraWidth, nearestDistance);
float travelFade = 1.0 - smoothstep(1.65, 4.2, rayLen); float selfShadowLimiter = mix(1.0, 0.06, selfLight);
float waxTransmission = 0.38 + 0.28 * smoothstep(bodyTop - 0.32, bodyTop + 0.05, closest.y); float waxExitHeight = smoothstep(body.y, bodyTop, closestY);
return clamp((umbra * 0.34 + softEdge * 0.2) * vertical * travelFade * waxTransmission, 0.0, 0.38); 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) { float candleContactOcclusion(vec3 point, vec3 body, vec2 bodyData) {
vec2 delta = point.xz - body.xz; vec2 delta = point.xz - body.xz;
float base = 1.0 - smoothstep(bodyData.x * 0.72, bodyData.x * 2.55, length(delta)); 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 contact = 0.0;
float projectedShadow = 0.0;
for (int bodyIndex = 0; bodyIndex < 3; bodyIndex++) { for (int bodyIndex = 0; bodyIndex < 3; bodyIndex++) {
contact = max(contact, candleContactOcclusion(point, candleBodyPositions[bodyIndex], candleBodyData[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++) { 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( .replace(
@@ -588,25 +646,34 @@ function configureTableShader(material) {
smoothstep(0.0, 0.08, sceneReflectionUv.y) * smoothstep(0.0, 0.08, sceneReflectionUv.y) *
smoothstep(0.0, 0.08, 1.0 - sceneReflectionUv.x) * smoothstep(0.0, 0.08, 1.0 - sceneReflectionUv.x) *
smoothstep(0.0, 0.08, 1.0 - sceneReflectionUv.y); 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; vec3 sceneReflection = texture2D(sceneReflectionMap, roughReflectionUv).rgb;
sceneReflection = pow(max(sceneReflection, vec3(0.0)), vec3(0.88)) * sceneReflectionInBounds * sceneReflectionEdge; 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)); 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 dust = texture2D(tableDustMap, tableDustUv).r;
float reflectionCleanliness = 1.0 - dust * 0.52; float reflectionCleanliness = 1.0 - dust * 0.62;
vec3 combinedReflection = (roomReflection * 0.22 + sceneReflection * 0.32) * reflectionCleanliness; vec3 combinedReflection = (roomReflection * 0.18 + sceneReflection * 0.5) * reflectionCleanliness;
float fresnel = pow(1.0 - max(dot(viewDirWorld, tableNormalWorld), 0.0), 1.85); float fresnel = pow(1.0 - max(dot(viewDirWorld, tableNormalWorld), 0.0), 1.85);
float tableReflectionMask = smoothstep(-0.095, -0.025, vTableWorldPosition.y); float tableReflectionMask = smoothstep(-0.095, -0.025, vTableWorldPosition.y);
vec3 reflectedSurface = combinedReflection * (0.56 + fresnel * 0.44); 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 contactAo = 1.0 - smoothstep(0.0, 0.9, length(vTableWorldPosition.xz * vec2(0.34, 0.58))) * 0.16;
float dustDulling = dust * tableReflectionMask; 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 *= mix(1.0, contactAo, tableReflectionMask * 0.55);
outgoingLight += tableReflectionMask * reflectionLift * (0.12 + fresnel * 0.2) * reflectionCleanliness; outgoingLight = mix(outgoingLight, reflectedSurface, tableReflectionMask * (0.07 + fresnel * 0.14) * reflectionCleanliness);
outgoingLight += tableReflectionMask * combinedReflection * 0.012; outgoingLight += tableReflectionMask * roomReflection * 0.008 * reflectionCleanliness;
outgoingLight = mix(outgoingLight, outgoingLight * vec3(0.78, 0.74, 0.66) + vec3(0.035, 0.028, 0.02), dustDulling * 0.18); 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.62, 0.52, 0.42), candleOcclusion); 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 <opaque_fragment>` #include <opaque_fragment>`
); );
}; };