Stabilize WebGL lighting lab
This commit is contained in:
+412
-175
@@ -1,4 +1,9 @@
|
||||
import * as THREE from 'https://esm.sh/three@0.165.0';
|
||||
import { EffectComposer } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/EffectComposer.js';
|
||||
import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/RenderPass.js';
|
||||
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.js';
|
||||
import { SMAAPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SMAAPass.js';
|
||||
import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
|
||||
|
||||
const canvas = document.getElementById('scene');
|
||||
canvas.style.cursor = 'grab';
|
||||
@@ -18,7 +23,7 @@ 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}`;
|
||||
labStatus.textContent = tableDebugName === 'ao' ? 'scene debug: SSAO' : `table debug: ${tableDebugName}`;
|
||||
}
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
@@ -32,6 +37,13 @@ const generatedTextureCanvases = {};
|
||||
const maxTextureAnisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||
const reflectionPixelRatio = Math.min(window.devicePixelRatio || 1, 2);
|
||||
const reflectionTargetSize = new THREE.Vector2();
|
||||
let sceneComposerTarget = null;
|
||||
let composer = null;
|
||||
let sceneRenderPass = null;
|
||||
let sceneAoPass = null;
|
||||
let sceneSmaaPass = null;
|
||||
let sceneOutputPass = null;
|
||||
const aoExcludedObjects = [];
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x080604);
|
||||
@@ -66,8 +78,33 @@ const reflectionUp = new THREE.Vector3();
|
||||
const candleShadowSources = [];
|
||||
const candleWorldPosition = new THREE.Vector3();
|
||||
const flameWorldPosition = new THREE.Vector3();
|
||||
const bookShadowMapSize = 1536;
|
||||
const bookShadowTargets = Array.from({ length: 3 }, () => {
|
||||
const target = new THREE.WebGLRenderTarget(bookShadowMapSize, bookShadowMapSize, {
|
||||
colorSpace: THREE.NoColorSpace,
|
||||
depthBuffer: true,
|
||||
stencilBuffer: false
|
||||
});
|
||||
target.texture.colorSpace = THREE.NoColorSpace;
|
||||
target.texture.minFilter = THREE.LinearFilter;
|
||||
target.texture.magFilter = THREE.LinearFilter;
|
||||
target.texture.generateMipmaps = false;
|
||||
return target;
|
||||
});
|
||||
const bookShadowCameras = Array.from({ length: 3 }, () => new THREE.PerspectiveCamera(78, 1, 0.03, 7.2));
|
||||
const bookShadowMatrices = Array.from({ length: 3 }, () => new THREE.Matrix4());
|
||||
const bookShadowBiasMatrix = new THREE.Matrix4().set(
|
||||
0.5, 0, 0, 0.5,
|
||||
0, 0.5, 0, 0.5,
|
||||
0, 0, 0.5, 0.5,
|
||||
0, 0, 0, 1
|
||||
);
|
||||
const bookShadowDepthMaterial = new THREE.MeshDepthMaterial({
|
||||
depthPacking: THREE.RGBADepthPacking
|
||||
});
|
||||
bookShadowDepthMaterial.blending = THREE.NoBlending;
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(28, 1, 0.1, 80);
|
||||
const camera = new THREE.PerspectiveCamera(28, 1, 0.1, 40);
|
||||
const cameraRig = {
|
||||
target: new THREE.Vector3(0, 0.16, -0.04),
|
||||
yaw: 0,
|
||||
@@ -89,6 +126,8 @@ if (urlParams.get('view') === 'wide') {
|
||||
}
|
||||
updateCameraRig(0);
|
||||
|
||||
configureScenePostprocessing();
|
||||
|
||||
const clock = new THREE.Clock();
|
||||
const book = new THREE.Group();
|
||||
scene.add(book);
|
||||
@@ -153,12 +192,27 @@ const materials = {
|
||||
})
|
||||
};
|
||||
|
||||
configureBookShadowReceiver(materials.leather, 0.52);
|
||||
configureBookShadowReceiver(materials.coverEdge, 0.42);
|
||||
configureBookShadowReceiver(materials.pageBlock, 0.46);
|
||||
configureBookShadowReceiver(materials.pageEdge, 0.34);
|
||||
configureBookShadowReceiver(materials.leftPage, 0.38);
|
||||
configureBookShadowReceiver(materials.rightPage, 0.38);
|
||||
|
||||
buildTable();
|
||||
buildLighting();
|
||||
buildBook();
|
||||
loadAiRoomReflection();
|
||||
window.BookLabDebug = {
|
||||
textures: generatedTextureCanvases,
|
||||
ready: false,
|
||||
renderedFrames: 0,
|
||||
get sceneAoPass() {
|
||||
return sceneAoPass;
|
||||
},
|
||||
get composer() {
|
||||
return composer;
|
||||
},
|
||||
exportTexture(name) {
|
||||
return generatedTextureCanvases[name]?.toDataURL('image/png') || null;
|
||||
}
|
||||
@@ -176,16 +230,16 @@ function buildTable() {
|
||||
tableTexture.wrapT = THREE.RepeatWrapping;
|
||||
tableTexture.repeat.set(2.15, 1.45);
|
||||
tableTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||
const tableNormal = createTableNormalTexture();
|
||||
const tableNormal = loadUtilityTexture('/assets/webgl/table_normal_2k.png');
|
||||
tableNormal.wrapS = THREE.RepeatWrapping;
|
||||
tableNormal.wrapT = THREE.RepeatWrapping;
|
||||
tableNormal.repeat.set(2.15, 1.45);
|
||||
tableNormal.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||
tableDustTexture = createTableDustTexture();
|
||||
tableDustTexture = loadUtilityTexture('/assets/webgl/table_dust_4k.png');
|
||||
tableDustTexture.wrapS = THREE.ClampToEdgeWrapping;
|
||||
tableDustTexture.wrapT = THREE.ClampToEdgeWrapping;
|
||||
tableDustTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||
tableGreaseTexture = createTableGreaseTexture();
|
||||
tableGreaseTexture = loadUtilityTexture('/assets/webgl/table_grease_4k.png');
|
||||
tableGreaseTexture.wrapS = THREE.ClampToEdgeWrapping;
|
||||
tableGreaseTexture.wrapT = THREE.ClampToEdgeWrapping;
|
||||
tableGreaseTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||
@@ -205,10 +259,172 @@ function buildTable() {
|
||||
configureTableShader(tableMaterial);
|
||||
tableMesh = new THREE.Mesh(new THREE.BoxGeometry(9.8, 0.28, 6.6, 1, 1, 1), tableMaterial);
|
||||
tableMesh.position.y = -0.16;
|
||||
tableMesh.receiveShadow = true;
|
||||
tableMesh.receiveShadow = false;
|
||||
scene.add(tableMesh);
|
||||
}
|
||||
|
||||
function loadUtilityTexture(url) {
|
||||
const texture = new THREE.TextureLoader().load(url);
|
||||
texture.colorSpace = THREE.NoColorSpace;
|
||||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.generateMipmaps = true;
|
||||
return texture;
|
||||
}
|
||||
|
||||
function configureBookShadowReceiver(material, strength) {
|
||||
material.customProgramCacheKey = () => `book-shadow-receiver-${strength.toFixed(2)}`;
|
||||
material.onBeforeCompile = (shader) => {
|
||||
shader.uniforms.bookShadowMaps = { value: bookShadowTargets.map((target) => target.texture) };
|
||||
shader.uniforms.bookShadowMatrices = { value: bookShadowMatrices };
|
||||
shader.uniforms.bookShadowMapTexelSize = { value: new THREE.Vector2(1 / bookShadowMapSize, 1 / bookShadowMapSize) };
|
||||
shader.uniforms.bookShadowReceiverStrength = { value: strength };
|
||||
|
||||
shader.vertexShader = shader.vertexShader
|
||||
.replace(
|
||||
'#include <common>',
|
||||
'#include <common>\nvarying vec3 vBookReceiverWorldPosition;'
|
||||
)
|
||||
.replace(
|
||||
'#include <project_vertex>',
|
||||
'vBookReceiverWorldPosition = (modelMatrix * vec4(transformed, 1.0)).xyz;\n#include <project_vertex>'
|
||||
);
|
||||
|
||||
shader.fragmentShader = shader.fragmentShader
|
||||
.replace(
|
||||
'#include <common>',
|
||||
`#include <common>
|
||||
uniform sampler2D bookShadowMaps[3];
|
||||
uniform mat4 bookShadowMatrices[3];
|
||||
uniform vec2 bookShadowMapTexelSize;
|
||||
uniform float bookShadowReceiverStrength;
|
||||
varying vec3 vBookReceiverWorldPosition;
|
||||
|
||||
float bookReceiverUnpackRGBADepth(vec4 packedDepth) {
|
||||
const vec4 unpackFactors = vec4(
|
||||
1.0 / (256.0 * 256.0 * 256.0),
|
||||
1.0 / (256.0 * 256.0),
|
||||
1.0 / 256.0,
|
||||
1.0
|
||||
);
|
||||
return dot(packedDepth, unpackFactors);
|
||||
}
|
||||
|
||||
float bookReceiverCompare(vec4 packedDepth, float currentDepth) {
|
||||
float closestDepth = bookReceiverUnpackRGBADepth(packedDepth);
|
||||
return smoothstep(0.003, 0.022, currentDepth - closestDepth - 0.0045);
|
||||
}
|
||||
|
||||
float bookReceiverSample0(vec4 shadowCoord) {
|
||||
vec3 coord = shadowCoord.xyz / max(shadowCoord.w, 0.0001);
|
||||
float inBounds = step(0.0, coord.x) * step(0.0, coord.y) *
|
||||
step(coord.x, 1.0) * step(coord.y, 1.0) * step(0.0, coord.z) * step(coord.z, 1.0);
|
||||
if (inBounds < 0.5) return 0.0;
|
||||
float shadow = 0.0;
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int y = -1; y <= 1; y++) {
|
||||
vec2 offset = vec2(float(x), float(y)) * bookShadowMapTexelSize * 1.35;
|
||||
shadow += bookReceiverCompare(texture2D(bookShadowMaps[0], coord.xy + offset), coord.z);
|
||||
}
|
||||
}
|
||||
return clamp(shadow / 9.0, 0.0, 1.0) * inBounds;
|
||||
}
|
||||
|
||||
float bookReceiverSample1(vec4 shadowCoord) {
|
||||
vec3 coord = shadowCoord.xyz / max(shadowCoord.w, 0.0001);
|
||||
float inBounds = step(0.0, coord.x) * step(0.0, coord.y) *
|
||||
step(coord.x, 1.0) * step(coord.y, 1.0) * step(0.0, coord.z) * step(coord.z, 1.0);
|
||||
if (inBounds < 0.5) return 0.0;
|
||||
float shadow = 0.0;
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int y = -1; y <= 1; y++) {
|
||||
vec2 offset = vec2(float(x), float(y)) * bookShadowMapTexelSize * 1.35;
|
||||
shadow += bookReceiverCompare(texture2D(bookShadowMaps[1], coord.xy + offset), coord.z);
|
||||
}
|
||||
}
|
||||
return clamp(shadow / 9.0, 0.0, 1.0) * inBounds;
|
||||
}
|
||||
|
||||
float bookReceiverSample2(vec4 shadowCoord) {
|
||||
vec3 coord = shadowCoord.xyz / max(shadowCoord.w, 0.0001);
|
||||
float inBounds = step(0.0, coord.x) * step(0.0, coord.y) *
|
||||
step(coord.x, 1.0) * step(coord.y, 1.0) * step(0.0, coord.z) * step(coord.z, 1.0);
|
||||
if (inBounds < 0.5) return 0.0;
|
||||
float shadow = 0.0;
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int y = -1; y <= 1; y++) {
|
||||
vec2 offset = vec2(float(x), float(y)) * bookShadowMapTexelSize * 1.35;
|
||||
shadow += bookReceiverCompare(texture2D(bookShadowMaps[2], coord.xy + offset), coord.z);
|
||||
}
|
||||
}
|
||||
return clamp(shadow / 9.0, 0.0, 1.0) * inBounds;
|
||||
}
|
||||
|
||||
float bookReceiverShadowField(vec3 worldPosition) {
|
||||
float shadow0 = bookReceiverSample0(bookShadowMatrices[0] * vec4(worldPosition, 1.0));
|
||||
float shadow1 = bookReceiverSample1(bookShadowMatrices[1] * vec4(worldPosition, 1.0));
|
||||
float shadow2 = bookReceiverSample2(bookShadowMatrices[2] * vec4(worldPosition, 1.0));
|
||||
return clamp(max(max(shadow0, shadow1), shadow2), 0.0, 1.0);
|
||||
}`
|
||||
)
|
||||
.replace(
|
||||
'#include <opaque_fragment>',
|
||||
`float bookReceiverShadow = bookReceiverShadowField(vBookReceiverWorldPosition) * bookShadowReceiverStrength;
|
||||
outgoingLight *= mix(vec3(1.0), vec3(0.38, 0.29, 0.2), bookReceiverShadow);
|
||||
#include <opaque_fragment>`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function configureScenePostprocessing() {
|
||||
sceneComposerTarget = new THREE.WebGLRenderTarget(1, 1, {
|
||||
colorSpace: THREE.SRGBColorSpace,
|
||||
depthBuffer: true,
|
||||
stencilBuffer: false,
|
||||
samples: renderer.capabilities.isWebGL2 ? 8 : 0
|
||||
});
|
||||
sceneComposerTarget.texture.colorSpace = THREE.SRGBColorSpace;
|
||||
sceneComposerTarget.texture.minFilter = THREE.LinearFilter;
|
||||
sceneComposerTarget.texture.magFilter = THREE.LinearFilter;
|
||||
|
||||
composer = new EffectComposer(renderer, sceneComposerTarget);
|
||||
composer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
sceneRenderPass = new RenderPass(scene, camera);
|
||||
composer.addPass(sceneRenderPass);
|
||||
|
||||
sceneAoPass = new SSAOPass(scene, camera, 1, 1, 64);
|
||||
sceneAoPass.kernelRadius = 0.48;
|
||||
sceneAoPass.minDistance = 0.00025;
|
||||
sceneAoPass.maxDistance = 0.065;
|
||||
if (tableDebugName === 'ao' && SSAOPass.OUTPUT?.Blur !== undefined) {
|
||||
sceneAoPass.output = SSAOPass.OUTPUT.Blur;
|
||||
} else if (SSAOPass.OUTPUT?.Default !== undefined) {
|
||||
sceneAoPass.output = SSAOPass.OUTPUT.Default;
|
||||
}
|
||||
const renderAoPass = sceneAoPass.render.bind(sceneAoPass);
|
||||
sceneAoPass.render = (...args) => {
|
||||
aoExcludedObjects.forEach((object) => {
|
||||
object.userData.wasVisibleForAo = object.visible;
|
||||
object.visible = false;
|
||||
});
|
||||
try {
|
||||
renderAoPass(...args);
|
||||
} finally {
|
||||
aoExcludedObjects.forEach((object) => {
|
||||
object.visible = object.userData.wasVisibleForAo;
|
||||
delete object.userData.wasVisibleForAo;
|
||||
});
|
||||
}
|
||||
};
|
||||
composer.addPass(sceneAoPass);
|
||||
|
||||
sceneSmaaPass = new SMAAPass(1, 1);
|
||||
composer.addPass(sceneSmaaPass);
|
||||
|
||||
sceneOutputPass = new OutputPass();
|
||||
composer.addPass(sceneOutputPass);
|
||||
}
|
||||
|
||||
function buildLighting() {
|
||||
scene.add(new THREE.AmbientLight(0x120b06, 0.22));
|
||||
|
||||
@@ -246,6 +462,8 @@ function addCandle(x, y, z, intensity, height) {
|
||||
waxGlow.position.copy(wax.position);
|
||||
waxGlow.castShadow = false;
|
||||
waxGlow.receiveShadow = false;
|
||||
waxGlow.userData.excludeFromAo = true;
|
||||
aoExcludedObjects.push(waxGlow);
|
||||
candle.add(waxGlow);
|
||||
|
||||
const wickTopY = height + 0.075;
|
||||
@@ -261,10 +479,17 @@ function addCandle(x, y, z, intensity, height) {
|
||||
);
|
||||
wick.position.y = height + 0.015;
|
||||
wick.rotation.x = 0.16;
|
||||
wick.castShadow = false;
|
||||
wick.receiveShadow = false;
|
||||
candle.add(wick);
|
||||
|
||||
const flame = createFlame();
|
||||
flame.position.y = wickTopY + 0.055;
|
||||
flame.userData.excludeFromAo = true;
|
||||
flame.traverse((child) => {
|
||||
child.userData.excludeFromAo = true;
|
||||
aoExcludedObjects.push(child);
|
||||
});
|
||||
candle.add(flame);
|
||||
|
||||
const baseLightIntensity = intensity * 7.4;
|
||||
@@ -509,7 +734,7 @@ function createWaxMaterial(height) {
|
||||
}
|
||||
|
||||
function configureTableShader(material) {
|
||||
material.customProgramCacheKey = () => 'book-lab-table-planar-environment-reflection-v2';
|
||||
material.customProgramCacheKey = () => 'book-lab-table-planar-environment-reflection-v7';
|
||||
material.onBeforeCompile = (shader) => {
|
||||
tableShader = shader;
|
||||
shader.uniforms.roomReflectionMap = { value: tableRoomReflectionTexture };
|
||||
@@ -526,6 +751,9 @@ function configureTableShader(material) {
|
||||
shader.uniforms.candleBodyData = {
|
||||
value: [new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2()]
|
||||
};
|
||||
shader.uniforms.bookShadowMaps = { value: bookShadowTargets.map((target) => target.texture) };
|
||||
shader.uniforms.bookShadowMatrices = { value: bookShadowMatrices };
|
||||
shader.uniforms.bookShadowMapTexelSize = { value: new THREE.Vector2(1 / bookShadowMapSize, 1 / bookShadowMapSize) };
|
||||
shader.uniforms.tableDebugMode = { value: tableDebugMode };
|
||||
|
||||
shader.vertexShader = shader.vertexShader
|
||||
@@ -550,6 +778,9 @@ function configureTableShader(material) {
|
||||
uniform vec3 candleBodyPositions[3];
|
||||
uniform vec3 candleFlamePositions[3];
|
||||
uniform vec2 candleBodyData[3];
|
||||
uniform sampler2D bookShadowMaps[3];
|
||||
uniform mat4 bookShadowMatrices[3];
|
||||
uniform vec2 bookShadowMapTexelSize;
|
||||
uniform int tableDebugMode;
|
||||
varying vec3 vTableWorldPosition;
|
||||
varying vec4 vSceneReflectionCoord;
|
||||
@@ -634,20 +865,6 @@ function configureTableShader(material) {
|
||||
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.32;
|
||||
}
|
||||
|
||||
float candleContactField(vec3 point) {
|
||||
float contact = 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++) {
|
||||
@@ -657,6 +874,109 @@ function configureTableShader(material) {
|
||||
}
|
||||
}
|
||||
return clamp(projectedShadow, 0.0, 0.46);
|
||||
}
|
||||
|
||||
float candlePlanarShadowLobe(vec3 point, vec3 flame, vec3 body, vec2 bodyData, float selfLight) {
|
||||
vec2 lightToBody = body.xz - flame.xz;
|
||||
float lightDistance = length(lightToBody);
|
||||
vec2 direction = lightDistance > 0.012 ? lightToBody / lightDistance : normalize(vec2(0.42, 0.91));
|
||||
vec2 perpendicular = vec2(-direction.y, direction.x);
|
||||
vec2 delta = point.xz - body.xz;
|
||||
float along = dot(delta, direction);
|
||||
float side = abs(dot(delta, perpendicular));
|
||||
float radius = bodyData.x;
|
||||
float softContact = 1.0 - smoothstep(radius * 0.72, radius * 3.4, length(delta));
|
||||
float lobeLength = mix(radius * 3.4, radius * (4.8 + lightDistance * 0.92), 1.0 - selfLight);
|
||||
float lobeWidth = radius * (1.6 + max(along, 0.0) * mix(0.72, 0.46, selfLight));
|
||||
float frontGate = smoothstep(-radius * 0.42, radius * 0.34, along);
|
||||
float distanceFade = 1.0 - smoothstep(radius * 0.7, lobeLength, along);
|
||||
float sideFade = 1.0 - smoothstep(lobeWidth * 0.54, lobeWidth, side);
|
||||
float directional = frontGate * distanceFade * sideFade;
|
||||
float heightFade = smoothstep(body.y - 0.02, body.y + bodyData.y * 0.72, flame.y);
|
||||
float waxTransmission = mix(0.54, 0.78, selfLight);
|
||||
float strength = mix(0.32, 0.2, selfLight) * heightFade * (1.0 - waxTransmission * 0.48);
|
||||
return clamp(max(softContact * 0.2, directional * strength), 0.0, 0.38);
|
||||
}
|
||||
|
||||
float candlePlanarShadowField(vec3 point) {
|
||||
float shadow = 0.0;
|
||||
for (int bodyIndex = 0; bodyIndex < 3; bodyIndex++) {
|
||||
for (int flameIndex = 0; flameIndex < 3; flameIndex++) {
|
||||
float selfLight = bodyIndex == flameIndex ? 1.0 : 0.0;
|
||||
shadow = max(shadow, candlePlanarShadowLobe(point, candleFlamePositions[flameIndex], candleBodyPositions[bodyIndex], candleBodyData[bodyIndex], selfLight));
|
||||
}
|
||||
}
|
||||
return clamp(shadow, 0.0, 0.5);
|
||||
}
|
||||
|
||||
float bookUnpackRGBADepth(vec4 packedDepth) {
|
||||
const vec4 unpackFactors = vec4(
|
||||
1.0 / (256.0 * 256.0 * 256.0),
|
||||
1.0 / (256.0 * 256.0),
|
||||
1.0 / 256.0,
|
||||
1.0
|
||||
);
|
||||
return dot(packedDepth, unpackFactors);
|
||||
}
|
||||
|
||||
float bookShadowCompare(vec4 packedDepth, float currentDepth) {
|
||||
float closestDepth = bookUnpackRGBADepth(packedDepth);
|
||||
return smoothstep(0.001, 0.018, currentDepth - closestDepth - 0.0018);
|
||||
}
|
||||
|
||||
float bookShadowSample0(vec4 shadowCoord) {
|
||||
vec3 coord = shadowCoord.xyz / max(shadowCoord.w, 0.0001);
|
||||
float inBounds = step(0.0, coord.x) * step(0.0, coord.y) *
|
||||
step(coord.x, 1.0) * step(coord.y, 1.0) * step(0.0, coord.z) * step(coord.z, 1.0);
|
||||
if (inBounds < 0.5) return 0.0;
|
||||
|
||||
float shadow = 0.0;
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int y = -1; y <= 1; y++) {
|
||||
vec2 offset = vec2(float(x), float(y)) * bookShadowMapTexelSize * 1.65;
|
||||
shadow += bookShadowCompare(texture2D(bookShadowMaps[0], coord.xy + offset), coord.z);
|
||||
}
|
||||
}
|
||||
return clamp(shadow / 9.0, 0.0, 1.0) * inBounds;
|
||||
}
|
||||
|
||||
float bookShadowSample1(vec4 shadowCoord) {
|
||||
vec3 coord = shadowCoord.xyz / max(shadowCoord.w, 0.0001);
|
||||
float inBounds = step(0.0, coord.x) * step(0.0, coord.y) *
|
||||
step(coord.x, 1.0) * step(coord.y, 1.0) * step(0.0, coord.z) * step(coord.z, 1.0);
|
||||
if (inBounds < 0.5) return 0.0;
|
||||
|
||||
float shadow = 0.0;
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int y = -1; y <= 1; y++) {
|
||||
vec2 offset = vec2(float(x), float(y)) * bookShadowMapTexelSize * 1.65;
|
||||
shadow += bookShadowCompare(texture2D(bookShadowMaps[1], coord.xy + offset), coord.z);
|
||||
}
|
||||
}
|
||||
return clamp(shadow / 9.0, 0.0, 1.0) * inBounds;
|
||||
}
|
||||
|
||||
float bookShadowSample2(vec4 shadowCoord) {
|
||||
vec3 coord = shadowCoord.xyz / max(shadowCoord.w, 0.0001);
|
||||
float inBounds = step(0.0, coord.x) * step(0.0, coord.y) *
|
||||
step(coord.x, 1.0) * step(coord.y, 1.0) * step(0.0, coord.z) * step(coord.z, 1.0);
|
||||
if (inBounds < 0.5) return 0.0;
|
||||
|
||||
float shadow = 0.0;
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int y = -1; y <= 1; y++) {
|
||||
vec2 offset = vec2(float(x), float(y)) * bookShadowMapTexelSize * 1.65;
|
||||
shadow += bookShadowCompare(texture2D(bookShadowMaps[2], coord.xy + offset), coord.z);
|
||||
}
|
||||
}
|
||||
return clamp(shadow / 9.0, 0.0, 1.0) * inBounds;
|
||||
}
|
||||
|
||||
float bookMeshShadowField(vec3 point) {
|
||||
float shadow0 = bookShadowSample0(bookShadowMatrices[0] * vec4(point, 1.0));
|
||||
float shadow1 = bookShadowSample1(bookShadowMatrices[1] * vec4(point, 1.0));
|
||||
float shadow2 = bookShadowSample2(bookShadowMatrices[2] * vec4(point, 1.0));
|
||||
return clamp(max(max(shadow0, shadow1), shadow2) * 0.62, 0.0, 0.62);
|
||||
}`
|
||||
)
|
||||
.replace(
|
||||
@@ -702,24 +1022,23 @@ function configureTableShader(material) {
|
||||
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 candleContact = candleContactField(vTableWorldPosition) * tableReflectionMask;
|
||||
float candleProjectedShadow = candleProjectedShadowField(vTableWorldPosition) * tableReflectionMask;
|
||||
float candleOcclusion = clamp(candleContact + candleProjectedShadow * 0.42, 0.0, 0.48);
|
||||
float candleProjectedShadow = max(
|
||||
max(candleProjectedShadowField(vTableWorldPosition), candlePlanarShadowField(vTableWorldPosition)),
|
||||
bookMeshShadowField(vTableWorldPosition)
|
||||
) * tableReflectionMask;
|
||||
float candleOcclusion = clamp(candleProjectedShadow * 1.46, 0.0, 0.82);
|
||||
vec3 normalDebug = normalize(normal) * 0.5 + 0.5;
|
||||
outgoingLight *= mix(1.0, contactAo, tableReflectionMask * 0.55);
|
||||
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);
|
||||
outgoingLight *= mix(vec3(1.0), vec3(0.19, 0.15, 0.115), 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);
|
||||
if (tableDebugMode == 8) outgoingLight = vec3(grease);
|
||||
#include <opaque_fragment>`
|
||||
);
|
||||
@@ -981,152 +1300,6 @@ function tintAmbientFromCanvas(canvas) {
|
||||
candleBounceLight.intensity = 0.28;
|
||||
}
|
||||
|
||||
function createTableNormalTexture() {
|
||||
const canvas = document.createElement('canvas');
|
||||
generatedTextureCanvases.tableNormal = canvas;
|
||||
canvas.width = 2048;
|
||||
canvas.height = 2048;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const image = ctx.createImageData(canvas.width, canvas.height);
|
||||
for (let y = 0; y < canvas.height; y += 1) {
|
||||
for (let x = 0; x < canvas.width; x += 1) {
|
||||
const i = (y * canvas.width + x) * 4;
|
||||
const grain = Math.sin(x * 0.028) * 8 + Math.sin((x + y) * 0.011) * 5 + Math.sin(x * 0.11 + y * 0.007) * 2;
|
||||
const pore = Math.sin(y * 0.12 + Math.sin(x * 0.016) * 2.1) * 3 + (Math.random() - 0.5) * 6;
|
||||
image.data[i] = 128 + grain;
|
||||
image.data[i + 1] = 128 + pore;
|
||||
image.data[i + 2] = 255;
|
||||
image.data[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
ctx.putImageData(image, 0, 0);
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.colorSpace = THREE.NoColorSpace;
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
|
||||
function createTableDustTexture() {
|
||||
const canvas = document.createElement('canvas');
|
||||
generatedTextureCanvases.tableDust = canvas;
|
||||
canvas.width = 4096;
|
||||
canvas.height = 4096;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'rgb(1, 1, 1)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const image = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
for (let y = 0; y < canvas.height; y += 1) {
|
||||
for (let x = 0; x < canvas.width; x += 1) {
|
||||
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) * 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;
|
||||
image.data[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
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})`);
|
||||
gradient.addColorStop(0.42, `rgba(150, 150, 150, ${alpha * 0.32})`);
|
||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
ctx.scale(rx / Math.max(rx, ry), ry / Math.max(rx, ry));
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, Math.max(rx, ry), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
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;
|
||||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.generateMipmaps = true;
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
|
||||
function drawCentered(ctx, text, y, size) {
|
||||
ctx.font = `${size}px Georgia, "Times New Roman", serif`;
|
||||
ctx.textAlign = 'center';
|
||||
@@ -1155,6 +1328,8 @@ function resize() {
|
||||
const width = Math.max(1, window.innerWidth);
|
||||
const height = Math.max(1, window.innerHeight);
|
||||
renderer.setSize(width, height, false);
|
||||
if (composer) composer.setSize(width, height);
|
||||
if (sceneAoPass) sceneAoPass.setSize(width, height);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
const desiredReflectionScale = reflectionPixelRatio * 1.5;
|
||||
@@ -1278,6 +1453,61 @@ function updateCandleShadowUniforms() {
|
||||
});
|
||||
}
|
||||
|
||||
function updateBookShadowMaps() {
|
||||
if (!tableShader || candleShadowSources.length < 3) return;
|
||||
|
||||
const previousRenderTarget = renderer.getRenderTarget();
|
||||
const previousXrEnabled = renderer.xr.enabled;
|
||||
const previousShadowAutoUpdate = renderer.shadowMap.autoUpdate;
|
||||
const previousToneMappingExposure = renderer.toneMappingExposure;
|
||||
const previousOverrideMaterial = scene.overrideMaterial;
|
||||
const previousClearColor = new THREE.Color();
|
||||
renderer.getClearColor(previousClearColor);
|
||||
const previousClearAlpha = renderer.getClearAlpha();
|
||||
|
||||
const hiddenObjects = [tableMesh, ...candleShadowSources].filter(Boolean);
|
||||
hiddenObjects.forEach((object) => {
|
||||
object.userData.wasVisibleForBookShadow = object.visible;
|
||||
object.visible = false;
|
||||
});
|
||||
|
||||
renderer.xr.enabled = false;
|
||||
renderer.shadowMap.autoUpdate = false;
|
||||
renderer.toneMappingExposure = 1;
|
||||
renderer.setClearColor(0xffffff, 1);
|
||||
scene.overrideMaterial = bookShadowDepthMaterial;
|
||||
|
||||
candleShadowSources.forEach((candle, index) => {
|
||||
candle.userData.flame.getWorldPosition(flameWorldPosition);
|
||||
const shadowCamera = bookShadowCameras[index];
|
||||
shadowCamera.position.copy(flameWorldPosition);
|
||||
shadowCamera.lookAt(0, 0.09, 0);
|
||||
shadowCamera.updateProjectionMatrix();
|
||||
shadowCamera.updateMatrixWorld();
|
||||
shadowCamera.matrixWorldInverse.copy(shadowCamera.matrixWorld).invert();
|
||||
|
||||
bookShadowMatrices[index]
|
||||
.copy(bookShadowBiasMatrix)
|
||||
.multiply(shadowCamera.projectionMatrix)
|
||||
.multiply(shadowCamera.matrixWorldInverse);
|
||||
|
||||
renderer.setRenderTarget(bookShadowTargets[index]);
|
||||
renderer.clear();
|
||||
renderer.render(scene, shadowCamera);
|
||||
});
|
||||
|
||||
scene.overrideMaterial = previousOverrideMaterial;
|
||||
hiddenObjects.forEach((object) => {
|
||||
object.visible = object.userData.wasVisibleForBookShadow;
|
||||
delete object.userData.wasVisibleForBookShadow;
|
||||
});
|
||||
renderer.setClearColor(previousClearColor, previousClearAlpha);
|
||||
renderer.toneMappingExposure = previousToneMappingExposure;
|
||||
renderer.shadowMap.autoUpdate = previousShadowAutoUpdate;
|
||||
renderer.xr.enabled = previousXrEnabled;
|
||||
renderer.setRenderTarget(previousRenderTarget);
|
||||
}
|
||||
|
||||
function updateTableReflection() {
|
||||
if (!tableMesh || !tableShader) return;
|
||||
|
||||
@@ -1354,6 +1584,13 @@ function animate() {
|
||||
}
|
||||
});
|
||||
updateCandleShadowUniforms();
|
||||
updateBookShadowMaps();
|
||||
updateTableReflection();
|
||||
renderer.render(scene, camera);
|
||||
if (composer) {
|
||||
composer.render();
|
||||
} else {
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
window.BookLabDebug.renderedFrames += 1;
|
||||
window.BookLabDebug.ready = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user