Soften WebGL paper rendering

This commit is contained in:
2026-06-07 12:22:26 +02:00
parent de81a7c5c5
commit 7725ce9c73
5 changed files with 80 additions and 73 deletions
+1 -1
View File
@@ -280,6 +280,6 @@
console.log(message); console.log(message);
}; };
</script> </script>
<script type="module" src="/js/loader.js?v=20260607-webgl-loader-quality-fix"></script> <script type="module" src="/js/loader.js?v=20260607-webgl-paper-loader-fix"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -3,7 +3,7 @@
* Defines the canonical page geometry used by the WebGL book renderer. * Defines the canonical page geometry used by the WebGL book renderer.
*/ */
import { BaseModule } from './base-module.js'; import { BaseModule } from './base-module.js';
import { calculateProceduralBookThickness, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-loader-quality-fix'; import { calculateProceduralBookThickness, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-paper-loader-fix';
export const BOOK_TEXTURE_WIDTH = 3072; export const BOOK_TEXTURE_WIDTH = 3072;
+7 -5
View File
@@ -62,6 +62,8 @@ class BookTextureRendererModule extends BaseModule {
this.pageFormat = this.getModule('book-page-format'); this.pageFormat = this.getModule('book-page-format');
this.pagination = this.getModule('book-pagination'); this.pagination = this.getModule('book-pagination');
this.localization = this.getModule('localization'); this.localization = this.getModule('localization');
this.reportProgress(10, 'Waiting for book fonts');
if (document.fonts?.ready) await document.fonts.ready;
this.reportProgress(20, 'Preparing page texture canvases'); this.reportProgress(20, 'Preparing page texture canvases');
this.createPageCanvases(); this.createPageCanvases();
this.drawEmptySpread(); this.drawEmptySpread();
@@ -120,18 +122,18 @@ class BookTextureRendererModule extends BaseModule {
if (!canvas || !ctx) return; if (!canvas || !ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#fff7dc'; ctx.fillStyle = '#f2ead0';
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
const shade = ctx.createLinearGradient(0, 0, canvas.width, 0); const shade = ctx.createLinearGradient(0, 0, canvas.width, 0);
if (side === 'left') { if (side === 'left') {
shade.addColorStop(0, 'rgba(255, 255, 255, 0.10)'); shade.addColorStop(0, 'rgba(255, 255, 255, 0.06)');
shade.addColorStop(0.78, 'rgba(255, 255, 255, 0)'); shade.addColorStop(0.78, 'rgba(255, 255, 255, 0)');
shade.addColorStop(1, 'rgba(82, 42, 14, 0.16)'); shade.addColorStop(1, 'rgba(70, 48, 28, 0.08)');
} else { } else {
shade.addColorStop(0, 'rgba(82, 42, 14, 0.16)'); shade.addColorStop(0, 'rgba(70, 48, 28, 0.08)');
shade.addColorStop(0.22, 'rgba(255, 255, 255, 0)'); shade.addColorStop(0.22, 'rgba(255, 255, 255, 0)');
shade.addColorStop(1, 'rgba(255, 255, 255, 0.10)'); shade.addColorStop(1, 'rgba(255, 255, 255, 0.06)');
} }
ctx.fillStyle = shade; ctx.fillStyle = shade;
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
+1 -1
View File
@@ -24,7 +24,7 @@ const ModuleState = {
ERROR: 'ERROR' ERROR: 'ERROR'
}; };
const MODULE_CACHE_BUSTER = '20260607-webgl-loader-quality-fix'; const MODULE_CACHE_BUSTER = '20260607-webgl-paper-loader-fix';
window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER; window.MODULE_CACHE_BUSTER = MODULE_CACHE_BUSTER;
/** /**
+70 -65
View File
@@ -4,7 +4,7 @@ import { RenderPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postproces
import { SSAOPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/SSAOPass.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 { 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'; import { OutputPass } from 'https://esm.sh/three@0.165.0/examples/jsm/postprocessing/OutputPass.js';
import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-loader-quality-fix'; import { PROCEDURAL_BOOK, createProceduralBookModel, snapProceduralPageCount } from './procedural-book-model.js?v=20260607-webgl-paper-loader-fix';
const canvas = document.getElementById('scene'); const canvas = document.getElementById('scene');
canvas.style.cursor = 'grab'; canvas.style.cursor = 'grab';
@@ -182,7 +182,7 @@ const fastFlipOverlap = 5;
let activeFlips = []; let activeFlips = [];
let pendingPageFlips = 0; let pendingPageFlips = 0;
const paperColor = new THREE.Color(0xf1ead2); const paperColor = new THREE.Color(0xece4ca);
const inkColor = '#1a1009'; const inkColor = '#1a1009';
await reportLabStep(48, 'Preparing high-resolution page textures'); await reportLabStep(48, 'Preparing high-resolution page textures');
@@ -253,69 +253,69 @@ const materials = {
side: THREE.DoubleSide side: THREE.DoubleSide
}), }),
pageBlock: new THREE.MeshStandardMaterial({ pageBlock: new THREE.MeshStandardMaterial({
color: 0xf4eed8, color: 0xeee6cc,
map: paperTextures.color, map: paperTextures.color,
normalMap: paperTextures.normal, normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.014, 0.014), normalScale: new THREE.Vector2(0.008, 0.008),
roughnessMap: paperTextures.roughness, roughnessMap: paperTextures.roughness,
roughness: 0.88, roughness: 0.88,
metalness: 0, metalness: 0,
envMapIntensity: 0.06 envMapIntensity: 0.025
}), }),
pageEdge: new THREE.MeshStandardMaterial({ pageEdge: new THREE.MeshStandardMaterial({
color: 0xf0e5c7, color: 0xe8ddbe,
map: paperTextures.edge, map: paperTextures.edge,
normalMap: paperTextures.normal, normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.012, 0.012), normalScale: new THREE.Vector2(0.008, 0.008),
roughnessMap: paperTextures.roughness, roughnessMap: paperTextures.roughness,
roughness: 0.94, roughness: 0.94,
metalness: 0, metalness: 0,
envMapIntensity: 0.05 envMapIntensity: 0.02
}), }),
pageSurface: new THREE.MeshStandardMaterial({ pageSurface: new THREE.MeshStandardMaterial({
color: 0xf5efd9, color: 0xeee6cc,
map: paperTextures.color, map: paperTextures.color,
normalMap: paperTextures.normal, normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.012, 0.012), normalScale: new THREE.Vector2(0.006, 0.006),
roughnessMap: paperTextures.roughness, roughnessMap: paperTextures.roughness,
roughness: 0.9, roughness: 0.9,
metalness: 0, metalness: 0,
emissive: 0x14110b, emissive: 0x14110b,
emissiveIntensity: 0.012, emissiveIntensity: 0.004,
envMapIntensity: 0.035, envMapIntensity: 0.012,
side: THREE.DoubleSide side: THREE.DoubleSide
}), }),
flipPageSurface: new THREE.MeshStandardMaterial({ flipPageSurface: new THREE.MeshStandardMaterial({
color: 0xf5efd9, color: 0xeee6cc,
roughness: 0.92, roughness: 0.92,
metalness: 0, metalness: 0,
emissive: 0x100d08, emissive: 0x100d08,
emissiveIntensity: 0.01, emissiveIntensity: 0.004,
envMapIntensity: 0.02, envMapIntensity: 0.01,
side: THREE.DoubleSide side: THREE.DoubleSide
}), }),
leftPage: new THREE.MeshStandardMaterial({ leftPage: new THREE.MeshStandardMaterial({
color: 0xffffff, color: 0xffffff,
map: leftTexture, map: leftTexture,
normalMap: paperTextures.normal, normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.01, 0.01), normalScale: new THREE.Vector2(0.004, 0.004),
roughnessMap: paperTextures.roughness, roughnessMap: paperTextures.roughness,
roughness: 0.86, roughness: 0.86,
metalness: 0, metalness: 0,
emissive: 0x11100c, emissive: 0x11100c,
emissiveIntensity: 0.012, emissiveIntensity: 0.004,
side: THREE.DoubleSide side: THREE.DoubleSide
}), }),
rightPage: new THREE.MeshStandardMaterial({ rightPage: new THREE.MeshStandardMaterial({
color: 0xffffff, color: 0xffffff,
map: rightTexture, map: rightTexture,
normalMap: paperTextures.normal, normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.01, 0.01), normalScale: new THREE.Vector2(0.004, 0.004),
roughnessMap: paperTextures.roughness, roughnessMap: paperTextures.roughness,
roughness: 0.86, roughness: 0.86,
metalness: 0, metalness: 0,
emissive: 0x11100c, emissive: 0x11100c,
emissiveIntensity: 0.012, emissiveIntensity: 0.004,
side: THREE.DoubleSide side: THREE.DoubleSide
}), }),
spineCloth: new THREE.MeshStandardMaterial({ spineCloth: new THREE.MeshStandardMaterial({
@@ -352,12 +352,12 @@ configureBookShadowReceiver(materials.leather, 0.52);
configureBookShadowReceiver(materials.hingeLeather, 0.36); configureBookShadowReceiver(materials.hingeLeather, 0.36);
configureBookShadowReceiver(materials.spineBaseLeather, 0.34); configureBookShadowReceiver(materials.spineBaseLeather, 0.34);
configureBookShadowReceiver(materials.coverEdge, 0.28); configureBookShadowReceiver(materials.coverEdge, 0.28);
configureBookShadowReceiver(materials.pageBlock, 0.3); configureBookShadowReceiver(materials.pageBlock, 0.18);
configureBookShadowReceiver(materials.pageEdge, 0.24); configureBookShadowReceiver(materials.pageEdge, 0.16);
configureBookShadowReceiver(materials.pageSurface, 0.2); configureBookShadowReceiver(materials.pageSurface, 0.11);
configureBookShadowReceiver(materials.flipPageSurface, 0.2); configureBookShadowReceiver(materials.flipPageSurface, 0.11);
configureBookShadowReceiver(materials.leftPage, 0.18); configureBookShadowReceiver(materials.leftPage, 0.08);
configureBookShadowReceiver(materials.rightPage, 0.18); configureBookShadowReceiver(materials.rightPage, 0.08);
configureBookShadowReceiver(materials.spineCloth, 0.48); configureBookShadowReceiver(materials.spineCloth, 0.48);
configureBookShadowReceiver(materials.headband, 0.62); configureBookShadowReceiver(materials.headband, 0.62);
@@ -369,7 +369,7 @@ await reportLabStep(78, 'Building physical book stack');
buildBook(); buildBook();
notifyBookPageCountChanged(); notifyBookPageCountChanged();
await reportLabStep(82, 'Loading room reflection texture'); await reportLabStep(82, 'Loading room reflection texture');
loadAiRoomReflection(); await loadAiRoomReflection();
await reportLabStep(86, 'Preparing static shadow and mirror maps'); await reportLabStep(86, 'Preparing static shadow and mirror maps');
primeSceneForLoader(); primeSceneForLoader();
await reportLabStep(90, 'Compiled WebGL scene passes'); await reportLabStep(90, 'Compiled WebGL scene passes');
@@ -656,11 +656,11 @@ function configureBookShadowReceiver(material, strength) {
float sideFill = grazingSide * sideReach; float sideFill = grazingSide * sideReach;
float tableFill = tableReach * (0.16 + underside * 0.22) * (1.0 - upFacing * 0.58); float tableFill = tableReach * (0.16 + underside * 0.22) * (1.0 - upFacing * 0.58);
float pageFill = smoothstep(0.02, 0.2, tableDistance) * (1.0 - smoothstep(0.24, 0.72, tableDistance)); float pageFill = smoothstep(0.02, 0.2, tableDistance) * (1.0 - smoothstep(0.24, 0.72, tableDistance));
vec3 tableWarmth = vec3(0.042, 0.034, 0.028) * tableFill; vec3 tableWarmth = vec3(0.026, 0.024, 0.021) * tableFill;
vec3 roomWarmth = vec3(0.032, 0.032, 0.03) * sideFill; vec3 roomWarmth = vec3(0.024, 0.024, 0.023) * sideFill;
vec3 pageWarmth = vec3(0.032, 0.032, 0.029) * pageFill * grazingSide * (1.0 - upFacing * 0.42); vec3 pageWarmth = vec3(0.022, 0.022, 0.02) * pageFill * grazingSide * (1.0 - upFacing * 0.42);
vec3 indirect = tableWarmth + roomWarmth + pageWarmth; vec3 indirect = tableWarmth + roomWarmth + pageWarmth;
return albedo * indirect * mix(1.0, 0.86, shadow); return albedo * indirect * mix(1.0, 0.92, shadow);
} }
float spineClothThread(float coordinate, float frequency, float sharpness) { float spineClothThread(float coordinate, float frequency, float sharpness) {
@@ -690,8 +690,8 @@ function configureBookShadowReceiver(material, strength) {
sin((uv.y * 211.0 - uv.x * 53.0) * 6.28318530718); sin((uv.y * 211.0 - uv.x * 53.0) * 6.28318530718);
float cloud = sin((uv.x * 17.0 + uv.y * 11.0) * 6.28318530718) * float cloud = sin((uv.x * 17.0 + uv.y * 11.0) * 6.28318530718) *
sin((uv.x * 29.0 - uv.y * 23.0) * 6.28318530718); sin((uv.x * 29.0 - uv.y * 23.0) * 6.28318530718);
float fiber = clamp(fleck * 0.008 + cloud * 0.012, -0.02, 0.026); float fiber = clamp(fleck * 0.005 + cloud * 0.007, -0.012, 0.014);
vec3 paperTint = mix(vec3(0.94, 0.925, 0.875), vec3(1.025, 1.015, 0.97), clamp(0.56 + fiber, 0.0, 1.0)); vec3 paperTint = mix(vec3(0.965, 0.955, 0.915), vec3(1.01, 1.0, 0.96), clamp(0.5 + fiber, 0.0, 1.0));
return baseLight * paperTint; return baseLight * paperTint;
} }
@@ -708,7 +708,7 @@ function configureBookShadowReceiver(material, strength) {
${isHardcoverPaper ? 'outgoingLight = hardcoverPaperLight(vBookSurfaceUv, outgoingLight);' : ''} ${isHardcoverPaper ? 'outgoingLight = hardcoverPaperLight(vBookSurfaceUv, outgoingLight);' : ''}
${isHeadband ? 'outgoingLight = headbandCreviceLight(vBookSurfaceUv, outgoingLight);' : ''} ${isHeadband ? 'outgoingLight = headbandCreviceLight(vBookSurfaceUv, outgoingLight);' : ''}
float bookReceiverShadow = bookReceiverShadowField(vBookReceiverWorldPosition) * bookShadowReceiverStrength; float bookReceiverShadow = bookReceiverShadowField(vBookReceiverWorldPosition) * bookShadowReceiverStrength;
outgoingLight *= mix(vec3(1.0), ${isHeadband ? 'vec3(0.16, 0.095, 0.055)' : isHardcoverPaper ? 'vec3(0.68, 0.62, 0.52)' : 'vec3(0.38, 0.29, 0.2)'}, bookReceiverShadow); outgoingLight *= mix(vec3(1.0), ${isHeadband ? 'vec3(0.16, 0.095, 0.055)' : isHardcoverPaper ? 'vec3(0.82, 0.78, 0.68)' : 'vec3(0.38, 0.29, 0.2)'}, bookReceiverShadow);
outgoingLight += bookLocalBounce(vBookReceiverWorldPosition, normalize(vBookReceiverWorldNormal), bookReceiverShadow, diffuseColor.rgb); outgoingLight += bookLocalBounce(vBookReceiverWorldPosition, normalize(vBookReceiverWorldNormal), bookReceiverShadow, diffuseColor.rgb);
#include <opaque_fragment>` #include <opaque_fragment>`
); );
@@ -1479,11 +1479,11 @@ function configureHardcoverPaperMaterial(material, { useEdgeMap = false } = {})
material.userData.isHardcoverPaper = true; material.userData.isHardcoverPaper = true;
if (!material.map) material.map = useEdgeMap ? paperTextures.edge : paperTextures.color; if (!material.map) material.map = useEdgeMap ? paperTextures.edge : paperTextures.color;
material.normalMap = paperTextures.normal; material.normalMap = paperTextures.normal;
material.normalScale = material.normalScale ?? new THREE.Vector2(useEdgeMap ? 0.012 : 0.01, useEdgeMap ? 0.012 : 0.01); material.normalScale = material.normalScale ?? new THREE.Vector2(useEdgeMap ? 0.008 : 0.006, useEdgeMap ? 0.008 : 0.006);
material.roughnessMap = paperTextures.roughness; material.roughnessMap = paperTextures.roughness;
material.roughness = Math.max(material.roughness ?? 0.9, useEdgeMap ? 0.94 : 0.9); material.roughness = Math.max(material.roughness ?? 0.94, useEdgeMap ? 0.96 : 0.94);
material.metalness = 0; material.metalness = 0;
material.envMapIntensity = Math.min(material.envMapIntensity ?? 0.025, 0.035); material.envMapIntensity = Math.min(material.envMapIntensity ?? 0.012, 0.02);
material.needsUpdate = true; material.needsUpdate = true;
} }
@@ -1568,13 +1568,13 @@ function handlePageCanvases(event) {
function drawCanvasPageTexture(canvas, sourceCanvas, side) { function drawCanvasPageTexture(canvas, sourceCanvas, side) {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fffaf0'; ctx.fillStyle = '#f2ead0';
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
const shade = ctx.createLinearGradient(0, 0, canvas.width, 0); const shade = ctx.createLinearGradient(0, 0, canvas.width, 0);
shade.addColorStop(0, 'rgba(93, 55, 24, 0.10)'); shade.addColorStop(0, 'rgba(70, 48, 28, 0.04)');
shade.addColorStop(side === 'left' ? 0.85 : 0.15, 'rgba(255, 255, 255, 0)'); shade.addColorStop(side === 'left' ? 0.85 : 0.15, 'rgba(255, 255, 255, 0)');
shade.addColorStop(1, 'rgba(85, 49, 21, 0.08)'); shade.addColorStop(1, 'rgba(70, 48, 28, 0.04)');
ctx.fillStyle = shade; ctx.fillStyle = shade;
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
@@ -2599,33 +2599,38 @@ function createRoomReflectionTexture() {
} }
function loadAiRoomReflection() { function loadAiRoomReflection() {
new THREE.TextureLoader().load('/assets/webgl/room_reflection_candlelit_study_equirect_4k.png', (texture) => { return new Promise((resolve) => {
texture.colorSpace = THREE.SRGBColorSpace; new THREE.TextureLoader().load('/assets/webgl/room_reflection_candlelit_study_equirect_4k.png', (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping; texture.colorSpace = THREE.SRGBColorSpace;
texture.anisotropy = renderer.capabilities.getMaxAnisotropy(); texture.mapping = THREE.EquirectangularReflectionMapping;
texture.minFilter = THREE.LinearMipmapLinearFilter; texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
texture.magFilter = THREE.LinearFilter; texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.generateMipmaps = true; texture.magFilter = THREE.LinearFilter;
texture.needsUpdate = true; texture.generateMipmaps = true;
tableRoomReflectionTexture = texture; texture.needsUpdate = true;
if (tableShader) { tableRoomReflectionTexture = texture;
tableShader.uniforms.roomReflectionMap.value = texture; if (tableShader) {
} tableShader.uniforms.roomReflectionMap.value = texture;
markStaticSceneBuffersDirty(); }
markStaticSceneBuffersDirty();
const image = texture.image; const image = texture.image;
if (!image) return; if (image) {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth || image.width; canvas.width = image.naturalWidth || image.width;
canvas.height = image.naturalHeight || image.height; canvas.height = image.naturalHeight || image.height;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, canvas.width, canvas.height); ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
generatedTextureCanvases.aiRoomReflection = canvas; generatedTextureCanvases.aiRoomReflection = canvas;
tintAmbientFromCanvas(canvas); tintAmbientFromCanvas(canvas);
markStaticSceneBuffersDirty(); markStaticSceneBuffersDirty();
}, undefined, () => { }
tintAmbientFromCanvas(generatedTextureCanvases.roomReflection); resolve(texture);
markStaticSceneBuffersDirty(); }, undefined, () => {
tintAmbientFromCanvas(generatedTextureCanvases.roomReflection);
markStaticSceneBuffersDirty();
resolve(tableRoomReflectionTexture);
});
}); });
} }