Add table shader diagnostics and candle shadow model
This commit is contained in:
+101
-34
@@ -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 <opaque_fragment>`
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user