Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
- <title>Painted backdrop test</title>
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
- <link href="https://fonts.googleapis.com/css2?family=Pixelify+Sans:wght@500;700&family=Work+Sans:wght@400;500;600&display=swap" rel="stylesheet">
- <style>
- :root {
- --dusk: rgba(28, 24, 48, 0.86);
- --dusk-edge: rgba(190, 176, 230, 0.22);
- --ink: #ece7f8;
- --ink-soft: #b3aad0;
- --lantern: #ffcf6a;
- --frost: #5cc8ff;
- --track: rgba(255, 255, 255, 0.16);
- box-sizing: border-box;
- padding-top: env(safe-area-inset-top, 0px);
- padding-bottom: env(safe-area-inset-bottom, 0px);
- color-scheme: dark;
- }
- @media (prefers-color-scheme: light) {
- :root:not([data-theme="dark"]) {
- --dusk: rgba(246, 243, 255, 0.9);
- --dusk-edge: rgba(60, 50, 100, 0.2);
- --ink: #221d38;
- --ink-soft: #5b5378;
- --lantern: #b87400;
- --frost: #0a7fc0;
- --track: rgba(30, 20, 60, 0.18);
- color-scheme: light;
- }
- }
- :root[data-theme="dark"] {
- --dusk: rgba(28, 24, 48, 0.86);
- --dusk-edge: rgba(190, 176, 230, 0.22);
- --ink: #ece7f8;
- --ink-soft: #b3aad0;
- --lantern: #ffcf6a;
- --frost: #5cc8ff;
- --track: rgba(255, 255, 255, 0.16);
- }
- *, *::before, *::after { box-sizing: inherit; }
- html { scroll-padding-top: env(safe-area-inset-top, 0px); height: 100%; }
- body {
- height: 100%; margin: 0; overflow: hidden; background: #15122a;
- font-family: "Work Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
- color: var(--ink);
- }
- canvas#view { position: fixed; inset: 0; width: 100%; height: 100%; display: block; cursor: crosshair; }
- .panel {
- position: fixed; left: 14px; top: calc(14px + env(safe-area-inset-top, 0px));
- width: 290px; max-width: calc(100vw - 28px); max-height: calc(100% - 28px); overflow-y: auto;
- background: var(--dusk); border: 1px solid var(--dusk-edge); border-radius: 10px;
- padding: 16px 16px 14px; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
- font-size: 13px; line-height: 1.45;
- }
- .panel.hidden { display: none; }
- .panel h1 {
- font-family: "Pixelify Sans", "Work Sans", system-ui, sans-serif;
- font-weight: 700; font-size: 22px; line-height: 1.1; margin: 0 0 4px; letter-spacing: 0.01em;
- }
- .panel .lede { color: var(--ink-soft); margin: 0 0 12px; }
- .readout { display: grid; grid-template-columns: 1fr auto; gap: 2px 12px; margin: 0 0 12px; font-variant-numeric: tabular-nums; }
- .readout dt { color: var(--ink-soft); }
- .readout dd { margin: 0; text-align: right; font-weight: 600; }
- .readout dd.big { color: var(--lantern); font-size: 15px; }
- .row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 8px 0; }
- .row label { flex: 1; }
- .row output { min-width: 58px; text-align: right; font-variant-numeric: tabular-nums; color: var(--ink-soft); }
- select, input[type="range"] { accent-color: var(--lantern); }
- select {
- font: inherit; color: var(--ink); background: transparent; border: 1px solid var(--track);
- border-radius: 6px; padding: 3px 6px;
- }
- select option { color: #221d38; }
- input[type="range"] { width: 120px; }
- .toggle { display: flex; align-items: center; gap: 8px; margin: 6px 0; cursor: pointer; }
- .toggle input { accent-color: var(--lantern); width: 15px; height: 15px; margin: 0; }
- .toggle kbd { margin-left: auto; }
- kbd {
- font-family: inherit; font-size: 11px; color: var(--ink-soft);
- border: 1px solid var(--track); border-radius: 4px; padding: 0 5px;
- }
- .legend { display: flex; align-items: center; gap: 8px; margin: 2px 0 0 23px; color: var(--ink-soft); font-size: 12px; }
- .swatch { width: 12px; height: 12px; border-radius: 2px; background: var(--frost); flex: none; }
- .keys { margin: 12px 0 0; padding-top: 10px; border-top: 1px solid var(--track); color: var(--ink-soft); font-size: 12px; }
- .keys p { margin: 3px 0; }
- :focus-visible { outline: 2px solid var(--lantern); outline-offset: 2px; }
- .hint {
- position: fixed; left: 50%; bottom: calc(22px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%);
- background: var(--dusk); border: 1px solid var(--dusk-edge); border-radius: 8px;
- padding: 8px 14px; font-size: 13px; pointer-events: none; white-space: nowrap;
- }
- .hint.hidden { display: none; }
- .loading {
- position: fixed; inset: 0; display: grid; place-items: center; background: #15122a; color: #ece7f8;
- font-family: "Pixelify Sans", "Work Sans", system-ui, sans-serif; font-size: 22px;
- }
- .loading.hidden { display: none; }
- .error { font-family: "Work Sans", system-ui, sans-serif; font-size: 15px; max-width: 420px; text-align: center; line-height: 1.5; }
- .group { margin: 10px 0 0; padding-top: 10px; border-top: 1px solid var(--track); }
- .group h2 { font-size: 13px; font-weight: 600; margin: 0 0 4px; }
- .toggle.main { font-weight: 600; }
- .btn {
- font: inherit; color: var(--ink); background: transparent; border: 1px solid var(--track);
- border-radius: 6px; padding: 5px 10px; margin: 6px 0 2px; cursor: pointer; display: inline-flex; gap: 8px; align-items: center;
- }
- .btn:hover { border-color: var(--lantern); }
- </style>
- </head>
- <body>
- <canvas id="view"></canvas>
- <div class="loading" id="loading">Building the valley…</div>
- <aside class="panel" id="panel" aria-label="Test controls">
- <h1>Painted backdrop test</h1>
- <p class="lede">Near things are live 3D. Everything farther away is a snapshot painted onto the sky, redrawn from where you stand now and then.</p>
- <dl class="readout">
- <dt>Frame rate</dt><dd id="fps">–</dd>
- <dt>Internal resolution</dt><dd id="res">–</dd>
- <dt>Backdrop painted</dt><dd id="age">–</dd>
- <dt>Repaints so far</dt><dd id="count">0</dd>
- </dl>
- <label class="toggle main"><input type="checkbox" id="optBackdrop" checked> Painted backdrop <kbd>V</kbd></label>
- <label class="toggle"><input type="checkbox" id="optTint"> Show the painted part <kbd>F</kbd></label>
- <div class="legend"><span class="swatch"></span>Blue is backdrop, the rest is live 3D</div>
- <div class="row">
- <label for="dLive">Live 3D out to</label>
- <input type="range" id="dLive" min="20" max="600" step="5" value="150">
- <output id="dLiveOut">150 m</output>
- </div>
- <div class="row">
- <label for="dFade">Fade-in band</label>
- <input type="range" id="dFade" min="0" max="150" step="5" value="30">
- <output id="dFadeOut">30 m</output>
- </div>
- <div class="row">
- <label for="rDist">Repaint every</label>
- <input type="range" id="rDist" min="0" max="500" step="5" value="60">
- <output id="rDistOut">60 m</output>
- </div>
- <div class="row">
- <label for="rTime">Also repaint every</label>
- <input type="range" id="rTime" min="0" max="30" step="0.5" value="0">
- <output id="rTimeOut">off</output>
- </div>
- <div class="row">
- <label for="dis">Dissolve length</label>
- <input type="range" id="dis" min="0" max="5" step="0.1" value="1">
- <output id="disOut">1 s</output>
- </div>
- <label class="toggle"><input type="checkbox" id="optAnim"> Keep moving things live <kbd>L</kbd></label>
- <button type="button" class="btn" id="repaint">Repaint now <kbd>Space</kbd></button>
- <div class="group">
- <h2>Look and movement</h2>
- <div class="row">
- <label for="pw">Pixel width</label>
- <select id="pw">
- <option value="160">160</option>
- <option value="240">240</option>
- <option value="320" selected>320</option>
- <option value="426">426</option>
- <option value="640">640</option>
- <option value="full">Full resolution</option>
- </select>
- </div>
- <div class="row">
- <label for="speed">Walk speed</label>
- <input type="range" id="speed" min="1" max="40" step="0.5" value="5">
- <output id="speedOut">5 m/s</output>
- </div>
- <div class="row">
- <label for="haze">Haze</label>
- <input type="range" id="haze" min="0" max="100" step="1" value="30">
- <output id="hazeOut">30</output>
- </div>
- <label class="toggle"><input type="checkbox" id="optAuto" checked> Walk the path automatically <kbd>T</kbd></label>
- <label class="toggle"><input type="checkbox" id="optDither" checked> Limited palette with dither <kbd>G</kbd></label>
- <label class="toggle"><input type="checkbox" id="optOutline" checked> Pixel outlines <kbd>O</kbd></label>
- </div>
- <div class="keys">
- <p>Click the scene to look around. Esc releases the mouse.</p>
- <p><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> move, <kbd>Shift</kbd> run ×3, <kbd>R</kbd> back to the start, <kbd>P</kbd> next pixel width, <kbd>H</kbd> hide this panel</p>
- </div>
- </aside>
- <div class="hint" id="hint">Click to look around. W A S D to move.</div>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
- <script>
- (() => {
- "use strict";
- const $ = (id) => document.getElementById(id);
- if (typeof THREE === "undefined") {
- $("loading").innerHTML = '<div class="error">The 3D library did not load. Check your connection and reload the page.</div>';
- return;
- }
- // ---------------------------------------------------------------------------
- // Noise and helpers
- // ---------------------------------------------------------------------------
- function hash2(x, y) {
- let h = (Math.imul(x | 0, 374761393) + Math.imul(y | 0, 668265263)) | 0;
- h = Math.imul(h ^ (h >>> 13), 1274126177);
- h ^= h >>> 16;
- return (h >>> 0) / 4294967296;
- }
- function vnoise(x, y) {
- const xi = Math.floor(x), yi = Math.floor(y);
- const xf = x - xi, yf = y - yi;
- const u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf);
- const a = hash2(xi, yi), b = hash2(xi + 1, yi), c = hash2(xi, yi + 1), d = hash2(xi + 1, yi + 1);
- return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
- }
- function fbm(x, y, oct) {
- let s = 0, a = 0.5, f = 1, n = 0;
- for (let i = 0; i < oct; i++) { s += a * vnoise(x * f + i * 17.3, y * f - i * 9.1); n += a; a *= 0.5; f *= 2.03; }
- return s / n;
- }
- function ridged(x, y, oct) {
- let s = 0, a = 0.5, f = 1, n = 0;
- for (let i = 0; i < oct; i++) {
- let r = 1 - Math.abs(vnoise(x * f + i * 31.7, y * f + i * 5.3) * 2 - 1);
- r *= r; s += a * r; n += a; a *= 0.5; f *= 2.1;
- }
- return s / n;
- }
- const smooth = (a, b, x) => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); };
- let seed = 1337;
- const rand = () => {
- seed = (seed + 0x6D2B79F5) | 0;
- let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
- };
- // ---------------------------------------------------------------------------
- // World layout (meters; forward is -z)
- // ---------------------------------------------------------------------------
- const CASTLE = { x: 0, z: -1480 };
- const RING = { x: 0, z: -900 };
- const PATH_START = 120, PATH_END = -1060;
- const pathX = (z) => 34 * Math.sin(z * 0.0062) + 18 * Math.sin(z * 0.017 + 1.3);
- function H(x, z) {
- const side = Math.abs(x - pathX(z));
- let h = (fbm(x * 0.004, z * 0.004, 4) - 0.5) * 34;
- h += smooth(70, 650, side) * (100 + 110 * fbm(x * 0.0022 + 7.1, z * 0.0022 - 3.3, 3));
- h += 26 * smooth(-260, 60, z);
- const dx = x - CASTLE.x, dz = z - CASTLE.z;
- const e = Math.exp(-(dx * dx + dz * dz) / (320 * 320));
- if (e > 0.001) {
- h += 640 * Math.min(1, 1.3 * e);
- h += 140 * (fbm(x * 0.012 + 3, z * 0.012, 3) - 0.5) * e * (1 - smooth(0.7, 0.95, 1.3 * e));
- }
- const rr = Math.hypot(x - RING.x, z - RING.z);
- const ring = smooth(1900, 3500, rr);
- const back = smooth(2300, 3300, -z) * (1 - smooth(1300, 2600, Math.abs(x)));
- const m = Math.max(ring, back);
- if (m > 0) h += m * (620 + 900 * ridged(x * 0.0011, z * 0.0011, 4));
- return h;
- }
- // ---------------------------------------------------------------------------
- // Renderer, scene, camera
- // ---------------------------------------------------------------------------
- const canvas = $("view");
- let renderer;
- try {
- renderer = new THREE.WebGLRenderer({ canvas, antialias: false, powerPreference: "high-performance" });
- } catch (err) {
- $("loading").innerHTML = '<div class="error">WebGL is not available in this browser, so the scene cannot render. Try a recent Chrome, Edge or Firefox with hardware acceleration on.</div>';
- return;
- }
- renderer.setPixelRatio(1);
- const scene = new THREE.Scene();
- const HORIZON = new THREE.Color("#a497c2");
- const ZENITH = new THREE.Color("#2f3268");
- scene.fog = new THREE.FogExp2(new THREE.Color("#9e93bf"), 0.00011);
- const VFOV = 62;
- const camera = new THREE.PerspectiveCamera(VFOV, 16 / 9, 0.5, 14000);
- camera.rotation.order = "YXZ";
- const SUN_DIR = new THREE.Vector3(-0.55, 0.62, 0.55).normalize();
- scene.add(new THREE.HemisphereLight(0xc4b8ec, 0x39452c, 0.62));
- const sun = new THREE.DirectionalLight(0xffe2c4, 0.85);
- sun.position.copy(SUN_DIR).multiplyScalar(1000);
- scene.add(sun);
- // ---------------------------------------------------------------------------
- // Live/backdrop split. Every scene material gets a distance test:
- // live pass: keep what is within D of the camera (dithered fade over the last W meters)
- // snapshot pass: keep only what is beyond D - W of the snapshot point
- // ---------------------------------------------------------------------------
- const clipU = {
- center: { value: new THREE.Vector3() },
- D: { value: 150 }, W: { value: 30 }, mode: { value: 0 }, animLive: { value: 0 }
- };
- const CLIP_VERT = `
- vec4 cwp = vec4(transformed, 1.0);
- #ifdef USE_INSTANCING
- cwp = instanceMatrix * cwp;
- #endif
- vClipWorld = (modelMatrix * cwp).xyz;`;
- const CLIP_FRAG_DECL = `
- varying vec3 vClipWorld;
- uniform vec3 uClipCenter; uniform float uClipD; uniform float uClipW; uniform float uClipMode;
- uniform float uAnimLive; uniform float uIsAnim;
- float clipBayer2(vec2 a) { a = floor(a); return fract(dot(a, vec2(0.5, a.y * 0.75))); }
- float clipBayer4(vec2 a) { return clipBayer2(0.5 * a) * 0.25 + clipBayer2(a); }`;
- const CLIP_FRAG = `
- {
- float cd = distance(vClipWorld, uClipCenter);
- bool alwaysLive = uIsAnim * uAnimLive > 0.5;
- float W = max(uClipW, 0.5);
- if (uClipMode > 0.5) {
- if (alwaysLive || cd < uClipD - W) discard;
- } else if (uClipMode > -0.5 && !alwaysLive) {
- float keep = 1.0 - smoothstep(uClipD - W, uClipD, cd);
- if (keep <= clipBayer4(gl_FragCoord.xy)) discard;
- }
- }`;
- function patch(mat, isAnim = false) {
- const isAnimU = { value: isAnim ? 1 : 0 };
- mat.onBeforeCompile = (shader) => {
- shader.uniforms.uClipCenter = clipU.center;
- shader.uniforms.uClipD = clipU.D;
- shader.uniforms.uClipW = clipU.W;
- shader.uniforms.uClipMode = clipU.mode;
- shader.uniforms.uAnimLive = clipU.animLive;
- shader.uniforms.uIsAnim = isAnimU;
- shader.vertexShader = shader.vertexShader
- .replace("#include <common>", "#include <common>\nvarying vec3 vClipWorld;")
- .replace("#include <project_vertex>", "#include <project_vertex>\n" + CLIP_VERT);
- shader.fragmentShader = shader.fragmentShader
- .replace("#include <common>", "#include <common>\n" + CLIP_FRAG_DECL)
- .replace("#include <clipping_planes_fragment>", "#include <clipping_planes_fragment>\n" + CLIP_FRAG);
- };
- return mat;
- }
- // Sky with the painted backdrop on it (follows the camera, so it never gets closer)
- const skyU = {
- horizon: { value: HORIZON }, zenith: { value: ZENITH }, sunDir: { value: SUN_DIR },
- tOld: { value: null }, tNew: { value: null }, mixT: { value: 1 },
- useBackdrop: { value: 1 }, tint: { value: 0 }
- };
- const sky = new THREE.Mesh(
- new THREE.SphereGeometry(11000, 32, 16),
- new THREE.ShaderMaterial({
- side: THREE.BackSide, depthWrite: false, depthTest: false, fog: false,
- uniforms: skyU,
- vertexShader: `
- varying vec3 vDir;
- void main() {
- vDir = position;
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
- }`,
- fragmentShader: `
- uniform vec3 horizon; uniform vec3 zenith; uniform vec3 sunDir;
- uniform samplerCube tOld; uniform samplerCube tNew;
- uniform float mixT; uniform float useBackdrop; uniform float tint;
- varying vec3 vDir;
- float b2(vec2 a) { a = floor(a); return fract(dot(a, vec2(0.5, a.y * 0.75))); }
- float b4(vec2 a) { return b2(0.5 * a) * 0.25 + b2(a); }
- void main() {
- vec3 d = normalize(vDir);
- float t = clamp(d.y * 1.5 + 0.03, 0.0, 1.0);
- vec3 c = mix(horizon, zenith, pow(t, 0.75));
- float s = max(dot(d, sunDir), 0.0);
- c += vec3(1.0, 0.78, 0.55) * (pow(s, 48.0) * 0.45 + pow(s, 6.0) * 0.08);
- if (useBackdrop > 0.5) {
- // pixel-dither dissolve from the old painting to the new one
- vec4 p = (mixT > b4(gl_FragCoord.xy)) ? textureCube(tNew, d) : textureCube(tOld, d);
- vec3 pc = p.rgb;
- if (tint > 0.5) pc = mix(pc, vec3(0.25, 0.72, 1.0) * p.a, 0.5);
- c = pc + c * (1.0 - p.a);
- }
- gl_FragColor = vec4(c, 1.0);
- }`
- })
- );
- sky.renderOrder = -1;
- sky.frustumCulled = false;
- scene.add(sky);
- const flat = (color, extra = {}) => patch(new THREE.MeshPhongMaterial(Object.assign({
- color, flatShading: true, shininess: 0, specular: 0x000000
- }, extra)));
- const animFlat = (color, extra = {}) => patch(new THREE.MeshPhongMaterial(Object.assign({
- color, flatShading: true, shininess: 0, specular: 0x000000
- }, extra)), true);
- // Merge several (geometry, color, matrix) parts into one non-indexed geometry with vertex colors
- function mergeParts(parts) {
- const pos = [], col = [];
- const c = new THREE.Color();
- for (const p of parts) {
- const g = p.geo.index ? p.geo.toNonIndexed() : p.geo.clone();
- if (p.matrix) g.applyMatrix4(p.matrix);
- const a = g.attributes.position.array;
- c.set(p.color);
- for (let i = 0; i < a.length; i += 3) { pos.push(a[i], a[i + 1], a[i + 2]); col.push(c.r, c.g, c.b); }
- g.dispose();
- }
- const out = new THREE.BufferGeometry();
- out.setAttribute("position", new THREE.Float32BufferAttribute(pos, 3));
- out.setAttribute("color", new THREE.Float32BufferAttribute(col, 3));
- out.computeVertexNormals();
- return out;
- }
- const M = () => new THREE.Matrix4();
- const at = (x, y, z, ry = 0, sx = 1, sy = 1, sz = 1) =>
- M().compose(new THREE.Vector3(x, y, z), new THREE.Quaternion().setFromEuler(new THREE.Euler(0, ry, 0)), new THREE.Vector3(sx, sy, sz));
- // Animated pieces
- const anim = { windmill: null, birds: [], clouds: null, mist: null };
- function buildWorld() {
- // ---------------- Terrain ----------------
- const N = 400, S = 4600, A = 0.24, CZ = -500;
- const map = (u) => S * (A * u + (1 - A) * u * u * u);
- const verts = new Float32Array((N + 1) * (N + 1) * 3);
- let k = 0;
- for (let j = 0; j <= N; j++) {
- const z = CZ + map(-1 + 2 * j / N);
- for (let i = 0; i <= N; i++) {
- const x = map(-1 + 2 * i / N);
- verts[k++] = x; verts[k++] = H(x, z); verts[k++] = z;
- }
- }
- const idx = new Uint32Array(N * N * 6);
- k = 0;
- for (let j = 0; j < N; j++) for (let i = 0; i < N; i++) {
- const a = j * (N + 1) + i, b = a + 1, c = a + N + 1, d = c + 1;
- idx[k++] = a; idx[k++] = c; idx[k++] = b;
- idx[k++] = b; idx[k++] = c; idx[k++] = d;
- }
- const tg = new THREE.BufferGeometry();
- tg.setAttribute("position", new THREE.BufferAttribute(verts, 3));
- tg.setIndex(new THREE.BufferAttribute(idx, 1));
- tg.computeVertexNormals();
- const nrm = tg.attributes.normal.array;
- const cols = new Float32Array(verts.length);
- const cGrassA = new THREE.Color("#3c7038"), cGrassB = new THREE.Color("#78a443"), cMeadow = new THREE.Color("#8cb84e");
- const cForest = new THREE.Color("#2c4f2e"), cRock = new THREE.Color("#5d5874"), cRockDark = new THREE.Color("#403c58");
- const cSnow = new THREE.Color("#dcd6ec"), tmp = new THREE.Color();
- for (let v = 0; v < verts.length; v += 3) {
- const x = verts[v], h = verts[v + 1], z = verts[v + 2], ny = nrm[v + 1];
- const n = fbm(x * 0.02, z * 0.02, 2);
- tmp.copy(cGrassA).lerp(cGrassB, n);
- const side = Math.abs(x - pathX(z));
- if (z < -150 && z > -1000) tmp.lerp(cMeadow, 0.55 * (1 - smooth(60, 260, side)) * n);
- const forestN = fbm(x * 0.005 + 11, z * 0.005 - 4, 3);
- tmp.lerp(cForest, 0.7 * smooth(0.47, 0.6, forestN) * smooth(40, 160, side));
- if (z > -90 && Math.abs(x) < 700) tmp.lerp(cForest, 0.5 * smooth(-90, -30, z));
- const steep = 1 - ny;
- const rockAmt = Math.max(smooth(0.28, 0.5, steep), smooth(620, 820, h + n * 120));
- tmp.lerp(n > 0.5 ? cRock : cRockDark, rockAmt);
- const snowAmt = smooth(1250, 1400, h + n * 160) * smooth(0.25, 0.45, ny);
- tmp.lerp(cSnow, snowAmt);
- cols[v] = tmp.r; cols[v + 1] = tmp.g; cols[v + 2] = tmp.b;
- }
- tg.setAttribute("color", new THREE.BufferAttribute(cols, 3));
- scene.add(new THREE.Mesh(tg, flat(0xffffff, { vertexColors: true })));
- // ---------------- Path ----------------
- {
- const pos = [], col = [];
- const stone = [new THREE.Color("#d8c9a4"), new THREE.Color("#bfae8a"), new THREE.Color("#e6dcc0"), new THREE.Color("#a99a7c")];
- const step = 2;
- let prev = null;
- for (let z = PATH_START; z >= PATH_END; z -= step) {
- const x = pathX(z);
- const tx = pathX(z - 1) - x, tz = -1;
- const len = Math.hypot(tx, tz);
- const nx = -tz / len, nz = tx / len; // perpendicular
- const w = 1.6 + 0.35 * Math.sin(z * 0.21) + 0.25 * (hash2(z | 0, 7) - 0.5);
- const L = [x + nx * w, 0, z + nz * w], R = [x - nx * w, 0, z - nz * w];
- L[1] = H(L[0], L[2]) + 0.3; R[1] = H(R[0], R[2]) + 0.3;
- if (prev) {
- const c1 = stone[hash2(z | 0, 1) * 4 | 0], c2 = stone[hash2(z | 0, 2) * 4 | 0];
- pos.push(...prev.L, ...prev.R, ...L); col.push(c1.r, c1.g, c1.b, c1.r, c1.g, c1.b, c1.r, c1.g, c1.b);
- pos.push(...prev.R, ...R, ...L); col.push(c2.r, c2.g, c2.b, c2.r, c2.g, c2.b, c2.r, c2.g, c2.b);
- }
- prev = { L, R };
- }
- const g = new THREE.BufferGeometry();
- g.setAttribute("position", new THREE.Float32BufferAttribute(pos, 3));
- g.setAttribute("color", new THREE.Float32BufferAttribute(col, 3));
- g.computeVertexNormals();
- const mat = flat(0xffffff, { vertexColors: true, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2, side: THREE.DoubleSide });
- scene.add(new THREE.Mesh(g, mat));
- }
- // ---------------- Trees and bushes ----------------
- const coniferGeo = mergeParts([
- { geo: new THREE.CylinderGeometry(0.35, 0.5, 3.2, 5, 1, true), color: "#4a3527", matrix: at(0, 1.6, 0) },
- { geo: new THREE.ConeGeometry(3.3, 6, 7, 1, true), color: "#2c5536", matrix: at(0, 5.2, 0) },
- { geo: new THREE.ConeGeometry(2.6, 5, 7, 1, true), color: "#356640", matrix: at(0, 8.2, 0, 0.4) },
- { geo: new THREE.ConeGeometry(1.8, 4.2, 7, 1, true), color: "#417449", matrix: at(0, 11, 0, 0.8) }
- ]);
- const roundGeo = mergeParts([
- { geo: new THREE.CylinderGeometry(0.35, 0.5, 3.4, 5, 1, true), color: "#523b2a", matrix: at(0, 1.7, 0) },
- { geo: new THREE.IcosahedronGeometry(3.4, 0), color: "#4f8a3c", matrix: at(0, 5.6, 0, 0, 1, 0.9, 1) },
- { geo: new THREE.IcosahedronGeometry(2.2, 0), color: "#5f9a44", matrix: at(1.2, 7.4, 0.6, 0.7) }
- ]);
- const treeMat = flat(0xffffff, { vertexColors: true });
- const MAX_CON = 22000, MAX_ROUND = 12000;
- const conifers = new THREE.InstancedMesh(coniferGeo, treeMat, MAX_CON);
- const rounds = new THREE.InstancedMesh(roundGeo, treeMat, MAX_ROUND);
- let nCon = 0, nRound = 0;
- const m4 = new THREE.Matrix4(), q = new THREE.Quaternion(), e3 = new THREE.Euler(), p3 = new THREE.Vector3(), s3 = new THREE.Vector3();
- const tint = new THREE.Color();
- function place(mesh, idx, x, y, z, s, lean = 0) {
- e3.set((rand() - 0.5) * lean, rand() * Math.PI * 2, (rand() - 0.5) * lean);
- q.setFromEuler(e3); p3.set(x, y, z); s3.set(s * (0.9 + rand() * 0.2), s, s * (0.9 + rand() * 0.2));
- m4.compose(p3, q, s3);
- mesh.setMatrixAt(idx, m4);
- const b = 0.78 + rand() * 0.4;
- tint.setRGB(b * (0.95 + rand() * 0.1), b, b * (0.9 + rand() * 0.15));
- mesh.setColorAt(idx, tint);
- }
- function treeDensity(x, z) {
- const side = Math.abs(x - pathX(z));
- if (z < PATH_START + 60 && z > PATH_END - 20 && side < 7) return 0;
- if (Math.hypot(x - CASTLE.x, z - CASTLE.z) < 360) return 0;
- let d = 0.035;
- if (z > -90 && z < 520 && Math.abs(x) < 700) d = Math.max(d, 0.8 * smooth(-90, -30, z) * (1 - smooth(450, 700, Math.abs(x))));
- const n = fbm(x * 0.005 + 11, z * 0.005 - 4, 3);
- d = Math.max(d, smooth(0.47, 0.6, n) * 0.55 * smooth(40, 160, side));
- if (z < -240 && z > -950 && side < 220) d *= 0.3;
- return d;
- }
- const STEP = 10;
- for (let gz = 600; gz > -2600 && (nCon < MAX_CON || nRound < MAX_ROUND); gz -= STEP) {
- for (let gx = -1900; gx < 1900; gx += STEP) {
- const x = gx + (rand() - 0.5) * STEP * 0.95, z = gz + (rand() - 0.5) * STEP * 0.95;
- if (rand() > treeDensity(x, z)) continue;
- const h = H(x, z);
- if (h > 780) continue;
- if (Math.abs(H(x + 3, z) - h) > 2.6 || Math.abs(H(x, z + 3) - h) > 2.6) continue;
- const side = Math.abs(x - pathX(z));
- const roundChance = side < 250 && z < -60 ? 0.35 : 0.07;
- if (rand() < roundChance) {
- if (nRound < MAX_ROUND) place(rounds, nRound++, x, h - 0.4, z, 0.8 + rand() * 0.7, 0.08);
- } else if (nCon < MAX_CON) {
- place(conifers, nCon++, x, h - 0.4, z, 0.8 + rand() * 1.1, 0.06);
- }
- }
- }
- // Bushes along the path for strong foreground motion
- for (let z = PATH_START + 40; z > PATH_END; z -= 1.6) {
- for (const sgn of [-1, 1]) {
- if (rand() > 0.55) continue;
- const off = 2.6 + Math.pow(rand(), 1.6) * 14;
- const x = pathX(z) + sgn * off;
- const zz = z + (rand() - 0.5) * 1.5;
- if (nRound < MAX_ROUND) place(rounds, nRound++, x, H(x, zz) - 1.1 - rand() * 0.4, zz, 0.22 + rand() * 0.28, 0.3);
- }
- }
- conifers.count = nCon; rounds.count = nRound;
- conifers.instanceMatrix.needsUpdate = true; rounds.instanceMatrix.needsUpdate = true;
- if (conifers.instanceColor) conifers.instanceColor.needsUpdate = true;
- if (rounds.instanceColor) rounds.instanceColor.needsUpdate = true;
- conifers.frustumCulled = false; rounds.frustumCulled = false;
- scene.add(conifers, rounds);
- // ---------------- Village ----------------
- const houseGeo = mergeParts([
- { geo: new THREE.BoxGeometry(8, 8, 6), color: "#e6dbc0", matrix: at(0, 1, 0) },
- { geo: new THREE.ConeGeometry(6.4, 4.2, 4), color: "#a5432c", matrix: at(0, 7.1, 0, Math.PI / 4, 1, 1, 0.78) },
- { geo: new THREE.BoxGeometry(1, 3, 1), color: "#6c5a4c", matrix: at(2.2, 7.6, 1) },
- { geo: new THREE.BoxGeometry(1.4, 2.2, 0.2), color: "#4a3527", matrix: at(0, -1.9, 3.05) }
- ]);
- const houses = new THREE.InstancedMesh(houseGeo, flat(0xffffff, { vertexColors: true }), 90);
- const placed = [];
- let tries = 0, nHouse = 0;
- while (nHouse < 90 && tries++ < 5000) {
- const t = rand();
- const z = -250 - Math.pow(t, 1.3) * 720;
- const side = (rand() < 0.5 ? -1 : 1) * (13 + Math.pow(rand(), 1.7) * 230);
- const x = pathX(z) + side;
- const h = H(x, z);
- if (Math.abs(H(x + 5, z) - h) > 2.2 || Math.abs(H(x, z + 5) - h) > 2.2) continue;
- if (placed.some((p) => Math.hypot(p[0] - x, p[1] - z) < 17)) continue;
- placed.push([x, z]);
- const facePath = Math.atan2(pathX(z) - x, 0) + (rand() - 0.5) * 0.8;
- const s = 0.85 + rand() * 0.45;
- m4.compose(p3.set(x, h + 3 * s - 0.2, z), q.setFromEuler(e3.set(0, facePath, 0)), s3.set(s * (0.9 + rand() * 0.4), s, s));
- houses.setMatrixAt(nHouse, m4);
- const b = 0.85 + rand() * 0.25;
- tint.setRGB(b, b * (0.93 + rand() * 0.08), b * (0.88 + rand() * 0.12));
- houses.setColorAt(nHouse, tint);
- nHouse++;
- }
- houses.count = nHouse;
- houses.frustumCulled = false;
- scene.add(houses);
- // Windmill (a moving object in the middle distance)
- {
- const wz = -430, wx = pathX(wz) - 58, wh = H(wx, wz);
- const g = new THREE.Group();
- g.position.set(wx, wh - 1, wz);
- g.add(new THREE.Mesh(new THREE.CylinderGeometry(2.6, 4, 16, 8), animFlat(0xe0d4b8)));
- g.children[0].position.y = 8;
- const cap = new THREE.Mesh(new THREE.ConeGeometry(3.4, 4, 8), animFlat(0x7a3a2a));
- cap.position.y = 18; g.add(cap);
- const hub = new THREE.Group(); hub.position.set(0, 15.5, 3.4); g.add(hub);
- for (let i = 0; i < 4; i++) {
- const arm = new THREE.Group(); arm.rotation.z = i * Math.PI / 2;
- const blade = new THREE.Mesh(new THREE.BoxGeometry(2.2, 11, 0.25), animFlat(0xcfc2a8));
- blade.position.y = 6.2; arm.add(blade);
- const spar = new THREE.Mesh(new THREE.BoxGeometry(0.35, 12, 0.35), animFlat(0x5a4636));
- spar.position.y = 6; arm.add(spar);
- hub.add(arm);
- }
- g.rotation.y = 0.35;
- scene.add(g);
- anim.windmill = hub;
- }
- // ---------------- Castle ----------------
- const castleY = H(CASTLE.x, CASTLE.z);
- let minTop = castleY;
- for (let a = 0; a < Math.PI * 2; a += Math.PI / 8) minTop = Math.min(minTop, H(CASTLE.x + Math.cos(a) * 128, CASTLE.z + Math.sin(a) * 128));
- {
- const S = 1.8;
- const wall = "#3e3c58", wallLight = "#4a4766", roof = "#27263e";
- const parts = [];
- const base = minTop - 12;
- // Curtain wall ring
- parts.push({ geo: new THREE.CylinderGeometry(70 * S, 74 * S, 40 * S, 10, 1, true), color: wall, matrix: at(0, base + 20 * S, 0) });
- parts.push({ geo: new THREE.CylinderGeometry(70 * S, 70 * S, 2, 10), color: wallLight, matrix: at(0, base + 40 * S, 0) });
- // Outer towers
- for (let i = 0; i < 6; i++) {
- const a = i / 6 * Math.PI * 2 + 0.3;
- const tx = Math.cos(a) * 71 * S, tz = Math.sin(a) * 71 * S;
- const hgt = (52 + (i % 3) * 8) * S;
- parts.push({ geo: new THREE.CylinderGeometry(7 * S, 7.6 * S, hgt, 8), color: wallLight, matrix: at(tx, base + hgt / 2, tz) });
- parts.push({ geo: new THREE.ConeGeometry(9 * S, 22 * S, 8), color: roof, matrix: at(tx, base + hgt + 11 * S, tz) });
- }
- // Inner towers
- for (let i = 0; i < 4; i++) {
- const a = i / 4 * Math.PI * 2 + Math.PI / 4;
- const tx = Math.cos(a) * 34 * S, tz = Math.sin(a) * 34 * S;
- const hgt = (78 + (i % 2) * 14) * S;
- parts.push({ geo: new THREE.CylinderGeometry(9 * S, 10 * S, hgt, 8), color: wall, matrix: at(tx, base + hgt / 2, tz) });
- parts.push({ geo: new THREE.ConeGeometry(11.5 * S, 32 * S, 8), color: roof, matrix: at(tx, base + hgt + 16 * S, tz) });
- }
- // Keep and spire
- const keepH = 118 * S;
- parts.push({ geo: new THREE.BoxGeometry(32 * S, keepH, 32 * S), color: wallLight, matrix: at(0, base + keepH / 2, 0, 0.2) });
- parts.push({ geo: new THREE.ConeGeometry(19 * S, 64 * S, 4), color: roof, matrix: at(0, base + keepH + 32 * S, 0, 0.2 + Math.PI / 4) });
- for (let i = 0; i < 4; i++) {
- const a = 0.2 + i * Math.PI / 2 + Math.PI / 4;
- const tx = Math.sin(a) * 22 * S, tz = Math.cos(a) * 22 * S;
- parts.push({ geo: new THREE.CylinderGeometry(4 * S, 4 * S, 22 * S, 6), color: wall, matrix: at(tx, base + keepH + 6 * S, tz) });
- parts.push({ geo: new THREE.ConeGeometry(5.2 * S, 16 * S, 6), color: roof, matrix: at(tx, base + keepH + 25 * S, tz) });
- }
- const cg = mergeParts(parts);
- const castle = new THREE.Mesh(cg, flat(0xffffff, { vertexColors: true }));
- castle.position.set(CASTLE.x, 0, CASTLE.z);
- scene.add(castle);
- // Lit windows
- const winGeo = new THREE.BoxGeometry(2.4 * S, 4.2 * S, 1);
- const wins = new THREE.InstancedMesh(winGeo, patch(new THREE.MeshBasicMaterial({ color: 0xffcf6a, fog: true })), 160);
- let nw = 0;
- const addWin = (x, y, z, ry) => { if (nw < 160) { wins.setMatrixAt(nw++, at(CASTLE.x + x, y, CASTLE.z + z, ry)); } };
- for (let f = 0; f < 4; f++) {
- const ry = 0.2 + f * Math.PI / 2;
- for (let r = 0; r < 9; r++) for (let c = -1; c <= 1; c++) {
- if (rand() > 0.5) continue;
- const lx = c * 8.5 * S, ly = base + (20 + r * 11) * S, lz = 16.1 * S;
- addWin(Math.sin(ry) * lz + Math.cos(ry) * lx, ly, Math.cos(ry) * lz - Math.sin(ry) * lx, ry);
- }
- }
- for (let i = 0; i < 4; i++) {
- const a = i / 4 * Math.PI * 2 + Math.PI / 4;
- for (let r = 0; r < 5; r++) {
- if (rand() > 0.6) continue;
- const ang = a + (rand() - 0.5) * 1.2 + Math.PI * 0.75;
- const rx = Math.cos(a) * 34 * S + Math.cos(ang) * 9.6 * S, rz = Math.sin(a) * 34 * S + Math.sin(ang) * 9.6 * S;
- addWin(rx, base + (22 + r * 12) * S, rz, -ang + Math.PI / 2);
- }
- }
- wins.count = nw;
- wins.frustumCulled = false;
- scene.add(wins);
- // Birds circling the towers
- const birdMat = patch(new THREE.MeshBasicMaterial({ color: 0x1c1a2c, side: THREE.DoubleSide, fog: true }), true);
- const wingGeo = new THREE.BufferGeometry();
- wingGeo.setAttribute("position", new THREE.Float32BufferAttribute([0, 0, -1, 0, 0, 1, 1, 0, 0.2], 3));
- function makeBird(size) {
- const b = new THREE.Group();
- const l = new THREE.Mesh(wingGeo, birdMat), r = new THREE.Mesh(wingGeo, birdMat);
- r.scale.x = -1; l.scale.multiplyScalar(size); r.scale.set(-size, size, size);
- b.add(l, r); b.userData = { l, r };
- return b;
- }
- for (let i = 0; i < 14; i++) {
- const b = makeBird(7 + rand() * 3);
- b.userData.center = new THREE.Vector3(CASTLE.x, base + (130 + rand() * 90) * S, CASTLE.z);
- b.userData.radius = 90 + rand() * 170; b.userData.speed = (0.08 + rand() * 0.06) * (rand() < 0.5 ? -1 : 1);
- b.userData.phase = rand() * Math.PI * 2; b.userData.flap = 3 + rand() * 2;
- scene.add(b); anim.birds.push(b);
- }
- // A few birds over the village, closer to the viewer
- for (let i = 0; i < 8; i++) {
- const b = makeBird(1.8 + rand() * 0.8);
- const cz = -520;
- b.userData.center = new THREE.Vector3(pathX(cz) + 20, H(pathX(cz), cz) + 45 + rand() * 25, cz);
- b.userData.radius = 50 + rand() * 60; b.userData.speed = (0.18 + rand() * 0.1) * (rand() < 0.5 ? -1 : 1);
- b.userData.phase = rand() * Math.PI * 2; b.userData.flap = 5 + rand() * 3;
- scene.add(b); anim.birds.push(b);
- }
- // Mist ring under the castle
- const mist = new THREE.Group();
- mist.position.set(CASTLE.x, 0, CASTLE.z);
- const puffGeo = new THREE.IcosahedronGeometry(1, 1);
- const mistMat = animFlat(0xcfc7e2, { emissive: 0x2a2440 });
- for (let i = 0; i < 26; i++) {
- const a = i / 26 * Math.PI * 2 + rand() * 0.2;
- const r = 270 + rand() * 120;
- const p = new THREE.Mesh(puffGeo, mistMat);
- p.position.set(Math.cos(a) * r, castleY - 330 + rand() * 80, Math.sin(a) * r);
- p.scale.set(80 + rand() * 60, 26 + rand() * 18, 60 + rand() * 40);
- p.rotation.y = rand() * 3;
- mist.add(p);
- }
- scene.add(mist);
- anim.mist = mist;
- }
- // ---------------- Clouds ----------------
- const clouds = new THREE.Group();
- clouds.position.set(RING.x, 0, RING.z);
- const cloudGeo = new THREE.IcosahedronGeometry(1, 1);
- const cloudMat = animFlat(0xbfb6d6, { emissive: 0x26203c });
- for (let i = 0; i < 34; i++) {
- const a = rand() * Math.PI * 2, r = 1500 + rand() * 2600, y = 900 + rand() * 700;
- const cl = new THREE.Group();
- cl.position.set(Math.cos(a) * r, y, Math.sin(a) * r);
- cl.rotation.y = rand() * Math.PI;
- const nP = 4 + (rand() * 4 | 0);
- for (let j = 0; j < nP; j++) {
- const p = new THREE.Mesh(cloudGeo, cloudMat);
- p.position.set((rand() - 0.5) * 260, (rand() - 0.5) * 30, (rand() - 0.5) * 120);
- p.scale.set(70 + rand() * 90, 26 + rand() * 30, 55 + rand() * 60);
- cl.add(p);
- }
- clouds.add(cl);
- }
- scene.add(clouds);
- anim.clouds = clouds;
- }
- // ---------------------------------------------------------------------------
- // Low-resolution render target and composite pass
- // ---------------------------------------------------------------------------
- let rt = null, lowW = 320, lowH = 180;
- const post = new THREE.ShaderMaterial({
- depthTest: false, depthWrite: false,
- uniforms: {
- tColor: { value: null }, tDepth: { value: null },
- lowRes: { value: new THREE.Vector2(320, 180) },
- cNear: { value: camera.near }, cFar: { value: camera.far },
- useDither: { value: 1 }, useOutline: { value: 1 }, levels: { value: 14 }
- },
- vertexShader: `
- varying vec2 vUv;
- void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }`,
- fragmentShader: `
- uniform sampler2D tColor; uniform sampler2D tDepth;
- uniform vec2 lowRes; uniform float cNear; uniform float cFar;
- uniform float useDither; uniform float useOutline; uniform float levels;
- varying vec2 vUv;
- float linDepth(float d) { float z = d * 2.0 - 1.0; return (2.0 * cNear * cFar) / (cFar + cNear - z * (cFar - cNear)); }
- float bayer2(vec2 a) { a = floor(a); return fract(dot(a, vec2(0.5, a.y * 0.75))); }
- float bayer4(vec2 a) { return bayer2(0.5 * a) * 0.25 + bayer2(a); }
- float depthAt(vec2 px) {
- float d = texture2D(tDepth, (px + 0.5) / lowRes).x;
- return d < 0.99999 ? linDepth(d) : cFar * 10.0;
- }
- void main() {
- vec2 px = floor(vUv * lowRes);
- vec2 uv = (px + 0.5) / lowRes;
- vec3 col = texture2D(tColor, uv).rgb;
- float d = texture2D(tDepth, uv).x;
- if (useOutline > 0.5 && d < 0.99999) {
- float lz = linDepth(d);
- float far = max(max(depthAt(px + vec2(1.0, 0.0)), depthAt(px - vec2(1.0, 0.0))),
- max(depthAt(px + vec2(0.0, 1.0)), depthAt(px - vec2(0.0, 1.0))));
- if (far > lz * 1.15 + 1.0) col *= 0.52;
- }
- if (useDither > 0.5) {
- float b = bayer4(px);
- col = floor(clamp(col, 0.0, 1.0) * levels + 0.5 + (b - 0.5) * 0.55) / levels;
- }
- gl_FragColor = vec4(col, 1.0);
- }`
- });
- const postScene = new THREE.Scene();
- const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), post);
- quad.frustumCulled = false;
- postScene.add(quad);
- const postCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
- // ---------------------------------------------------------------------------
- // Backdrop snapshots: two cube maps (old and new) captured from a point
- // ---------------------------------------------------------------------------
- let cubeCam = null;
- let cubes = [null, null], newIdx = 0;
- const snap = { pos: new THREE.Vector3(), age: 0, count: 0, dissolveT: 1, pending: false };
- const cfg = { backdrop: true, D: 150, W: 30, rDist: 60, rTime: 0, dissolve: 1, animLive: false };
- const clearTmp = new THREE.Color();
- function makeCubes(size) {
- for (const c of cubes) if (c) c.dispose();
- const mk = () => new THREE.WebGLCubeRenderTarget(size, {
- format: THREE.RGBAFormat, generateMipmaps: false,
- minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter
- });
- cubes = [mk(), mk()];
- if (!cubeCam) cubeCam = new THREE.CubeCamera(0.5, 14000, cubes[0]);
- }
- function capture(target, where) {
- sky.visible = false;
- clipU.mode.value = 1;
- clipU.center.value.copy(where);
- renderer.getClearColor(clearTmp);
- const alpha = renderer.getClearAlpha();
- renderer.setClearColor(0x000000, 0);
- cubeCam.renderTarget = target;
- cubeCam.position.copy(where);
- cubeCam.updateMatrixWorld(true);
- cubeCam.update(renderer, scene);
- renderer.setClearColor(clearTmp, alpha);
- sky.visible = true;
- }
- function repaint(instant) {
- if (!instant && snap.dissolveT < 1 && cfg.dissolve > 0) { snap.pending = true; return; }
- newIdx = 1 - newIdx;
- capture(cubes[newIdx], camera.position);
- skyU.tOld.value = cubes[1 - newIdx].texture;
- skyU.tNew.value = cubes[newIdx].texture;
- snap.pos.copy(camera.position);
- snap.age = 0;
- snap.count++;
- snap.pending = false;
- snap.dissolveT = (instant || cfg.dissolve <= 0) ? 1 : 0;
- skyU.mixT.value = snap.dissolveT;
- $("count").textContent = String(snap.count);
- }
- const PW_OPTIONS = ["160", "240", "320", "426", "640", "full"];
- let pixelSetting = "320";
- let ready = false;
- function resize() {
- const w = Math.max(1, window.innerWidth), h = Math.max(1, window.innerHeight);
- renderer.setSize(w, h, false);
- camera.aspect = w / h;
- camera.updateProjectionMatrix();
- lowW = pixelSetting === "full" ? w : Math.min(parseInt(pixelSetting, 10), w);
- lowH = Math.max(1, Math.round(lowW * h / w));
- if (rt) { rt.depthTexture.dispose(); rt.dispose(); }
- rt = new THREE.WebGLRenderTarget(lowW, lowH, {
- minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter, generateMipmaps: false, depthBuffer: true
- });
- rt.depthTexture = new THREE.DepthTexture(lowW, lowH);
- rt.depthTexture.type = THREE.UnsignedIntType;
- post.uniforms.tColor.value = rt.texture;
- post.uniforms.tDepth.value = rt.depthTexture;
- post.uniforms.lowRes.value.set(lowW, lowH);
- // A cube face spans 90°, about the same as the screen width, so match the internal width
- const face = Math.min(2048, Math.max(64, Math.round(lowW * 1.0)));
- if (!cubes[0] || cubes[0].width !== face) {
- makeCubes(face);
- if (ready) { repaint(true); repaint(true); }
- }
- $("res").textContent = `${lowW} × ${lowH}`;
- }
- window.addEventListener("resize", resize);
- // ---------------------------------------------------------------------------
- // Controls
- // ---------------------------------------------------------------------------
- const keys = Object.create(null);
- let yaw = 0, pitch = 0.15, locked = false;
- const startZ = (() => { const m = /z=(-?\d+)/.exec(location.hash); return m ? Math.max(PATH_END + 60, Math.min(PATH_START - 20, +m[1])) : 60; })();
- const pos = new THREE.Vector3(pathX(startZ), 0, startZ);
- let pathZ = startZ;
- const opts = { auto: true, tint: false, dither: true, outline: true, speed: 5, haze: 30 };
- canvas.addEventListener("click", () => { if (!locked && canvas.requestPointerLock) canvas.requestPointerLock(); });
- document.addEventListener("pointerlockchange", () => {
- locked = document.pointerLockElement === canvas;
- $("hint").classList.toggle("hidden", locked);
- });
- document.addEventListener("mousemove", (e) => {
- if (!locked) return;
- yaw -= e.movementX * 0.0022;
- pitch = Math.max(-1.45, Math.min(1.45, pitch - e.movementY * 0.0022));
- });
- function syncUI() {
- $("optAuto").checked = opts.auto;
- $("optTint").checked = opts.tint;
- $("optBackdrop").checked = cfg.backdrop;
- $("optAnim").checked = cfg.animLive;
- $("optDither").checked = opts.dither;
- $("optOutline").checked = opts.outline;
- $("pw").value = pixelSetting;
- post.uniforms.useDither.value = opts.dither ? 1 : 0;
- post.uniforms.useOutline.value = opts.outline ? 1 : 0;
- skyU.useBackdrop.value = cfg.backdrop ? 1 : 0;
- skyU.tint.value = opts.tint ? 1 : 0;
- clipU.animLive.value = cfg.animLive ? 1 : 0;
- }
- function setAuto(on) {
- opts.auto = on;
- if (on) pathZ = Math.min(PATH_START - 20, Math.max(PATH_END + 50, pos.z));
- syncUI();
- }
- // Settings that change what goes into the painting take effect with a fresh repaint
- const repaintSoon = () => { if (ready) repaint(true); };
- const MOVE_KEYS = ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"];
- window.addEventListener("keydown", (e) => {
- const tag = e.target && e.target.tagName;
- if ((tag === "SELECT" || tag === "INPUT" || tag === "BUTTON") && !locked && !MOVE_KEYS.includes(e.code) && !e.code.startsWith("Shift")) return;
- keys[e.code] = true;
- if (e.code === "Space") e.preventDefault();
- if (e.repeat) return;
- if (e.code === "KeyF") { opts.tint = !opts.tint; syncUI(); }
- else if (e.code === "KeyV") { cfg.backdrop = !cfg.backdrop; syncUI(); if (cfg.backdrop) repaintSoon(); }
- else if (e.code === "KeyL") { cfg.animLive = !cfg.animLive; syncUI(); repaintSoon(); }
- else if (e.code === "Space") repaint(false);
- else if (e.code === "KeyT") setAuto(!opts.auto);
- else if (e.code === "KeyG") { opts.dither = !opts.dither; syncUI(); }
- else if (e.code === "KeyO") { opts.outline = !opts.outline; syncUI(); }
- else if (e.code === "KeyH") $("panel").classList.toggle("hidden");
- else if (e.code === "KeyR") { pathZ = 60; pos.set(pathX(60), H(pathX(60), 60) + 1.7, 60); yaw = 0; pitch = 0.15; camera.position.copy(pos); repaintSoon(); }
- else if (e.code === "KeyP") {
- pixelSetting = PW_OPTIONS[(PW_OPTIONS.indexOf(pixelSetting) + 1) % PW_OPTIONS.length];
- syncUI(); resize();
- }
- if (MOVE_KEYS.includes(e.code) && opts.auto) setAuto(false);
- });
- window.addEventListener("keyup", (e) => { keys[e.code] = false; });
- window.addEventListener("blur", () => { for (const k in keys) keys[k] = false; });
- $("optAuto").addEventListener("change", (e) => setAuto(e.target.checked));
- $("optTint").addEventListener("change", (e) => { opts.tint = e.target.checked; syncUI(); });
- $("optBackdrop").addEventListener("change", (e) => { cfg.backdrop = e.target.checked; syncUI(); if (cfg.backdrop) repaintSoon(); });
- $("optAnim").addEventListener("change", (e) => { cfg.animLive = e.target.checked; syncUI(); repaintSoon(); });
- $("optDither").addEventListener("change", (e) => { opts.dither = e.target.checked; syncUI(); });
- $("optOutline").addEventListener("change", (e) => { opts.outline = e.target.checked; syncUI(); });
- $("repaint").addEventListener("click", (e) => { repaint(false); e.currentTarget.blur(); });
- $("pw").addEventListener("change", (e) => { pixelSetting = e.target.value; resize(); e.target.blur(); });
- $("speed").addEventListener("input", (e) => { opts.speed = parseFloat(e.target.value); $("speedOut").textContent = `${opts.speed} m/s`; });
- $("haze").addEventListener("input", (e) => {
- opts.haze = parseInt(e.target.value, 10);
- $("hazeOut").textContent = String(opts.haze);
- scene.fog.density = 0.0000037 * opts.haze;
- });
- $("dLive").addEventListener("input", (e) => { cfg.D = +e.target.value; clipU.D.value = cfg.D; $("dLiveOut").textContent = `${cfg.D} m`; });
- $("dLive").addEventListener("change", repaintSoon);
- $("dFade").addEventListener("input", (e) => { cfg.W = +e.target.value; clipU.W.value = cfg.W; $("dFadeOut").textContent = `${cfg.W} m`; });
- $("dFade").addEventListener("change", repaintSoon);
- $("rDist").addEventListener("input", (e) => { cfg.rDist = +e.target.value; $("rDistOut").textContent = cfg.rDist === 0 ? "every frame" : `${cfg.rDist} m`; });
- $("rTime").addEventListener("input", (e) => { cfg.rTime = +e.target.value; $("rTimeOut").textContent = cfg.rTime === 0 ? "off" : `${cfg.rTime} s`; });
- $("dis").addEventListener("input", (e) => { cfg.dissolve = +e.target.value; $("disOut").textContent = cfg.dissolve === 0 ? "instant" : `${cfg.dissolve} s`; });
- // ---------------------------------------------------------------------------
- // Loop
- // ---------------------------------------------------------------------------
- const clock = new THREE.Clock();
- let frames = 0, fpsT = 0, hudT = 0, time = 0;
- const angDiff = (a, b) => { let d = a - b; while (d > Math.PI) d -= Math.PI * 2; while (d < -Math.PI) d += Math.PI * 2; return d; };
- function step(dt) {
- const v = opts.speed * (keys.ShiftLeft || keys.ShiftRight ? 3 : 1);
- if (opts.auto) {
- pathZ -= v * dt;
- if (pathZ < PATH_END + 40) { pathZ = PATH_START - 60; }
- pos.x = pathX(pathZ); pos.z = pathZ;
- const ahead = 45;
- const dx = pathX(pathZ - ahead) - pos.x, dz = -ahead;
- yaw += angDiff(Math.atan2(-dx, -dz), yaw) * Math.min(1, dt * (locked ? 0.6 : 1.5));
- if (!locked) pitch += (0.15 - pitch) * Math.min(1, dt * 1.5);
- } else {
- let f = 0, s = 0;
- if (keys.KeyW || keys.ArrowUp) f += 1;
- if (keys.KeyS || keys.ArrowDown) f -= 1;
- if (keys.KeyD || keys.ArrowRight) s += 1;
- if (keys.KeyA || keys.ArrowLeft) s -= 1;
- const len = Math.hypot(f, s) || 1;
- f /= len; s /= len;
- const sy = Math.sin(yaw), cy = Math.cos(yaw);
- pos.x += (-sy * f + cy * s) * v * dt;
- pos.z += (-cy * f - sy * s) * v * dt;
- pos.x = Math.max(-4000, Math.min(4000, pos.x));
- pos.z = Math.max(-4400, Math.min(3400, pos.z));
- }
- pos.y = H(pos.x, pos.z) + 1.7;
- camera.position.copy(pos);
- camera.rotation.set(pitch, yaw, 0);
- camera.updateMatrixWorld();
- sky.position.copy(pos);
- time += dt;
- if (anim.windmill) anim.windmill.rotation.z -= dt * 0.9;
- if (anim.clouds) anim.clouds.rotation.y += dt * 0.004;
- if (anim.mist) anim.mist.rotation.y += dt * 0.012;
- for (const b of anim.birds) {
- const u = b.userData;
- const a = u.phase + time * u.speed;
- b.position.set(u.center.x + Math.cos(a) * u.radius, u.center.y + Math.sin(time * 0.5 + u.phase) * 6, u.center.z + Math.sin(a) * u.radius);
- b.rotation.y = -a + (u.speed > 0 ? 0 : Math.PI);
- const flap = Math.sin(time * u.flap + u.phase) * 0.6;
- u.l.rotation.z = flap; u.r.rotation.z = -flap;
- }
- for (const b of anim.birds) b.updateMatrixWorld(true);
- // Backdrop bookkeeping
- snap.age += dt;
- if (snap.dissolveT < 1) {
- snap.dissolveT = cfg.dissolve > 0 ? Math.min(1, snap.dissolveT + dt / cfg.dissolve) : 1;
- skyU.mixT.value = snap.dissolveT;
- }
- if (cfg.backdrop) {
- const moved = pos.distanceTo(snap.pos);
- const due = snap.pending ||
- (cfg.rDist === 0) || moved >= cfg.rDist ||
- (cfg.rTime > 0 && snap.age >= cfg.rTime);
- if (due) repaint(false);
- }
- }
- function frame() {
- const dt = Math.min(clock.getDelta(), 0.1);
- step(dt);
- clipU.mode.value = cfg.backdrop ? 0 : -1;
- clipU.center.value.copy(camera.position);
- renderer.setRenderTarget(rt);
- renderer.render(scene, camera);
- renderer.setRenderTarget(null);
- renderer.render(postScene, postCam);
- frames++; fpsT += dt; hudT += dt;
- if (fpsT >= 0.5) { $("fps").textContent = `${Math.round(frames / fpsT)} fps`; frames = 0; fpsT = 0; }
- if (hudT >= 0.2) {
- hudT = 0;
- $("age").textContent = cfg.backdrop
- ? `${Math.round(pos.distanceTo(snap.pos))} m, ${snap.age.toFixed(1)} s ago`
- : "off";
- }
- requestAnimationFrame(frame);
- }
- setTimeout(() => {
- try {
- buildWorld();
- } catch (err) {
- console.error(err);
- $("loading").innerHTML = '<div class="error">The world failed to build. Reload the page to try again.</div>';
- return;
- }
- resize();
- syncUI();
- scene.fog.density = 0.0000037 * opts.haze;
- pos.y = H(pos.x, pos.z) + 1.7;
- camera.position.copy(pos);
- camera.rotation.set(pitch, yaw, 0);
- camera.updateMatrixWorld();
- ready = true;
- repaint(true); repaint(true);
- $("loading").classList.add("hidden");
- clock.getDelta();
- requestAnimationFrame(frame);
- }, 40);
- })();
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment