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);
};
</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>
</html>
+1 -1
View File
@@ -3,7 +3,7 @@
* Defines the canonical page geometry used by the WebGL book renderer.
*/
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;
+7 -5
View File
@@ -62,6 +62,8 @@ class BookTextureRendererModule extends BaseModule {
this.pageFormat = this.getModule('book-page-format');
this.pagination = this.getModule('book-pagination');
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.createPageCanvases();
this.drawEmptySpread();
@@ -120,18 +122,18 @@ class BookTextureRendererModule extends BaseModule {
if (!canvas || !ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#fff7dc';
ctx.fillStyle = '#f2ead0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const shade = ctx.createLinearGradient(0, 0, canvas.width, 0);
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(1, 'rgba(82, 42, 14, 0.16)');
shade.addColorStop(1, 'rgba(70, 48, 28, 0.08)');
} 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(1, 'rgba(255, 255, 255, 0.10)');
shade.addColorStop(1, 'rgba(255, 255, 255, 0.06)');
}
ctx.fillStyle = shade;
ctx.fillRect(0, 0, canvas.width, canvas.height);
+1 -1
View File
@@ -24,7 +24,7 @@ const ModuleState = {
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;
/**
+45 -40
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 { 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 { 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');
canvas.style.cursor = 'grab';
@@ -182,7 +182,7 @@ const fastFlipOverlap = 5;
let activeFlips = [];
let pendingPageFlips = 0;
const paperColor = new THREE.Color(0xf1ead2);
const paperColor = new THREE.Color(0xece4ca);
const inkColor = '#1a1009';
await reportLabStep(48, 'Preparing high-resolution page textures');
@@ -253,69 +253,69 @@ const materials = {
side: THREE.DoubleSide
}),
pageBlock: new THREE.MeshStandardMaterial({
color: 0xf4eed8,
color: 0xeee6cc,
map: paperTextures.color,
normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.014, 0.014),
normalScale: new THREE.Vector2(0.008, 0.008),
roughnessMap: paperTextures.roughness,
roughness: 0.88,
metalness: 0,
envMapIntensity: 0.06
envMapIntensity: 0.025
}),
pageEdge: new THREE.MeshStandardMaterial({
color: 0xf0e5c7,
color: 0xe8ddbe,
map: paperTextures.edge,
normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.012, 0.012),
normalScale: new THREE.Vector2(0.008, 0.008),
roughnessMap: paperTextures.roughness,
roughness: 0.94,
metalness: 0,
envMapIntensity: 0.05
envMapIntensity: 0.02
}),
pageSurface: new THREE.MeshStandardMaterial({
color: 0xf5efd9,
color: 0xeee6cc,
map: paperTextures.color,
normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.012, 0.012),
normalScale: new THREE.Vector2(0.006, 0.006),
roughnessMap: paperTextures.roughness,
roughness: 0.9,
metalness: 0,
emissive: 0x14110b,
emissiveIntensity: 0.012,
envMapIntensity: 0.035,
emissiveIntensity: 0.004,
envMapIntensity: 0.012,
side: THREE.DoubleSide
}),
flipPageSurface: new THREE.MeshStandardMaterial({
color: 0xf5efd9,
color: 0xeee6cc,
roughness: 0.92,
metalness: 0,
emissive: 0x100d08,
emissiveIntensity: 0.01,
envMapIntensity: 0.02,
emissiveIntensity: 0.004,
envMapIntensity: 0.01,
side: THREE.DoubleSide
}),
leftPage: new THREE.MeshStandardMaterial({
color: 0xffffff,
map: leftTexture,
normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.01, 0.01),
normalScale: new THREE.Vector2(0.004, 0.004),
roughnessMap: paperTextures.roughness,
roughness: 0.86,
metalness: 0,
emissive: 0x11100c,
emissiveIntensity: 0.012,
emissiveIntensity: 0.004,
side: THREE.DoubleSide
}),
rightPage: new THREE.MeshStandardMaterial({
color: 0xffffff,
map: rightTexture,
normalMap: paperTextures.normal,
normalScale: new THREE.Vector2(0.01, 0.01),
normalScale: new THREE.Vector2(0.004, 0.004),
roughnessMap: paperTextures.roughness,
roughness: 0.86,
metalness: 0,
emissive: 0x11100c,
emissiveIntensity: 0.012,
emissiveIntensity: 0.004,
side: THREE.DoubleSide
}),
spineCloth: new THREE.MeshStandardMaterial({
@@ -352,12 +352,12 @@ configureBookShadowReceiver(materials.leather, 0.52);
configureBookShadowReceiver(materials.hingeLeather, 0.36);
configureBookShadowReceiver(materials.spineBaseLeather, 0.34);
configureBookShadowReceiver(materials.coverEdge, 0.28);
configureBookShadowReceiver(materials.pageBlock, 0.3);
configureBookShadowReceiver(materials.pageEdge, 0.24);
configureBookShadowReceiver(materials.pageSurface, 0.2);
configureBookShadowReceiver(materials.flipPageSurface, 0.2);
configureBookShadowReceiver(materials.leftPage, 0.18);
configureBookShadowReceiver(materials.rightPage, 0.18);
configureBookShadowReceiver(materials.pageBlock, 0.18);
configureBookShadowReceiver(materials.pageEdge, 0.16);
configureBookShadowReceiver(materials.pageSurface, 0.11);
configureBookShadowReceiver(materials.flipPageSurface, 0.11);
configureBookShadowReceiver(materials.leftPage, 0.08);
configureBookShadowReceiver(materials.rightPage, 0.08);
configureBookShadowReceiver(materials.spineCloth, 0.48);
configureBookShadowReceiver(materials.headband, 0.62);
@@ -369,7 +369,7 @@ await reportLabStep(78, 'Building physical book stack');
buildBook();
notifyBookPageCountChanged();
await reportLabStep(82, 'Loading room reflection texture');
loadAiRoomReflection();
await loadAiRoomReflection();
await reportLabStep(86, 'Preparing static shadow and mirror maps');
primeSceneForLoader();
await reportLabStep(90, 'Compiled WebGL scene passes');
@@ -656,11 +656,11 @@ function configureBookShadowReceiver(material, strength) {
float sideFill = grazingSide * sideReach;
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));
vec3 tableWarmth = vec3(0.042, 0.034, 0.028) * tableFill;
vec3 roomWarmth = vec3(0.032, 0.032, 0.03) * sideFill;
vec3 pageWarmth = vec3(0.032, 0.032, 0.029) * pageFill * grazingSide * (1.0 - upFacing * 0.42);
vec3 tableWarmth = vec3(0.026, 0.024, 0.021) * tableFill;
vec3 roomWarmth = vec3(0.024, 0.024, 0.023) * sideFill;
vec3 pageWarmth = vec3(0.022, 0.022, 0.02) * pageFill * grazingSide * (1.0 - upFacing * 0.42);
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) {
@@ -690,8 +690,8 @@ function configureBookShadowReceiver(material, strength) {
sin((uv.y * 211.0 - uv.x * 53.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);
float fiber = clamp(fleck * 0.008 + cloud * 0.012, -0.02, 0.026);
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));
float fiber = clamp(fleck * 0.005 + cloud * 0.007, -0.012, 0.014);
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;
}
@@ -708,7 +708,7 @@ function configureBookShadowReceiver(material, strength) {
${isHardcoverPaper ? 'outgoingLight = hardcoverPaperLight(vBookSurfaceUv, outgoingLight);' : ''}
${isHeadband ? 'outgoingLight = headbandCreviceLight(vBookSurfaceUv, outgoingLight);' : ''}
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);
#include <opaque_fragment>`
);
@@ -1479,11 +1479,11 @@ function configureHardcoverPaperMaterial(material, { useEdgeMap = false } = {})
material.userData.isHardcoverPaper = true;
if (!material.map) material.map = useEdgeMap ? paperTextures.edge : paperTextures.color;
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.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.envMapIntensity = Math.min(material.envMapIntensity ?? 0.025, 0.035);
material.envMapIntensity = Math.min(material.envMapIntensity ?? 0.012, 0.02);
material.needsUpdate = true;
}
@@ -1568,13 +1568,13 @@ function handlePageCanvases(event) {
function drawCanvasPageTexture(canvas, sourceCanvas, side) {
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fffaf0';
ctx.fillStyle = '#f2ead0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
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(1, 'rgba(85, 49, 21, 0.08)');
shade.addColorStop(1, 'rgba(70, 48, 28, 0.04)');
ctx.fillStyle = shade;
ctx.fillRect(0, 0, canvas.width, canvas.height);
@@ -2599,6 +2599,7 @@ function createRoomReflectionTexture() {
}
function loadAiRoomReflection() {
return new Promise((resolve) => {
new THREE.TextureLoader().load('/assets/webgl/room_reflection_candlelit_study_equirect_4k.png', (texture) => {
texture.colorSpace = THREE.SRGBColorSpace;
texture.mapping = THREE.EquirectangularReflectionMapping;
@@ -2614,7 +2615,7 @@ function loadAiRoomReflection() {
markStaticSceneBuffersDirty();
const image = texture.image;
if (!image) return;
if (image) {
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth || image.width;
canvas.height = image.naturalHeight || image.height;
@@ -2623,9 +2624,13 @@ function loadAiRoomReflection() {
generatedTextureCanvases.aiRoomReflection = canvas;
tintAmbientFromCanvas(canvas);
markStaticSceneBuffersDirty();
}
resolve(texture);
}, undefined, () => {
tintAmbientFromCanvas(generatedTextureCanvases.roomReflection);
markStaticSceneBuffersDirty();
resolve(tableRoomReflectionTexture);
});
});
}