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>Far-field flattening 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: 300px; 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 10px; font-variant-numeric: tabular-nums; }
- .readout dt { color: var(--ink-soft); }
- .readout dd { margin: 0; text-align: right; font-weight: 600; }
- .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; }
- .row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 7px 0; }
- .row label { flex: 1; }
- .row output { min-width: 52px; 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: 112px; }
- .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; }
- .toggle.main { font-weight: 600; }
- 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; }
- </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>Far-field flattening test</h1>
- <p class="lede">Past the boundary, every object is drawn as if it were at the boundary, at its real size. Far things stop approaching until you actually reach them.</p>
- <dl class="readout">
- <dt>Frame rate</dt><dd id="fps">–</dd>
- <dt>Internal resolution</dt><dd id="res">–</dd>
- <dt>Castle, real distance</dt><dd id="cReal">–</dd>
- <dt>Castle, drawn at</dt><dd id="cDrawn">–</dd>
- </dl>
- <label class="toggle main"><input type="checkbox" id="optWarp" checked> Flatten the far field <kbd>V</kbd></label>
- <label class="toggle"><input type="checkbox" id="optTint"> Show the flattened zone <kbd>F</kbd></label>
- <div class="legend"><span class="swatch"></span>Stronger blue means flatter</div>
- <div class="row">
- <label for="dStart">Boundary starts at</label>
- <input type="range" id="dStart" min="40" max="800" step="10" value="120">
- <output id="dStartOut">120 m</output>
- </div>
- <div class="row">
- <label for="dWidth">Transition width</label>
- <input type="range" id="dWidth" min="10" max="800" step="10" value="150">
- <output id="dWidthOut">150 m</output>
- </div>
- <div class="row">
- <label for="drift">Far drift</label>
- <input type="range" id="drift" min="0" max="50" step="1" value="3">
- <output id="driftOut">3%</output>
- </div>
- <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="6">
- <output id="speedOut">6 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 = -1270;
- 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) / (110 * 110));
- if (e > 0.001) {
- h += 150 * Math.min(1, 1.3 * e);
- h += 45 * (fbm(x * 0.03 + 3, z * 0.03, 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;
- }
- // Warped region: everything inside is split into chunks that move as rigid units.
- // Outside it (the distant mountains) is left as ordinary geometry, like a skybox.
- const TILE = 60;
- const RX0 = -1980, RX1 = 1980, RZ0 = -2280, RZ1 = 900;
- const NTX = Math.round((RX1 - RX0) / TILE), NTZ = Math.round((RZ1 - RZ0) / TILE);
- const CASTLE_CHUNK_R = 160;
- const castleCenter = new THREE.Vector3(CASTLE.x, H(CASTLE.x, CASTLE.z), CASTLE.z);
- const tileCenters = new Float32Array(NTX * NTZ * 3);
- for (let j = 0; j < NTZ; j++) for (let i = 0; i < NTX; i++) {
- const cx = RX0 + (i + 0.5) * TILE, cz = RZ0 + (j + 0.5) * TILE;
- const o = (j * NTX + i) * 3;
- if (Math.hypot(cx - CASTLE.x, cz - CASTLE.z) < CASTLE_CHUNK_R) {
- tileCenters[o] = castleCenter.x; tileCenters[o + 1] = castleCenter.y; tileCenters[o + 2] = castleCenter.z;
- } else {
- tileCenters[o] = cx; tileCenters[o + 1] = H(cx, cz); tileCenters[o + 2] = cz;
- }
- }
- const inRegion = (x, z) => x >= RX0 && x < RX1 && z >= RZ0 && z < RZ1;
- function centerOf(x, z, out) {
- const i = Math.min(NTX - 1, Math.max(0, Math.floor((x - RX0) / TILE)));
- const j = Math.min(NTZ - 1, Math.max(0, Math.floor((z - RZ0) / TILE)));
- const o = (j * NTX + i) * 3;
- out[0] = tileCenters[o]; out[1] = tileCenters[o + 1]; out[2] = tileCenters[o + 2];
- return out;
- }
- // ---------------------------------------------------------------------------
- // The warp (same formula in JS and GLSL)
- // f(d) = d up to D, then eases over width W to a slope of c (the far drift).
- // Each chunk is slid along its direction from the camera to distance f(d), without shrinking.
- // ---------------------------------------------------------------------------
- const warp = { on: 1, D: 120, W: 150, c: 0.03 };
- function warpF(d) {
- const D = warp.D, W = Math.max(warp.W, 1), c = warp.c;
- let S = 0;
- if (d > D + W) S = 0.5 * W + (d - D - W);
- else if (d > D) { const t = (d - D) / W; S = W * (t * t * t - 0.5 * t * t * t * t); }
- return d - (1 - c) * S;
- }
- const uWarp = { value: new THREE.Vector4(120, 150, 0.03, 1) };
- const uTint = { value: 0 };
- const WARP_DECL = `
- uniform vec4 uWarp;
- #ifdef WARP_ATTR
- attribute vec3 aCenter;
- #else
- uniform vec3 uCenter;
- #endif
- varying float vWarpAmt;
- float warpF(float d) {
- float D = uWarp.x; float W = max(uWarp.y, 1.0); float c = uWarp.z;
- float S = 0.0;
- if (d > D + W) S = 0.5 * W + (d - D - W);
- else if (d > D) { float t = (d - D) / W; S = W * (t * t * t - 0.5 * t * t * t * t); }
- return d - (1.0 - c) * S;
- }
- `;
- const WARP_PROJECT = `
- vec4 wPos = vec4(transformed, 1.0);
- #ifdef USE_INSTANCING
- wPos = instanceMatrix * wPos;
- #endif
- wPos = modelMatrix * wPos;
- #ifdef WARP_ATTR
- vec3 wCenter = aCenter;
- #else
- vec3 wCenter = uCenter;
- #endif
- vec3 toC = wCenter - cameraPosition;
- float cDist = max(length(toC), 0.001);
- vec3 wShift = toC * (warpF(cDist) / cDist - 1.0) * uWarp.w;
- vWarpAmt = uWarp.w * (1.0 - uWarp.z) * smoothstep(uWarp.x, uWarp.x + max(uWarp.y, 1.0), cDist);
- vec4 mvPosition = viewMatrix * vec4(wPos.xyz + wShift, 1.0);
- gl_Position = projectionMatrix * mvPosition;
- `;
- // Fog uses the real distance so far things keep their haze
- const WARP_FOG = `
- #ifdef USE_FOG
- fogDepth = -(viewMatrix * wPos).z;
- #endif
- `;
- function warpify(mat, center) {
- const uCenter = center ? { value: center.clone() } : null;
- mat.defines = Object.assign({}, mat.defines, center ? { WARP_UNIFORM: "" } : { WARP_ATTR: "" });
- mat.onBeforeCompile = (shader) => {
- shader.uniforms.uWarp = uWarp;
- shader.uniforms.uTint = uTint;
- if (uCenter) shader.uniforms.uCenter = uCenter;
- shader.vertexShader = shader.vertexShader
- .replace("#include <common>", "#include <common>\n" + WARP_DECL)
- .replace("#include <project_vertex>", WARP_PROJECT)
- .replace("#include <fog_vertex>", WARP_FOG);
- shader.fragmentShader = shader.fragmentShader
- .replace("#include <common>", "#include <common>\nvarying float vWarpAmt;\nuniform float uTint;")
- .replace("#include <fog_fragment>", "#include <fog_fragment>\ngl_FragColor.rgb = mix(gl_FragColor.rgb, vec3(0.25, 0.72, 1.0), uTint * vWarpAmt * 0.6);");
- };
- return mat;
- }
- // ---------------------------------------------------------------------------
- // 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);
- const sky = new THREE.Mesh(
- new THREE.SphereGeometry(11000, 32, 16),
- new THREE.ShaderMaterial({
- side: THREE.BackSide, depthWrite: false, depthTest: false, fog: false,
- uniforms: { horizon: { value: HORIZON }, zenith: { value: ZENITH }, sunDir: { value: SUN_DIR } },
- vertexShader: `
- varying vec3 vDir;
- void main() {
- vDir = normalize(position);
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
- }`,
- fragmentShader: `
- uniform vec3 horizon; uniform vec3 zenith; uniform vec3 sunDir;
- varying vec3 vDir;
- 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);
- gl_FragColor = vec4(c, 1.0);
- }`
- })
- );
- sky.renderOrder = -1;
- sky.frustumCulled = false;
- scene.add(sky);
- const flat = (color, extra = {}) => new THREE.MeshPhongMaterial(Object.assign({
- color, flatShading: true, shininess: 0, specular: 0x000000
- }, extra));
- 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 at = (x, y, z, ry = 0, sx = 1, sy = 1, sz = 1) =>
- new THREE.Matrix4().compose(new THREE.Vector3(x, y, z), new THREE.Quaternion().setFromEuler(new THREE.Euler(0, ry, 0)), new THREE.Vector3(sx, sy, sz));
- const anim = { windmill: null, birds: [], clouds: null, mist: null };
- const noCull = (o) => { o.frustumCulled = false; return o; };
- // Terrain colors
- 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");
- function terrainColor(x, z, h, ny, out) {
- const n = fbm(x * 0.02, z * 0.02, 2);
- out.copy(cGrassA).lerp(cGrassB, n);
- const side = Math.abs(x - pathX(z));
- if (z < -150 && z > -1150) out.lerp(cMeadow, 0.55 * (1 - smooth(60, 260, side)) * n);
- const forestN = fbm(x * 0.005 + 11, z * 0.005 - 4, 3);
- out.lerp(cForest, 0.7 * smooth(0.47, 0.6, forestN) * smooth(40, 160, side));
- if (z > -90 && Math.abs(x) < 700) out.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));
- out.lerp(n > 0.5 ? cRock : cRockDark, rockAmt);
- out.lerp(cSnow, smooth(1250, 1400, h + n * 160) * smooth(0.25, 0.45, ny));
- return out;
- }
- function normalY(x, z) {
- const e = 4;
- const dx = H(x + e, z) - H(x - e, z), dz = H(x, z + e) - H(x, z - e);
- return (2 * e) / Math.hypot(dx, 2 * e, dz);
- }
- function buildWorld() {
- const tmpC = new THREE.Color();
- const cc = [0, 0, 0];
- // ---------------- Warped terrain: one mesh, each tile carries its chunk center ----------------
- {
- const SUB = 6, step = TILE / SUB;
- const vPer = (SUB + 1) * (SUB + 1);
- const nT = NTX * NTZ;
- const pos = new Float32Array(nT * vPer * 3), col = new Float32Array(nT * vPer * 3), cen = new Float32Array(nT * vPer * 3);
- const idx = new Uint32Array(nT * SUB * SUB * 6);
- let v = 0, k = 0;
- for (let j = 0; j < NTZ; j++) for (let i = 0; i < NTX; i++) {
- const x0 = RX0 + i * TILE, z0 = RZ0 + j * TILE;
- const o = (j * NTX + i) * 3;
- const base = v;
- for (let b = 0; b <= SUB; b++) for (let a = 0; a <= SUB; a++) {
- const x = x0 + a * step, z = z0 + b * step, h = H(x, z);
- pos[v * 3] = x; pos[v * 3 + 1] = h; pos[v * 3 + 2] = z;
- terrainColor(x, z, h, normalY(x, z), tmpC);
- col[v * 3] = tmpC.r; col[v * 3 + 1] = tmpC.g; col[v * 3 + 2] = tmpC.b;
- cen[v * 3] = tileCenters[o]; cen[v * 3 + 1] = tileCenters[o + 1]; cen[v * 3 + 2] = tileCenters[o + 2];
- v++;
- }
- for (let b = 0; b < SUB; b++) for (let a = 0; a < SUB; a++) {
- const p = base + b * (SUB + 1) + a, q = p + 1, r = p + SUB + 1, s = r + 1;
- idx[k++] = p; idx[k++] = r; idx[k++] = q;
- idx[k++] = q; idx[k++] = r; idx[k++] = s;
- }
- }
- const g = new THREE.BufferGeometry();
- g.setAttribute("position", new THREE.BufferAttribute(pos, 3));
- g.setAttribute("color", new THREE.BufferAttribute(col, 3));
- g.setAttribute("aCenter", new THREE.BufferAttribute(cen, 3));
- g.setIndex(new THREE.BufferAttribute(idx, 1));
- g.computeVertexNormals();
- scene.add(noCull(new THREE.Mesh(g, warpify(flat(0xffffff, { vertexColors: true })))));
- }
- // ---------------- Outer terrain (mountains), not warped ----------------
- {
- const N = 300, S = 4800, A = 0.22, CZ = -600;
- 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 = [];
- 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;
- const x0 = verts[a * 3], z0 = verts[a * 3 + 2], x1 = verts[d * 3], z1 = verts[d * 3 + 2];
- // skip cells fully inside the warped region
- if (x0 >= RX0 + 5 && x1 <= RX1 - 5 && z0 >= RZ0 + 5 && z1 <= RZ1 - 5) continue;
- idx.push(a, c, b, b, c, d);
- }
- const g = new THREE.BufferGeometry();
- g.setAttribute("position", new THREE.BufferAttribute(verts, 3));
- g.setIndex(idx);
- g.computeVertexNormals();
- const nrm = g.attributes.normal.array, cols = new Float32Array(verts.length);
- for (let v = 0; v < verts.length; v += 3) {
- terrainColor(verts[v], verts[v + 2], verts[v + 1], nrm[v + 1], tmpC);
- cols[v] = tmpC.r; cols[v + 1] = tmpC.g; cols[v + 2] = tmpC.b;
- }
- g.setAttribute("color", new THREE.BufferAttribute(cols, 3));
- scene.add(new THREE.Mesh(g, flat(0xffffff, { vertexColors: true })));
- }
- // ---------------- Path (each piece uses the chunk center of the tile it sits on) ----------------
- {
- const pos = [], col = [], cen = [];
- const stone = [new THREE.Color("#d8c9a4"), new THREE.Color("#bfae8a"), new THREE.Color("#e6dcc0"), new THREE.Color("#a99a7c")];
- let prev = null;
- for (let z = PATH_START; z >= PATH_END; z -= 2) {
- const x = pathX(z);
- const tx = pathX(z - 1) - x, tz = -1, len = Math.hypot(tx, tz);
- const nx = -tz / len, nz = tx / len;
- 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) {
- centerOf(x, z + 1, cc);
- const c1 = stone[hash2(z | 0, 1) * 4 | 0], c2 = stone[hash2(z | 0, 2) * 4 | 0];
- pos.push(...prev.L, ...prev.R, ...L, ...prev.R, ...R, ...L);
- for (let i = 0; i < 3; i++) col.push(c1.r, c1.g, c1.b);
- for (let i = 0; i < 3; i++) col.push(c2.r, c2.g, c2.b);
- for (let i = 0; i < 6; i++) cen.push(cc[0], cc[1], cc[2]);
- }
- 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.setAttribute("aCenter", new THREE.Float32BufferAttribute(cen, 3));
- g.computeVertexNormals();
- const mat = warpify(flat(0xffffff, { vertexColors: true, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2, side: THREE.DoubleSide }));
- scene.add(noCull(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 MAX_CON = 22000, MAX_ROUND = 12000;
- const conCenters = new Float32Array(MAX_CON * 3), roundCenters = new Float32Array(MAX_ROUND * 3);
- coniferGeo.setAttribute("aCenter", new THREE.InstancedBufferAttribute(conCenters, 3));
- roundGeo.setAttribute("aCenter", new THREE.InstancedBufferAttribute(roundCenters, 3));
- const conifers = new THREE.InstancedMesh(coniferGeo, warpify(flat(0xffffff, { vertexColors: true })), MAX_CON);
- const rounds = new THREE.InstancedMesh(roundGeo, warpify(flat(0xffffff, { vertexColors: true })), 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, centers, 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);
- centerOf(x, z, cc);
- centers[idx * 3] = cc[0]; centers[idx * 3 + 1] = cc[1]; centers[idx * 3 + 2] = cc[2];
- }
- 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) < 120) 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 = RZ1 - 5; gz > RZ0 + 5 && (nCon < MAX_CON || nRound < MAX_ROUND); gz -= STEP) {
- for (let gx = RX0 + 5; gx < RX1 - 5; gx += STEP) {
- const x = gx + (rand() - 0.5) * STEP * 0.95, z = gz + (rand() - 0.5) * STEP * 0.95;
- if (!inRegion(x, z) || 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, roundCenters, nRound++, x, h - 0.4, z, 0.8 + rand() * 0.7, 0.08);
- } else if (nCon < MAX_CON) {
- place(conifers, conCenters, nCon++, x, h - 0.4, z, 0.8 + rand() * 1.1, 0.06);
- }
- }
- }
- 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, zz = z + (rand() - 0.5) * 1.5;
- if (nRound < MAX_ROUND) place(rounds, roundCenters, nRound++, x, H(x, zz) - 1.1 - rand() * 0.4, zz, 0.22 + rand() * 0.28, 0.3);
- }
- }
- conifers.count = nCon; rounds.count = nRound;
- for (const m of [conifers, rounds]) {
- m.instanceMatrix.needsUpdate = true;
- if (m.instanceColor) m.instanceColor.needsUpdate = true;
- m.geometry.attributes.aCenter.needsUpdate = true;
- noCull(m); scene.add(m);
- }
- // ---------------- 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 houseCenters = new Float32Array(90 * 3);
- houseGeo.setAttribute("aCenter", new THREE.InstancedBufferAttribute(houseCenters, 3));
- const houses = new THREE.InstancedMesh(houseGeo, warpify(flat(0xffffff, { vertexColors: true })), 90);
- const placed = [];
- let tries = 0, nHouse = 0;
- while (nHouse < 90 && tries++ < 5000) {
- const z = -250 - Math.pow(rand(), 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);
- centerOf(x, z, cc);
- houseCenters.set(cc, nHouse * 3);
- nHouse++;
- }
- houses.count = nHouse;
- scene.add(noCull(houses));
- // Windmill
- {
- const wz = -430, wx = pathX(wz) - 58, wh = H(wx, wz);
- const c = new THREE.Vector3(...centerOf(wx, wz, cc));
- const g = new THREE.Group();
- g.position.set(wx, wh - 1, wz);
- const tower = new THREE.Mesh(new THREE.CylinderGeometry(2.6, 4, 16, 8), warpify(flat(0xe0d4b8), c));
- tower.position.y = 8; g.add(noCull(tower));
- const cap = new THREE.Mesh(new THREE.ConeGeometry(3.4, 4, 8), warpify(flat(0x7a3a2a), c));
- cap.position.y = 18; g.add(noCull(cap));
- const hub = new THREE.Group(); hub.position.set(0, 15.5, 3.4); g.add(hub);
- const bladeMat = warpify(flat(0xcfc2a8), c), sparMat = warpify(flat(0x5a4636), c);
- 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), bladeMat);
- blade.position.y = 6.2; arm.add(noCull(blade));
- const spar = new THREE.Mesh(new THREE.BoxGeometry(0.35, 12, 0.35), sparMat);
- spar.position.y = 6; arm.add(noCull(spar));
- hub.add(arm);
- }
- g.rotation.y = 0.35;
- scene.add(g);
- anim.windmill = hub;
- }
- // ---------------- Castle (one chunk together with its crag) ----------------
- {
- let minTop = castleCenter.y;
- for (let a = 0; a < Math.PI * 2; a += Math.PI / 8) minTop = Math.min(minTop, H(CASTLE.x + Math.cos(a) * 26, CASTLE.z + Math.sin(a) * 38));
- const S = 0.5;
- const wall = "#3e3c58", wallLight = "#4a4766", roof = "#27263e";
- const parts = [];
- const base = minTop - 4;
- 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, 1, 10), color: wallLight, matrix: at(0, base + 40 * S, 0) });
- 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, 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) });
- }
- 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, 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) });
- }
- 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 castle = new THREE.Mesh(mergeParts(parts), warpify(flat(0xffffff, { vertexColors: true }), castleCenter));
- castle.position.set(CASTLE.x, 0, CASTLE.z);
- scene.add(noCull(castle));
- const winGeo = new THREE.BoxGeometry(2.4 * S, 4.2 * S, 0.4);
- const wins = new THREE.InstancedMesh(winGeo, warpify(new THREE.MeshBasicMaterial({ color: 0xffcf6a }), castleCenter), 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.05 * 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;
- scene.add(noCull(wins));
- // Birds circling the towers and over the village
- 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, mat) {
- const b = new THREE.Group();
- const l = noCull(new THREE.Mesh(wingGeo, mat)), r = noCull(new THREE.Mesh(wingGeo, mat));
- l.scale.setScalar(size); r.scale.set(-size, size, size);
- b.add(l, r); b.userData = { l, r };
- return b;
- }
- const castleBirdMat = warpify(new THREE.MeshBasicMaterial({ color: 0x1c1a2c, side: THREE.DoubleSide }), castleCenter);
- for (let i = 0; i < 14; i++) {
- const b = makeBird(1.6 + rand() * 0.8, castleBirdMat);
- b.userData.center = new THREE.Vector3(CASTLE.x, base + (130 + rand() * 90) * S, CASTLE.z);
- b.userData.radius = 25 + rand() * 45; b.userData.speed = (0.25 + rand() * 0.2) * (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);
- }
- const vz = -520, vx = pathX(vz) + 20;
- const villageBirdMat = warpify(new THREE.MeshBasicMaterial({ color: 0x1c1a2c, side: THREE.DoubleSide }), new THREE.Vector3(...centerOf(vx, vz, cc)));
- for (let i = 0; i < 8; i++) {
- const b = makeBird(1.8 + rand() * 0.8, villageBirdMat);
- b.userData.center = new THREE.Vector3(vx, H(vx, vz) + 45 + rand() * 25, vz);
- 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 around the crag
- const mist = new THREE.Group();
- mist.position.set(CASTLE.x, 0, CASTLE.z);
- const puffGeo = new THREE.IcosahedronGeometry(1, 1);
- const mistMat = warpify(flat(0xcfc7e2, { emissive: 0x2a2440 }), castleCenter);
- for (let i = 0; i < 24; i++) {
- const a = i / 24 * Math.PI * 2 + rand() * 0.2, r = 75 + rand() * 30;
- const p = noCull(new THREE.Mesh(puffGeo, mistMat));
- p.position.set(Math.cos(a) * r, castleCenter.y - 70 + rand() * 20, Math.sin(a) * r);
- p.scale.set(20 + rand() * 14, 6 + rand() * 5, 15 + rand() * 10);
- p.rotation.y = rand() * 3;
- mist.add(p);
- }
- scene.add(mist);
- anim.mist = mist;
- }
- // ---------------- Clouds (sky layer, not warped) ----------------
- const clouds = new THREE.Group();
- clouds.position.set(RING.x, 0, RING.z);
- const cloudGeo = new THREE.IcosahedronGeometry(1, 1);
- const cloudMat = flat(0xbfb6d6, { emissive: 0x26203c });
- for (let i = 0; i < 34; i++) {
- const a = rand() * Math.PI * 2, r = 2200 + rand() * 2200, 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();
- postScene.add(noCull(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), post)));
- const postCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
- const PW_OPTIONS = ["160", "240", "320", "426", "640", "full"];
- let pixelSetting = "320";
- 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);
- $("res").textContent = `${lowW} × ${lowH}`;
- }
- window.addEventListener("resize", resize);
- // ---------------------------------------------------------------------------
- // Controls
- // ---------------------------------------------------------------------------
- const keys = Object.create(null);
- let yaw = 0, pitch = 0.08, 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: 6, 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 applyWarp() {
- uWarp.value.set(warp.D, warp.W, warp.c, warp.on);
- uTint.value = opts.tint ? 1 : 0;
- }
- function syncUI() {
- $("optAuto").checked = opts.auto;
- $("optTint").checked = opts.tint;
- $("optWarp").checked = !!warp.on;
- $("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;
- applyWarp();
- }
- function setAuto(on) {
- opts.auto = on;
- if (on) pathZ = Math.min(PATH_START - 20, Math.max(PATH_END + 50, pos.z));
- syncUI();
- }
- 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") && !locked && !MOVE_KEYS.includes(e.code) && !e.code.startsWith("Shift")) return;
- keys[e.code] = true;
- if (e.repeat) return;
- if (e.code === "KeyF") { opts.tint = !opts.tint; syncUI(); }
- else if (e.code === "KeyV") { warp.on = warp.on ? 0 : 1; syncUI(); }
- 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), 0, 60); yaw = 0; pitch = 0.08; }
- 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(); });
- $("optWarp").addEventListener("change", (e) => { warp.on = e.target.checked ? 1 : 0; syncUI(); });
- $("optDither").addEventListener("change", (e) => { opts.dither = e.target.checked; syncUI(); });
- $("optOutline").addEventListener("change", (e) => { opts.outline = e.target.checked; syncUI(); });
- $("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;
- });
- $("dStart").addEventListener("input", (e) => { warp.D = +e.target.value; $("dStartOut").textContent = `${warp.D} m`; applyWarp(); });
- $("dWidth").addEventListener("input", (e) => { warp.W = +e.target.value; $("dWidthOut").textContent = `${warp.W} m`; applyWarp(); });
- $("drift").addEventListener("input", (e) => { warp.c = +e.target.value / 100; $("driftOut").textContent = `${e.target.value}%`; applyWarp(); });
- // ---------------------------------------------------------------------------
- // 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; };
- const fmtDist = (d) => d >= 1000 ? `${(d / 1000).toFixed(2)} km` : `${Math.round(d)} m`;
- 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;
- const target = Math.atan2(-dx, -dz);
- yaw += angDiff(target, yaw) * Math.min(1, dt * (locked ? 0.6 : 1.5));
- if (!locked) pitch += (0.08 - 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(RX0 + 20, Math.min(RX1 - 20, pos.x));
- pos.z = Math.max(RZ0 + 20, Math.min(RZ1 - 20, 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.03;
- 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) * 3, 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;
- }
- }
- function frame() {
- const dt = Math.min(clock.getDelta(), 0.1);
- step(dt);
- 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;
- const d = pos.distanceTo(castleCenter);
- $("cReal").textContent = fmtDist(d);
- $("cDrawn").textContent = warp.on ? fmtDist(warpF(d)) : "same";
- }
- 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;
- $("loading").classList.add("hidden");
- clock.getDelta();
- requestAnimationFrame(frame);
- }, 40);
- })();
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment