Stabilize WebGL lighting lab
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const sourcePath = path.join(__dirname, '..', 'public', 'js', 'webgl-book-lab.js');
|
||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
|
||||
const checks = [
|
||||
['scene-level SSAO import', /SSAOPass/.test(source)],
|
||||
['postprocess anti-aliasing import', /SMAAPass/.test(source)],
|
||||
['composer uses explicit render target', /new THREE\.WebGLRenderTarget\(1, 1/.test(source) && /new EffectComposer\(renderer, sceneComposerTarget\)/.test(source)],
|
||||
['composer render path is active', /composer\.render\(\)/.test(source)],
|
||||
['static table maps are loaded from disk', /table_normal_2k\.png/.test(source) && /table_dust_4k\.png/.test(source) && /table_grease_4k\.png/.test(source)],
|
||||
['runtime table map generators removed from page', !/function createTableNormalTexture|function createTableDustTexture|function createTableGreaseTexture/.test(source)],
|
||||
['table primitive shadow receiving disabled', /tableMesh\.receiveShadow = false/.test(source)],
|
||||
['flames excluded from AO', /excludeFromAo = true/.test(source) && /aoExcludedObjects\.push\(child\)/.test(source)],
|
||||
['AO pass hides excluded objects with cleanup', /sceneAoPass\.render = \(\.\.\.args\) =>/.test(source) && /finally/.test(source)],
|
||||
['AO uses scene-scale sampling', /new SSAOPass\(scene, camera, 1, 1, 64\)/.test(source) && /sceneAoPass\.kernelRadius = 0\.48/.test(source) && /sceneAoPass\.minDistance = 0\.00025/.test(source) && /sceneAoPass\.maxDistance = 0\.065/.test(source)],
|
||||
['AO debug shows blurred occlusion map', /tableDebugName === 'ao' && SSAOPass\.OUTPUT\?\.Blur/.test(source) && /sceneAoPass\.output = SSAOPass\.OUTPUT\.Blur/.test(source)],
|
||||
['direct candle shadow lobe present', /candlePlanarShadowLobe/.test(source) && /candlePlanarShadowField/.test(source)],
|
||||
['direct candle shadow contributes to final table shader', /max\(candleProjectedShadowField\(vTableWorldPosition\), candlePlanarShadowField\(vTableWorldPosition\)\)/.test(source) && /bookMeshShadowField\(vTableWorldPosition\)/.test(source)],
|
||||
['book shadows use real light-space depth maps', /bookShadowTargets/.test(source) && /MeshDepthMaterial/.test(source) && /updateBookShadowMaps/.test(source) && /bookMeshShadowField/.test(source) && /bookShadowMaps\[0\]/.test(source)],
|
||||
['book materials receive real shadow maps', /configureBookShadowReceiver\(materials\.leftPage/.test(source) && /bookReceiverShadowField/.test(source) && /bookShadowReceiverStrength/.test(source)],
|
||||
['proxy book shadow shortcuts are forbidden', !/bookPlanarShadowLobe|bookProjectedShadowField|bookBoxShadow|segmentBoxHit/.test(source)],
|
||||
['final candle shadow is visible in composite', /candleOcclusion = clamp\(candleProjectedShadow \* 1\.46, 0\.0, 0\.82\)/.test(source) && /vec3\(0\.19, 0\.15, 0\.115\), candleOcclusion/.test(source)],
|
||||
['primitive candle shadow shortcuts stay disabled', /wax\.castShadow = false/.test(source) && /wick\.castShadow = false/.test(source) && /leftPage\.receiveShadow = false/.test(source) && /rightPage\.receiveShadow = false/.test(source)],
|
||||
['analytic contact fallback removed', !/surfaceContactOcclusion|candleContactField|candleContactOcclusion|bookContactField|candleFootOcclusion|contactAo/.test(source)],
|
||||
['debug AO remains scene-level', /scene debug: SSAO/.test(source)],
|
||||
['contact debug mode removed', !/contact:\s*9|tableDebugMode == 9/.test(source)],
|
||||
['render readiness flag is exposed', /BookLabDebug\.ready/.test(source) && /BookLabDebug\.renderedFrames/.test(source)]
|
||||
];
|
||||
|
||||
const failures = checks.filter(([, passed]) => !passed).map(([name]) => name);
|
||||
|
||||
if (failures.length) {
|
||||
console.error('WebGL book lab regression checks failed:');
|
||||
failures.forEach((name) => console.error(`- ${name}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`WebGL book lab regression checks passed (${checks.length}).`);
|
||||
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ASSET_DIR = ROOT / "public" / "assets" / "webgl"
|
||||
RNG = random.Random(1780504860)
|
||||
|
||||
|
||||
def save_normal(path: Path, size: int = 2048) -> None:
|
||||
image = Image.new("RGB", (size, size))
|
||||
pixels = image.load()
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
grain = (
|
||||
math.sin(x * 0.028) * 8
|
||||
+ math.sin((x + y) * 0.011) * 5
|
||||
+ math.sin(x * 0.11 + y * 0.007) * 2
|
||||
)
|
||||
pore = math.sin(y * 0.12 + math.sin(x * 0.016) * 2.1) * 3 + (RNG.random() - 0.5) * 6
|
||||
pixels[x, y] = (
|
||||
max(0, min(255, round(128 + grain))),
|
||||
max(0, min(255, round(128 + pore))),
|
||||
255,
|
||||
)
|
||||
image.save(path)
|
||||
|
||||
|
||||
def save_dust(path: Path, size: int = 4096) -> None:
|
||||
image = Image.new("L", (size, size), 1)
|
||||
pixels = image.load()
|
||||
for y in range(size):
|
||||
ny = y / size
|
||||
for x in range(size):
|
||||
nx = x / size
|
||||
edge_dust = max(0, 1 - min(nx, ny, 1 - nx, 1 - ny) * 18)
|
||||
micro_noise = (RNG.random() ** 7.2) * 5.5
|
||||
fine_film = max(0, math.sin(nx * 21 + math.sin(ny * 11) * 0.7) - 0.988) * 3
|
||||
book_shelter = math.exp(-((nx - 0.5) * 2.7) ** 2 - ((ny - 0.5) * 1.8) ** 2) * 1.5
|
||||
value = min(255, 1 + edge_dust * 3 + micro_noise + fine_film + book_shelter)
|
||||
pixels[x, y] = round(value)
|
||||
|
||||
draw = ImageDraw.Draw(image, "L")
|
||||
for _ in range(1700):
|
||||
x = RNG.random() * size
|
||||
y = RNG.random() * size
|
||||
radius = 0.08 + RNG.random() * 0.22
|
||||
value = round(230 * 0.028)
|
||||
draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=value)
|
||||
image.save(path)
|
||||
|
||||
|
||||
def soft_ellipse(draw: ImageDraw.ImageDraw, x: float, y: float, rx: float, ry: float, alpha: float) -> None:
|
||||
steps = 36
|
||||
for step in range(steps, 0, -1):
|
||||
scale = step / steps
|
||||
value = round(220 * alpha * (scale ** 2.1))
|
||||
draw.ellipse((x - rx * scale, y - ry * scale, x + rx * scale, y + ry * scale), fill=value)
|
||||
|
||||
|
||||
def save_grease(path: Path, size: int = 4096) -> None:
|
||||
image = Image.new("L", (size, size), 0)
|
||||
draw = ImageDraw.Draw(image, "L")
|
||||
|
||||
for x, y, rx, ry, alpha in [
|
||||
(0.17, 0.38, 210, 88, 0.088),
|
||||
(0.83, 0.24, 170, 76, 0.081),
|
||||
(0.73, 0.76, 185, 78, 0.072),
|
||||
(0.5, 0.52, 300, 120, 0.053),
|
||||
]:
|
||||
soft_ellipse(draw, size * x, size * y, rx, ry, alpha)
|
||||
|
||||
for _ in range(8):
|
||||
x = size * (0.2 + RNG.random() * 0.62)
|
||||
y = size * (0.18 + RNG.random() * 0.64)
|
||||
rx = 36 + RNG.random() * 22
|
||||
ry = 14 + RNG.random() * 8
|
||||
alpha = 0.102 + RNG.random() * 0.042
|
||||
soft_ellipse(draw, x, y, rx, ry, alpha)
|
||||
for ridge in [i / 100 for i in range(-70, 71, 18)]:
|
||||
bbox = (
|
||||
x - rx * (0.26 + abs(ridge) * 0.58),
|
||||
y - ry * (0.2 + abs(ridge) * 0.5),
|
||||
x + rx * (0.26 + abs(ridge) * 0.58),
|
||||
y + ry * (0.2 + abs(ridge) * 0.5),
|
||||
)
|
||||
draw.arc(bbox, 15, 345, fill=round(245 * alpha * 0.42), width=2)
|
||||
|
||||
image.save(path)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ASSET_DIR.mkdir(parents=True, exist_ok=True)
|
||||
save_normal(ASSET_DIR / "table_normal_2k.png")
|
||||
save_dust(ASSET_DIR / "table_dust_4k.png")
|
||||
save_grease(ASSET_DIR / "table_grease_4k.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user