Refine WebGL table surface contamination

This commit is contained in:
2026-06-04 13:37:41 +02:00
parent e1396d44bb
commit 444acb6229
+134 -26
View File
@@ -10,7 +10,8 @@ const tableDebugModes = {
room: 4,
scene: 5,
mask: 6,
ao: 7
ao: 7,
grease: 8
};
const urlParams = new URLSearchParams(window.location.search);
const tableDebugName = urlParams.get('tableDebug') || 'none';
@@ -40,6 +41,7 @@ let tableMesh = null;
let tableShader = null;
let tableRoomReflectionTexture = createRoomReflectionTexture();
let tableDustTexture = null;
let tableGreaseTexture = null;
const tableTopY = -0.02;
const tableReflectionTarget = new THREE.WebGLRenderTarget(4096, 2304, {
colorSpace: THREE.SRGBColorSpace,
@@ -183,17 +185,21 @@ function buildTable() {
tableDustTexture.wrapS = THREE.ClampToEdgeWrapping;
tableDustTexture.wrapT = THREE.ClampToEdgeWrapping;
tableDustTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
tableGreaseTexture = createTableGreaseTexture();
tableGreaseTexture.wrapS = THREE.ClampToEdgeWrapping;
tableGreaseTexture.wrapT = THREE.ClampToEdgeWrapping;
tableGreaseTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
const tableMaterial = new THREE.MeshPhysicalMaterial({
color: 0x8a4c22,
map: tableTexture,
normalMap: tableNormal,
normalScale: new THREE.Vector2(0.22, 0.18),
roughness: 0.38,
roughness: 0.42,
metalness: 0,
clearcoat: 0.54,
clearcoatRoughness: 0.48,
reflectivity: 0.34,
clearcoat: 0.32,
clearcoatRoughness: 0.58,
reflectivity: 0.18,
envMapIntensity: 0
});
configureTableShader(tableMaterial);
@@ -510,6 +516,7 @@ function configureTableShader(material) {
shader.uniforms.sceneReflectionMap = { value: tableReflectionTarget.texture };
shader.uniforms.sceneReflectionMatrix = { value: tableReflectionMatrix };
shader.uniforms.tableDustMap = { value: tableDustTexture };
shader.uniforms.tableGreaseMap = { value: tableGreaseTexture };
shader.uniforms.candleBodyPositions = {
value: [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]
};
@@ -538,6 +545,7 @@ function configureTableShader(material) {
uniform sampler2D roomReflectionMap;
uniform sampler2D sceneReflectionMap;
uniform sampler2D tableDustMap;
uniform sampler2D tableGreaseMap;
uniform mat4 sceneReflectionMatrix;
uniform vec3 candleBodyPositions[3];
uniform vec3 candleFlamePositions[3];
@@ -546,6 +554,22 @@ function configureTableShader(material) {
varying vec3 vTableWorldPosition;
varying vec4 vSceneReflectionCoord;
vec2 tableDustUvFromWorld(vec3 worldPosition) {
return clamp(vec2(worldPosition.x / 9.8 + 0.5, 0.5 - worldPosition.z / 6.6), vec2(0.0), vec2(1.0));
}
float tableTopMaskFromWorld(vec3 worldPosition) {
return smoothstep(-0.095, -0.025, worldPosition.y);
}
float tableDustFromWorld(vec3 worldPosition) {
return texture2D(tableDustMap, tableDustUvFromWorld(worldPosition)).r * tableTopMaskFromWorld(worldPosition);
}
float tableGreaseFromWorld(vec3 worldPosition) {
return texture2D(tableGreaseMap, tableDustUvFromWorld(worldPosition)).r * tableTopMaskFromWorld(worldPosition);
}
vec3 rotateRoomReflection(vec3 dir) {
const float yaw = 0.42;
float s = sin(yaw);
@@ -635,6 +659,16 @@ function configureTableShader(material) {
return clamp(projectedShadow, 0.0, 0.46);
}`
)
.replace(
'#include <roughnessmap_fragment>',
`#include <roughnessmap_fragment>
float tableSpecularGrease = tableGreaseFromWorld(vTableWorldPosition);
float tableSpecularDust = tableDustFromWorld(vTableWorldPosition) * (1.0 - smoothstep(0.025, 0.18, tableSpecularGrease) * 0.82);
float tableSpecularDustFilm = smoothstep(0.006, 0.034, tableSpecularDust);
float tableSpecularGreaseFilm = smoothstep(0.016, 0.14, tableSpecularGrease);
roughnessFactor = clamp(mix(roughnessFactor, 0.84, tableSpecularDustFilm * 0.32), 0.04, 1.0);
roughnessFactor = clamp(mix(roughnessFactor, 0.34, tableSpecularGreaseFilm * 0.3), 0.04, 1.0);`
)
.replace(
'#include <opaque_fragment>',
`vec3 viewDirWorld = normalize(cameraPosition - vTableWorldPosition);
@@ -652,23 +686,32 @@ function configureTableShader(material) {
smoothstep(0.0, 0.08, 1.0 - sceneReflectionUv.y);
vec3 sceneReflection = texture2D(sceneReflectionMap, sceneReflectionUv).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.62;
vec3 combinedReflection = (roomReflection * 0.18 + sceneReflection * 0.5) * reflectionCleanliness;
float grease = tableGreaseFromWorld(vTableWorldPosition);
float greaseDustWipe = smoothstep(0.016, 0.13, grease);
float dust = tableDustFromWorld(vTableWorldPosition) * (1.0 - greaseDustWipe * 0.82);
float dustFilm = smoothstep(0.006, 0.034, dust);
float greaseFilm = smoothstep(0.016, 0.14, grease);
float reflectionCleanliness = 1.0 - dustFilm * 0.2 - greaseFilm * 0.06;
vec3 dustBlurredSceneReflection = sceneReflection * 0.78 + roomReflection * 0.055;
vec3 greaseBlurredSceneReflection = sceneReflection * 0.7 + roomReflection * 0.18;
vec3 dustAwareSceneReflection = mix(sceneReflection, dustBlurredSceneReflection, dustFilm);
dustAwareSceneReflection = mix(dustAwareSceneReflection, greaseBlurredSceneReflection, greaseFilm);
vec3 combinedReflection = (roomReflection * 0.14 + dustAwareSceneReflection * 0.86) * 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 reflectedSurface = combinedReflection * (0.64 + fresnel * 0.36);
float reflectedLuma = dot(reflectedSurface, vec3(0.299, 0.587, 0.114));
reflectedSurface = mix(reflectedSurface, vec3(reflectedLuma), dustFilm * 0.42);
float contactAo = 1.0 - smoothstep(0.0, 0.9, length(vTableWorldPosition.xz * vec2(0.34, 0.58))) * 0.16;
float dustDulling = dust * 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(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(outgoingLight, reflectedSurface, tableReflectionMask * (0.16 + fresnel * 0.28 + greaseFilm * 0.065) * reflectionCleanliness);
outgoingLight += tableReflectionMask * roomReflection * 0.004 * reflectionCleanliness;
outgoingLight += tableReflectionMask * dustFilm * vec3(0.008, 0.0085, 0.009) * (0.22 + fresnel * 0.62);
outgoingLight += tableReflectionMask * greaseFilm * vec3(0.018, 0.013, 0.007) * (0.28 + fresnel * 0.58);
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);
@@ -677,6 +720,7 @@ function configureTableShader(material) {
if (tableDebugMode == 5) outgoingLight = sceneReflection;
if (tableDebugMode == 6) outgoingLight = vec3(tableReflectionMask);
if (tableDebugMode == 7) outgoingLight = vec3(candleContact);
if (tableDebugMode == 8) outgoingLight = vec3(grease);
#include <opaque_fragment>`
);
};
@@ -965,10 +1009,10 @@ function createTableNormalTexture() {
function createTableDustTexture() {
const canvas = document.createElement('canvas');
generatedTextureCanvases.tableDust = canvas;
canvas.width = 2048;
canvas.height = 2048;
canvas.width = 4096;
canvas.height = 4096;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgb(10, 10, 10)';
ctx.fillStyle = 'rgb(1, 1, 1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const image = ctx.getImageData(0, 0, canvas.width, canvas.height);
@@ -977,11 +1021,11 @@ function createTableDustTexture() {
const i = (y * canvas.width + x) * 4;
const nx = x / canvas.width;
const ny = y / canvas.height;
const edgeDust = Math.max(0, 1 - Math.min(nx, ny, 1 - nx, 1 - ny) * 9);
const grain = Math.random() < 0.018 ? 110 + Math.random() * 85 : Math.random() * 18;
const sweep = Math.max(0, Math.sin(nx * 32 + Math.sin(ny * 15) * 2.5) - 0.84) * 55;
const pageDust = Math.exp(-Math.pow((nx - 0.5) * 2.2, 2) - Math.pow((ny - 0.5) * 1.4, 2)) * 16;
const value = Math.min(255, 10 + edgeDust * 36 + grain + sweep + pageDust);
const edgeDust = Math.max(0, 1 - Math.min(nx, ny, 1 - nx, 1 - ny) * 18);
const microNoise = Math.pow(Math.random(), 7.2) * 5.5;
const fineFilm = Math.max(0, Math.sin(nx * 21 + Math.sin(ny * 11) * 0.7) - 0.988) * 3;
const bookShelter = Math.exp(-Math.pow((nx - 0.5) * 2.7, 2) - Math.pow((ny - 0.5) * 1.8, 2)) * 1.5;
const value = Math.min(255, 1 + edgeDust * 3 + microNoise + fineFilm + bookShelter);
image.data[i] = value;
image.data[i + 1] = value;
image.data[i + 2] = value;
@@ -990,6 +1034,36 @@ function createTableDustTexture() {
}
ctx.putImageData(image, 0, 0);
ctx.globalCompositeOperation = 'screen';
ctx.fillStyle = 'rgba(230, 230, 230, 0.028)';
for (let i = 0; i < 1700; i += 1) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const radius = 0.08 + Math.random() * 0.22;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalCompositeOperation = 'source-over';
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.NoColorSpace;
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.generateMipmaps = true;
texture.needsUpdate = true;
return texture;
}
function createTableGreaseTexture() {
const canvas = document.createElement('canvas');
generatedTextureCanvases.tableGrease = canvas;
canvas.width = 4096;
canvas.height = 4096;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const smudge = (x, y, rx, ry, alpha) => {
const gradient = ctx.createRadialGradient(x, y, 0, x, y, Math.max(rx, ry));
gradient.addColorStop(0, `rgba(220, 220, 220, ${alpha})`);
@@ -1005,10 +1079,44 @@ function createTableDustTexture() {
ctx.restore();
};
smudge(canvas.width * 0.17, canvas.height * 0.38, 170, 70, 0.12);
smudge(canvas.width * 0.83, canvas.height * 0.24, 120, 58, 0.1);
smudge(canvas.width * 0.73, canvas.height * 0.76, 155, 64, 0.09);
smudge(canvas.width * 0.5, canvas.height * 0.52, 260, 110, 0.055);
smudge(canvas.width * 0.17, canvas.height * 0.38, 210, 88, 0.088);
smudge(canvas.width * 0.83, canvas.height * 0.24, 170, 76, 0.081);
smudge(canvas.width * 0.73, canvas.height * 0.76, 185, 78, 0.072);
smudge(canvas.width * 0.5, canvas.height * 0.52, 300, 120, 0.053);
const fingerprint = (x, y, rx, ry, rotation, alpha) => {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, Math.max(rx, ry));
gradient.addColorStop(0, `rgba(235, 235, 235, ${alpha})`);
gradient.addColorStop(0.68, `rgba(200, 200, 200, ${alpha * 0.48})`);
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);
ctx.clip();
ctx.strokeStyle = `rgba(245, 245, 245, ${alpha * 0.42})`;
ctx.lineWidth = 2.4;
for (let ridge = -0.7; ridge <= 0.7; ridge += 0.18) {
ctx.beginPath();
ctx.ellipse(rx * ridge * 0.18, ry * ridge * 0.24, rx * (0.26 + Math.abs(ridge) * 0.58), ry * (0.2 + Math.abs(ridge) * 0.5), 0, Math.PI * 0.08, Math.PI * 1.92);
ctx.stroke();
}
ctx.restore();
};
ctx.globalCompositeOperation = 'screen';
for (let i = 0; i < 8; i += 1) {
const x = canvas.width * (0.2 + Math.random() * 0.62);
const y = canvas.height * (0.18 + Math.random() * 0.64);
fingerprint(x, y, 36 + Math.random() * 22, 14 + Math.random() * 8, Math.random() * Math.PI, 0.102 + Math.random() * 0.042);
}
ctx.globalCompositeOperation = 'source-over';
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.NoColorSpace;