From 925caa57bbb4cc8f845b04be87c0ec17bb4e1b10 Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Sat, 6 Jun 2026 08:53:29 +0200 Subject: [PATCH] Refine WebGL paper and spine materials --- public/js/procedural-book-model.js | 113 +++++++++++++++++++++++------ public/js/webgl-book-lab.js | 35 ++++----- 2 files changed, 109 insertions(+), 39 deletions(-) diff --git a/public/js/procedural-book-model.js b/public/js/procedural-book-model.js index a643687..cb1cc42 100644 --- a/public/js/procedural-book-model.js +++ b/public/js/procedural-book-model.js @@ -370,18 +370,23 @@ function addSimulatedStackBodies(group, context, model) { } function createStackBodyMaterials(context, model, side, isSinglePage = false) { - const baseColor = side < 0 ? '#d8c7a4' : '#e7d6b4'; - const lineColor = '#9a8058'; - const layerTexture = createStackLayerTexture(context, model.bundleCount, baseColor, lineColor); + const baseColor = side < 0 ? '#fff1c8' : '#fff7d7'; + const lineColor = '#c39a4b'; + const layerTextures = createStackLayerTextures(context, model.bundleCount, baseColor, lineColor); const surface = new THREE.MeshStandardMaterial({ - map: layerTexture, - roughness: 0.84, + map: layerTextures.color, + normalMap: layerTextures.normal, + normalScale: new THREE.Vector2(0.034, 0.034), + roughnessMap: layerTextures.roughness, + roughness: 0.88, metalness: 0, - envMapIntensity: 0.08, + envMapIntensity: 0.06, side: THREE.DoubleSide }); const edge = surface.clone(); - edge.map = layerTexture; + edge.map = layerTextures.color; + edge.normalMap = layerTextures.normal; + edge.roughnessMap = layerTextures.roughness; const bottom = context.materials.pageTop.clone(); const top = side < 0 && context.materials.leftPage ? context.materials.leftPage @@ -401,33 +406,97 @@ function createStackBodyMaterials(context, model, side, isSinglePage = false) { return [surface, edge, bottom, top, context.materials.spine]; } -function createStackLayerTexture(context, bundleCount, baseColor, lineColor) { +function createStackLayerTextures(context, bundleCount, baseColor, lineColor) { const canvas = document.createElement('canvas'); + const normalCanvas = document.createElement('canvas'); + const roughnessCanvas = document.createElement('canvas'); canvas.width = 2048; canvas.height = 1024; + normalCanvas.width = canvas.width; + normalCanvas.height = canvas.height; + roughnessCanvas.width = canvas.width; + roughnessCanvas.height = canvas.height; const context2d = canvas.getContext('2d'); + const normalContext = normalCanvas.getContext('2d'); + const roughnessContext = roughnessCanvas.getContext('2d'); context2d.fillStyle = baseColor; context2d.fillRect(0, 0, canvas.width, canvas.height); - context2d.strokeStyle = lineColor; - context2d.globalAlpha = 0.95; - context2d.lineWidth = 4.2; - context2d.lineCap = 'square'; - for (let row = 0; row < bundleCount; row += 1) { - const v = bundleCount <= 1 ? 0.5 : row / (bundleCount - 1); + normalContext.fillStyle = 'rgb(128, 128, 255)'; + normalContext.fillRect(0, 0, normalCanvas.width, normalCanvas.height); + roughnessContext.fillStyle = 'rgb(224, 224, 224)'; + roughnessContext.fillRect(0, 0, roughnessCanvas.width, roughnessCanvas.height); + + const irregular = (seed) => { + const value = Math.sin(seed * 12.9898 + 78.233) * 43758.5453; + return value - Math.floor(value); + }; + const drawLayerLine = (v, alpha, width, normalStrength) => { const y = (1 - v) * canvas.height; context2d.beginPath(); context2d.moveTo(-8, y); context2d.lineTo(canvas.width + 8, y); + context2d.strokeStyle = lineColor; + context2d.globalAlpha = alpha; + context2d.lineWidth = width; + context2d.lineCap = 'square'; context2d.stroke(); + + normalContext.beginPath(); + normalContext.moveTo(-8, y); + normalContext.lineTo(normalCanvas.width + 8, y); + const normalByte = Math.round(128 + normalStrength * 68); + normalContext.strokeStyle = `rgb(128, ${normalByte}, 255)`; + normalContext.globalAlpha = Math.min(1, alpha * 0.85); + normalContext.lineWidth = Math.max(1, width * 0.72); + normalContext.stroke(); + + roughnessContext.beginPath(); + roughnessContext.moveTo(-8, y); + roughnessContext.lineTo(roughnessCanvas.width + 8, y); + const roughnessByte = Math.round(218 + alpha * 28); + roughnessContext.strokeStyle = `rgb(${roughnessByte}, ${roughnessByte}, ${roughnessByte})`; + roughnessContext.globalAlpha = Math.min(1, alpha * 0.72); + roughnessContext.lineWidth = Math.max(1, width * 0.8); + roughnessContext.stroke(); + }; + + for (let row = 0; row < bundleCount; row += 1) { + const rowV = bundleCount <= 1 ? 0.5 : row / (bundleCount - 1); + const rowAccent = 0.5 + irregular(row + 0.37) * 0.28; + drawLayerLine(rowV, rowAccent, 2.6 + irregular(row + 2.13) * 0.8, 0.58); + if (row >= bundleCount - 1) continue; + const nextV = (row + 1) / Math.max(1, bundleCount - 1); + const interval = nextV - rowV; + const microLines = 12; + for (let sub = 1; sub < microLines; sub += 1) { + const seed = row * 17.0 + sub * 3.0; + const t = THREE.MathUtils.clamp((sub + (irregular(seed) - 0.5) * 0.22) / microLines, 0.04, 0.96); + const v = rowV + interval * t; + const alpha = 0.3 + irregular(seed + 1.91) * 0.24; + const width = 1.05 + irregular(seed + 5.47) * 1.05; + drawLayerLine(v, alpha, width, 0.34 + irregular(seed + 9.17) * 0.26); + } } - const texture = new THREE.CanvasTexture(canvas); - texture.colorSpace = THREE.SRGBColorSpace; - texture.anisotropy = context.maxAnisotropy; - texture.minFilter = THREE.LinearMipmapLinearFilter; - texture.magFilter = THREE.LinearFilter; - texture.generateMipmaps = true; - texture.needsUpdate = true; - return texture; + context2d.globalAlpha = 1; + normalContext.globalAlpha = 1; + roughnessContext.globalAlpha = 1; + + const colorTexture = new THREE.CanvasTexture(canvas); + const normalTexture = new THREE.CanvasTexture(normalCanvas); + const roughnessTexture = new THREE.CanvasTexture(roughnessCanvas); + [colorTexture, normalTexture, roughnessTexture].forEach((texture) => { + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.anisotropy = context.maxAnisotropy; + texture.minFilter = THREE.LinearMipmapLinearFilter; + texture.magFilter = THREE.LinearFilter; + texture.generateMipmaps = true; + texture.needsUpdate = true; + }); + colorTexture.colorSpace = THREE.SRGBColorSpace; + normalTexture.colorSpace = THREE.NoColorSpace; + roughnessTexture.colorSpace = THREE.NoColorSpace; + return { color: colorTexture, normal: normalTexture, roughness: roughnessTexture }; } function createLoftedLineBody(model, lines, depth) { diff --git a/public/js/webgl-book-lab.js b/public/js/webgl-book-lab.js index 9e2cbc0..475a5df 100644 --- a/public/js/webgl-book-lab.js +++ b/public/js/webgl-book-lab.js @@ -275,14 +275,14 @@ const materials = { side: THREE.DoubleSide }), spineCloth: new THREE.MeshStandardMaterial({ - color: 0x6f0808, + color: 0xa51d1d, map: spineClothTextures.color, normalMap: spineClothTextures.normal, - normalScale: new THREE.Vector2(0.075, 0.075), + normalScale: new THREE.Vector2(0.07, 0.07), roughnessMap: spineClothTextures.roughness, - roughness: 0.9, + roughness: 0.86, metalness: 0, - envMapIntensity: 0.045, + envMapIntensity: 0.075, side: THREE.DoubleSide }) }; @@ -536,8 +536,8 @@ function configureBookShadowReceiver(material, strength) { sin((uv.y * 380.0 - uv.x * 33.0) * 6.28318530718); float raisedThread = clamp(warp * 0.58 + weft * 0.44, 0.0, 1.0); float valley = clamp((1.0 - warp) * (1.0 - weft), 0.0, 1.0); - vec3 threadTint = mix(vec3(0.55, 0.19, 0.16), vec3(1.18, 0.78, 0.58), raisedThread); - float fiberShade = 0.9 + fineFiber * 0.035 - valley * 0.18; + vec3 threadTint = mix(vec3(0.72, 0.24, 0.2), vec3(1.34, 0.86, 0.68), raisedThread); + float fiberShade = 0.96 + fineFiber * 0.03 - valley * 0.11; return baseLight * threadTint * fiberShade; } @@ -547,12 +547,12 @@ function configureBookShadowReceiver(material, strength) { } vec3 hardcoverPaperLight(vec2 uv, vec3 baseLight) { - float laid = paperFiber(uv.y + sin(uv.x * 12.0) * 0.002, 88.0, 2.8); - float chain = paperFiber(uv.x + sin(uv.y * 8.0) * 0.0015, 18.0, 1.6); float fleck = sin((uv.x * 241.0 + uv.y * 97.0) * 6.28318530718) * sin((uv.y * 211.0 - uv.x * 53.0) * 6.28318530718); - float fiber = clamp(laid * 0.18 + chain * 0.1 + fleck * 0.025, -0.08, 0.24); - vec3 paperTint = mix(vec3(0.92, 0.9, 0.82), vec3(1.12, 1.08, 0.96), clamp(0.58 + fiber, 0.0, 1.0)); + 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.018 + cloud * 0.022, -0.04, 0.05); + vec3 paperTint = mix(vec3(0.96, 0.945, 0.89), vec3(1.08, 1.055, 0.98), clamp(0.62 + fiber, 0.0, 1.0)); return baseLight * paperTint; }` ) @@ -2049,9 +2049,9 @@ function createSpineClothTextures() { const height = threadAt(x, y); const wornFiber = 0.86 + 0.1 * Math.sin((x * 0.019 + y * 0.037)) + 0.04 * Math.sin((x * 0.083 - y * 0.011)); const threadGlow = THREE.MathUtils.clamp(0.58 + height * 0.46, 0, 1); - colorImage.data[index] = Math.round(95 * threadGlow * wornFiber); - colorImage.data[index + 1] = Math.round(10 * threadGlow * wornFiber); - colorImage.data[index + 2] = Math.round(9 * (0.84 + height * 0.12)); + colorImage.data[index] = Math.round(128 * threadGlow * wornFiber); + colorImage.data[index + 1] = Math.round(22 * threadGlow * wornFiber); + colorImage.data[index + 2] = Math.round(18 * (0.86 + height * 0.12)); colorImage.data[index + 3] = 255; const hLeft = threadAt((x - 1 + size) % size, y); @@ -2116,12 +2116,12 @@ function createHardcoverPaperTextures() { const fiberAt = (x, y) => { const nx = x / size; const ny = y / size; - const laid = Math.sin((ny * 92 + Math.sin(nx * 25.1327412287) * 0.12) * 6.28318530718); - const chain = Math.sin((nx * 18 + Math.sin(ny * 12.5663706144) * 0.06) * 6.28318530718); const pulpA = Math.sin((nx * 173 + ny * 67) * 6.28318530718); const pulpB = Math.sin((nx * 89 - ny * 131) * 6.28318530718); + const cloudA = Math.sin((nx * 19 + ny * 11) * 6.28318530718); + const cloudB = Math.sin((nx * 31 - ny * 27) * 6.28318530718); const fleck = Math.max(0, 0.5 - Math.abs(pulpA * pulpB)); - return laid * 0.08 + chain * 0.045 - fleck * 0.055; + return cloudA * cloudB * 0.026 - fleck * 0.035; }; for (let y = 0; y < size; y += 1) { @@ -2135,7 +2135,8 @@ function createHardcoverPaperTextures() { colorImage.data[index + 2] = Math.round(235 * shade); colorImage.data[index + 3] = 255; - const line = y % 34 === 0 ? 0.72 : y % 34 === 1 ? 0.82 : 1; + const linePhase = (y + Math.sin(x * 0.021) * 4) % 34; + const line = linePhase < 1.2 ? 0.72 : linePhase < 2.1 ? 0.82 : 1; edgeImage.data[index] = Math.round(255 * shade * line); edgeImage.data[index + 1] = Math.round(244 * shade * line); edgeImage.data[index + 2] = Math.round(207 * shade * line);