yaosio

Neat far field effect where small things far away are rendered very large.

Sep 24th, 2026
198
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 50.90 KB | Source Code | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
  6. <title>Far-field flattening test</title>
  7. <link rel="preconnect" href="https://fonts.googleapis.com">
  8. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  9. <link href="https://fonts.googleapis.com/css2?family=Pixelify+Sans:wght@500;700&family=Work+Sans:wght@400;500;600&display=swap" rel="stylesheet">
  10. <style>
  11.   :root {
  12.     --dusk: rgba(28, 24, 48, 0.86);
  13.     --dusk-edge: rgba(190, 176, 230, 0.22);
  14.     --ink: #ece7f8;
  15.     --ink-soft: #b3aad0;
  16.     --lantern: #ffcf6a;
  17.     --frost: #5cc8ff;
  18.     --track: rgba(255, 255, 255, 0.16);
  19.     box-sizing: border-box;
  20.     padding-top: env(safe-area-inset-top, 0px);
  21.     padding-bottom: env(safe-area-inset-bottom, 0px);
  22.     color-scheme: dark;
  23.   }
  24.   @media (prefers-color-scheme: light) {
  25.     :root:not([data-theme="dark"]) {
  26.       --dusk: rgba(246, 243, 255, 0.9);
  27.       --dusk-edge: rgba(60, 50, 100, 0.2);
  28.       --ink: #221d38;
  29.       --ink-soft: #5b5378;
  30.       --lantern: #b87400;
  31.       --frost: #0a7fc0;
  32.       --track: rgba(30, 20, 60, 0.18);
  33.       color-scheme: light;
  34.     }
  35.   }
  36.   :root[data-theme="dark"] {
  37.     --dusk: rgba(28, 24, 48, 0.86);
  38.     --dusk-edge: rgba(190, 176, 230, 0.22);
  39.     --ink: #ece7f8;
  40.     --ink-soft: #b3aad0;
  41.     --lantern: #ffcf6a;
  42.     --frost: #5cc8ff;
  43.     --track: rgba(255, 255, 255, 0.16);
  44.   }
  45.   *, *::before, *::after { box-sizing: inherit; }
  46.   html { scroll-padding-top: env(safe-area-inset-top, 0px); height: 100%; }
  47.   body {
  48.     height: 100%; margin: 0; overflow: hidden; background: #15122a;
  49.     font-family: "Work Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  50.     color: var(--ink);
  51.   }
  52.   canvas#view { position: fixed; inset: 0; width: 100%; height: 100%; display: block; cursor: crosshair; }
  53.  
  54.   .panel {
  55.     position: fixed; left: 14px; top: calc(14px + env(safe-area-inset-top, 0px));
  56.     width: 300px; max-width: calc(100vw - 28px); max-height: calc(100% - 28px); overflow-y: auto;
  57.     background: var(--dusk); border: 1px solid var(--dusk-edge); border-radius: 10px;
  58.     padding: 16px 16px 14px; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  59.     font-size: 13px; line-height: 1.45;
  60.   }
  61.   .panel.hidden { display: none; }
  62.   .panel h1 {
  63.     font-family: "Pixelify Sans", "Work Sans", system-ui, sans-serif;
  64.     font-weight: 700; font-size: 22px; line-height: 1.1; margin: 0 0 4px; letter-spacing: 0.01em;
  65.   }
  66.   .panel .lede { color: var(--ink-soft); margin: 0 0 12px; }
  67.   .readout { display: grid; grid-template-columns: 1fr auto; gap: 2px 12px; margin: 0 0 10px; font-variant-numeric: tabular-nums; }
  68.   .readout dt { color: var(--ink-soft); }
  69.   .readout dd { margin: 0; text-align: right; font-weight: 600; }
  70.   .group { margin: 10px 0 0; padding-top: 10px; border-top: 1px solid var(--track); }
  71.   .group h2 { font-size: 13px; font-weight: 600; margin: 0 0 4px; }
  72.   .row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 7px 0; }
  73.   .row label { flex: 1; }
  74.   .row output { min-width: 52px; text-align: right; font-variant-numeric: tabular-nums; color: var(--ink-soft); }
  75.   select, input[type="range"] { accent-color: var(--lantern); }
  76.   select {
  77.     font: inherit; color: var(--ink); background: transparent; border: 1px solid var(--track);
  78.     border-radius: 6px; padding: 3px 6px;
  79.   }
  80.   select option { color: #221d38; }
  81.   input[type="range"] { width: 112px; }
  82.   .toggle { display: flex; align-items: center; gap: 8px; margin: 6px 0; cursor: pointer; }
  83.   .toggle input { accent-color: var(--lantern); width: 15px; height: 15px; margin: 0; }
  84.   .toggle kbd { margin-left: auto; }
  85.   .toggle.main { font-weight: 600; }
  86.   kbd {
  87.     font-family: inherit; font-size: 11px; color: var(--ink-soft);
  88.     border: 1px solid var(--track); border-radius: 4px; padding: 0 5px;
  89.   }
  90.   .legend { display: flex; align-items: center; gap: 8px; margin: 2px 0 0 23px; color: var(--ink-soft); font-size: 12px; }
  91.   .swatch { width: 12px; height: 12px; border-radius: 2px; background: var(--frost); flex: none; }
  92.   .keys { margin: 12px 0 0; padding-top: 10px; border-top: 1px solid var(--track); color: var(--ink-soft); font-size: 12px; }
  93.   .keys p { margin: 3px 0; }
  94.   :focus-visible { outline: 2px solid var(--lantern); outline-offset: 2px; }
  95.  
  96.   .hint {
  97.     position: fixed; left: 50%; bottom: calc(22px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%);
  98.     background: var(--dusk); border: 1px solid var(--dusk-edge); border-radius: 8px;
  99.     padding: 8px 14px; font-size: 13px; pointer-events: none; white-space: nowrap;
  100.   }
  101.   .hint.hidden { display: none; }
  102.   .loading {
  103.     position: fixed; inset: 0; display: grid; place-items: center; background: #15122a; color: #ece7f8;
  104.     font-family: "Pixelify Sans", "Work Sans", system-ui, sans-serif; font-size: 22px;
  105.   }
  106.   .loading.hidden { display: none; }
  107.   .error { font-family: "Work Sans", system-ui, sans-serif; font-size: 15px; max-width: 420px; text-align: center; line-height: 1.5; }
  108. </style>
  109. </head>
  110. <body>
  111. <canvas id="view"></canvas>
  112.  
  113. <div class="loading" id="loading">Building the valley…</div>
  114.  
  115. <aside class="panel" id="panel" aria-label="Test controls">
  116.   <h1>Far-field flattening test</h1>
  117.   <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>
  118.  
  119.   <dl class="readout">
  120.     <dt>Frame rate</dt><dd id="fps">–</dd>
  121.     <dt>Internal resolution</dt><dd id="res">–</dd>
  122.     <dt>Castle, real distance</dt><dd id="cReal">–</dd>
  123.     <dt>Castle, drawn at</dt><dd id="cDrawn">–</dd>
  124.   </dl>
  125.  
  126.   <label class="toggle main"><input type="checkbox" id="optWarp" checked> Flatten the far field <kbd>V</kbd></label>
  127.   <label class="toggle"><input type="checkbox" id="optTint"> Show the flattened zone <kbd>F</kbd></label>
  128.   <div class="legend"><span class="swatch"></span>Stronger blue means flatter</div>
  129.  
  130.   <div class="row">
  131.     <label for="dStart">Boundary starts at</label>
  132.     <input type="range" id="dStart" min="40" max="800" step="10" value="120">
  133.     <output id="dStartOut">120 m</output>
  134.   </div>
  135.   <div class="row">
  136.     <label for="dWidth">Transition width</label>
  137.     <input type="range" id="dWidth" min="10" max="800" step="10" value="150">
  138.     <output id="dWidthOut">150 m</output>
  139.   </div>
  140.   <div class="row">
  141.     <label for="drift">Far drift</label>
  142.     <input type="range" id="drift" min="0" max="50" step="1" value="3">
  143.     <output id="driftOut">3%</output>
  144.   </div>
  145.  
  146.   <div class="group">
  147.     <h2>Look and movement</h2>
  148.     <div class="row">
  149.       <label for="pw">Pixel width</label>
  150.       <select id="pw">
  151.         <option value="160">160</option>
  152.         <option value="240">240</option>
  153.         <option value="320" selected>320</option>
  154.         <option value="426">426</option>
  155.         <option value="640">640</option>
  156.         <option value="full">Full resolution</option>
  157.       </select>
  158.     </div>
  159.     <div class="row">
  160.       <label for="speed">Walk speed</label>
  161.       <input type="range" id="speed" min="1" max="40" step="0.5" value="6">
  162.       <output id="speedOut">6 m/s</output>
  163.     </div>
  164.     <div class="row">
  165.       <label for="haze">Haze</label>
  166.       <input type="range" id="haze" min="0" max="100" step="1" value="30">
  167.       <output id="hazeOut">30</output>
  168.     </div>
  169.     <label class="toggle"><input type="checkbox" id="optAuto" checked> Walk the path automatically <kbd>T</kbd></label>
  170.     <label class="toggle"><input type="checkbox" id="optDither" checked> Limited palette with dither <kbd>G</kbd></label>
  171.     <label class="toggle"><input type="checkbox" id="optOutline" checked> Pixel outlines <kbd>O</kbd></label>
  172.   </div>
  173.  
  174.   <div class="keys">
  175.     <p>Click the scene to look around. Esc releases the mouse.</p>
  176.     <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>
  177.   </div>
  178. </aside>
  179.  
  180. <div class="hint" id="hint">Click to look around. W A S D to move.</div>
  181.  
  182. <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
  183. <script>
  184. (() => {
  185.   "use strict";
  186.   const $ = (id) => document.getElementById(id);
  187.  
  188.   if (typeof THREE === "undefined") {
  189.     $("loading").innerHTML = '<div class="error">The 3D library did not load. Check your connection and reload the page.</div>';
  190.     return;
  191.   }
  192.  
  193.   // ---------------------------------------------------------------------------
  194.   // Noise and helpers
  195.   // ---------------------------------------------------------------------------
  196.   function hash2(x, y) {
  197.     let h = (Math.imul(x | 0, 374761393) + Math.imul(y | 0, 668265263)) | 0;
  198.     h = Math.imul(h ^ (h >>> 13), 1274126177);
  199.     h ^= h >>> 16;
  200.     return (h >>> 0) / 4294967296;
  201.   }
  202.   function vnoise(x, y) {
  203.     const xi = Math.floor(x), yi = Math.floor(y);
  204.     const xf = x - xi, yf = y - yi;
  205.     const u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf);
  206.     const a = hash2(xi, yi), b = hash2(xi + 1, yi), c = hash2(xi, yi + 1), d = hash2(xi + 1, yi + 1);
  207.     return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
  208.   }
  209.   function fbm(x, y, oct) {
  210.     let s = 0, a = 0.5, f = 1, n = 0;
  211.     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; }
  212.    return s / n;
  213.  }
  214.  function ridged(x, y, oct) {
  215.    let s = 0, a = 0.5, f = 1, n = 0;
  216.    for (let i = 0; i < oct; i++) {
  217.      let r = 1 - Math.abs(vnoise(x * f + i * 31.7, y * f + i * 5.3) * 2 - 1);
  218.      r *= r; s += a * r; n += a; a *= 0.5; f *= 2.1;
  219.    }
  220.    return s / n;
  221.  }
  222.  const smooth = (a, b, x) => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); };
  223.   let seed = 1337;
  224.   const rand = () => {
  225.     seed = (seed + 0x6D2B79F5) | 0;
  226.     let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  227.     t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
  228.     return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  229.   };
  230.  
  231.   // ---------------------------------------------------------------------------
  232.   // World layout (meters; forward is -z)
  233.   // ---------------------------------------------------------------------------
  234.   const CASTLE = { x: 0, z: -1480 };
  235.   const RING = { x: 0, z: -900 };
  236.   const PATH_START = 120, PATH_END = -1270;
  237.   const pathX = (z) => 34 * Math.sin(z * 0.0062) + 18 * Math.sin(z * 0.017 + 1.3);
  238.  
  239.   function H(x, z) {
  240.     const side = Math.abs(x - pathX(z));
  241.     let h = (fbm(x * 0.004, z * 0.004, 4) - 0.5) * 34;
  242.     h += smooth(70, 650, side) * (100 + 110 * fbm(x * 0.0022 + 7.1, z * 0.0022 - 3.3, 3));
  243.     h += 26 * smooth(-260, 60, z);
  244.     const dx = x - CASTLE.x, dz = z - CASTLE.z;
  245.     const e = Math.exp(-(dx * dx + dz * dz) / (110 * 110));
  246.     if (e > 0.001) {
  247.       h += 150 * Math.min(1, 1.3 * e);
  248.       h += 45 * (fbm(x * 0.03 + 3, z * 0.03, 3) - 0.5) * e * (1 - smooth(0.7, 0.95, 1.3 * e));
  249.     }
  250.     const rr = Math.hypot(x - RING.x, z - RING.z);
  251.     const ring = smooth(1900, 3500, rr);
  252.     const back = smooth(2300, 3300, -z) * (1 - smooth(1300, 2600, Math.abs(x)));
  253.     const m = Math.max(ring, back);
  254.     if (m > 0) h += m * (620 + 900 * ridged(x * 0.0011, z * 0.0011, 4));
  255.     return h;
  256.   }
  257.  
  258.   // Warped region: everything inside is split into chunks that move as rigid units.
  259.   // Outside it (the distant mountains) is left as ordinary geometry, like a skybox.
  260.   const TILE = 60;
  261.   const RX0 = -1980, RX1 = 1980, RZ0 = -2280, RZ1 = 900;
  262.   const NTX = Math.round((RX1 - RX0) / TILE), NTZ = Math.round((RZ1 - RZ0) / TILE);
  263.   const CASTLE_CHUNK_R = 160;
  264.   const castleCenter = new THREE.Vector3(CASTLE.x, H(CASTLE.x, CASTLE.z), CASTLE.z);
  265.   const tileCenters = new Float32Array(NTX * NTZ * 3);
  266.   for (let j = 0; j < NTZ; j++) for (let i = 0; i < NTX; i++) {
  267.    const cx = RX0 + (i + 0.5) * TILE, cz = RZ0 + (j + 0.5) * TILE;
  268.    const o = (j * NTX + i) * 3;
  269.    if (Math.hypot(cx - CASTLE.x, cz - CASTLE.z) < CASTLE_CHUNK_R) {
  270.      tileCenters[o] = castleCenter.x; tileCenters[o + 1] = castleCenter.y; tileCenters[o + 2] = castleCenter.z;
  271.    } else {
  272.      tileCenters[o] = cx; tileCenters[o + 1] = H(cx, cz); tileCenters[o + 2] = cz;
  273.    }
  274.  }
  275.  const inRegion = (x, z) => x >= RX0 && x < RX1 && z >= RZ0 && z < RZ1;
  276.   function centerOf(x, z, out) {
  277.     const i = Math.min(NTX - 1, Math.max(0, Math.floor((x - RX0) / TILE)));
  278.     const j = Math.min(NTZ - 1, Math.max(0, Math.floor((z - RZ0) / TILE)));
  279.     const o = (j * NTX + i) * 3;
  280.     out[0] = tileCenters[o]; out[1] = tileCenters[o + 1]; out[2] = tileCenters[o + 2];
  281.     return out;
  282.   }
  283.  
  284.   // ---------------------------------------------------------------------------
  285.   // The warp (same formula in JS and GLSL)
  286.   // f(d) = d up to D, then eases over width W to a slope of c (the far drift).
  287.   // Each chunk is slid along its direction from the camera to distance f(d), without shrinking.
  288.   // ---------------------------------------------------------------------------
  289.   const warp = { on: 1, D: 120, W: 150, c: 0.03 };
  290.   function warpF(d) {
  291.     const D = warp.D, W = Math.max(warp.W, 1), c = warp.c;
  292.     let S = 0;
  293.     if (d > D + W) S = 0.5 * W + (d - D - W);
  294.     else if (d > D) { const t = (d - D) / W; S = W * (t * t * t - 0.5 * t * t * t * t); }
  295.     return d - (1 - c) * S;
  296.   }
  297.   const uWarp = { value: new THREE.Vector4(120, 150, 0.03, 1) };
  298.   const uTint = { value: 0 };
  299.  
  300.   const WARP_DECL = `
  301.     uniform vec4 uWarp;
  302.     #ifdef WARP_ATTR
  303.       attribute vec3 aCenter;
  304.     #else
  305.       uniform vec3 uCenter;
  306.     #endif
  307.     varying float vWarpAmt;
  308.     float warpF(float d) {
  309.       float D = uWarp.x; float W = max(uWarp.y, 1.0); float c = uWarp.z;
  310.       float S = 0.0;
  311.       if (d > D + W) S = 0.5 * W + (d - D - W);
  312.       else if (d > D) { float t = (d - D) / W; S = W * (t * t * t - 0.5 * t * t * t * t); }
  313.       return d - (1.0 - c) * S;
  314.     }
  315.   `;
  316.   const WARP_PROJECT = `
  317.     vec4 wPos = vec4(transformed, 1.0);
  318.     #ifdef USE_INSTANCING
  319.       wPos = instanceMatrix * wPos;
  320.     #endif
  321.     wPos = modelMatrix * wPos;
  322.     #ifdef WARP_ATTR
  323.       vec3 wCenter = aCenter;
  324.     #else
  325.       vec3 wCenter = uCenter;
  326.     #endif
  327.     vec3 toC = wCenter - cameraPosition;
  328.     float cDist = max(length(toC), 0.001);
  329.     vec3 wShift = toC * (warpF(cDist) / cDist - 1.0) * uWarp.w;
  330.     vWarpAmt = uWarp.w * (1.0 - uWarp.z) * smoothstep(uWarp.x, uWarp.x + max(uWarp.y, 1.0), cDist);
  331.     vec4 mvPosition = viewMatrix * vec4(wPos.xyz + wShift, 1.0);
  332.     gl_Position = projectionMatrix * mvPosition;
  333.   `;
  334.   // Fog uses the real distance so far things keep their haze
  335.   const WARP_FOG = `
  336.     #ifdef USE_FOG
  337.       fogDepth = -(viewMatrix * wPos).z;
  338.     #endif
  339.   `;
  340.  
  341.   function warpify(mat, center) {
  342.     const uCenter = center ? { value: center.clone() } : null;
  343.     mat.defines = Object.assign({}, mat.defines, center ? { WARP_UNIFORM: "" } : { WARP_ATTR: "" });
  344.     mat.onBeforeCompile = (shader) => {
  345.       shader.uniforms.uWarp = uWarp;
  346.       shader.uniforms.uTint = uTint;
  347.       if (uCenter) shader.uniforms.uCenter = uCenter;
  348.       shader.vertexShader = shader.vertexShader
  349.         .replace("#include <common>", "#include <common>\n" + WARP_DECL)
  350.         .replace("#include <project_vertex>", WARP_PROJECT)
  351.         .replace("#include <fog_vertex>", WARP_FOG);
  352.       shader.fragmentShader = shader.fragmentShader
  353.         .replace("#include <common>", "#include <common>\nvarying float vWarpAmt;\nuniform float uTint;")
  354.         .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);");
  355.     };
  356.     return mat;
  357.   }
  358.  
  359.   // ---------------------------------------------------------------------------
  360.   // Renderer, scene, camera
  361.   // ---------------------------------------------------------------------------
  362.   const canvas = $("view");
  363.   let renderer;
  364.   try {
  365.     renderer = new THREE.WebGLRenderer({ canvas, antialias: false, powerPreference: "high-performance" });
  366.   } catch (err) {
  367.     $("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>';
  368.     return;
  369.   }
  370.   renderer.setPixelRatio(1);
  371.  
  372.   const scene = new THREE.Scene();
  373.   const HORIZON = new THREE.Color("#a497c2");
  374.   const ZENITH = new THREE.Color("#2f3268");
  375.   scene.fog = new THREE.FogExp2(new THREE.Color("#9e93bf"), 0.00011);
  376.  
  377.   const VFOV = 62;
  378.   const camera = new THREE.PerspectiveCamera(VFOV, 16 / 9, 0.5, 14000);
  379.   camera.rotation.order = "YXZ";
  380.  
  381.   const SUN_DIR = new THREE.Vector3(-0.55, 0.62, 0.55).normalize();
  382.   scene.add(new THREE.HemisphereLight(0xc4b8ec, 0x39452c, 0.62));
  383.   const sun = new THREE.DirectionalLight(0xffe2c4, 0.85);
  384.   sun.position.copy(SUN_DIR).multiplyScalar(1000);
  385.   scene.add(sun);
  386.  
  387.   const sky = new THREE.Mesh(
  388.     new THREE.SphereGeometry(11000, 32, 16),
  389.     new THREE.ShaderMaterial({
  390.       side: THREE.BackSide, depthWrite: false, depthTest: false, fog: false,
  391.       uniforms: { horizon: { value: HORIZON }, zenith: { value: ZENITH }, sunDir: { value: SUN_DIR } },
  392.       vertexShader: `
  393.         varying vec3 vDir;
  394.         void main() {
  395.           vDir = normalize(position);
  396.           gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  397.         }`,
  398.       fragmentShader: `
  399.         uniform vec3 horizon; uniform vec3 zenith; uniform vec3 sunDir;
  400.         varying vec3 vDir;
  401.         void main() {
  402.           vec3 d = normalize(vDir);
  403.           float t = clamp(d.y * 1.5 + 0.03, 0.0, 1.0);
  404.           vec3 c = mix(horizon, zenith, pow(t, 0.75));
  405.           float s = max(dot(d, sunDir), 0.0);
  406.           c += vec3(1.0, 0.78, 0.55) * (pow(s, 48.0) * 0.45 + pow(s, 6.0) * 0.08);
  407.           gl_FragColor = vec4(c, 1.0);
  408.         }`
  409.     })
  410.   );
  411.   sky.renderOrder = -1;
  412.   sky.frustumCulled = false;
  413.   scene.add(sky);
  414.  
  415.   const flat = (color, extra = {}) => new THREE.MeshPhongMaterial(Object.assign({
  416.     color, flatShading: true, shininess: 0, specular: 0x000000
  417.   }, extra));
  418.  
  419.   function mergeParts(parts) {
  420.     const pos = [], col = [];
  421.     const c = new THREE.Color();
  422.     for (const p of parts) {
  423.       const g = p.geo.index ? p.geo.toNonIndexed() : p.geo.clone();
  424.       if (p.matrix) g.applyMatrix4(p.matrix);
  425.       const a = g.attributes.position.array;
  426.       c.set(p.color);
  427.       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); }
  428.      g.dispose();
  429.    }
  430.    const out = new THREE.BufferGeometry();
  431.    out.setAttribute("position", new THREE.Float32BufferAttribute(pos, 3));
  432.    out.setAttribute("color", new THREE.Float32BufferAttribute(col, 3));
  433.    out.computeVertexNormals();
  434.    return out;
  435.  }
  436.  const at = (x, y, z, ry = 0, sx = 1, sy = 1, sz = 1) =>
  437.     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));
  438.  
  439.   const anim = { windmill: null, birds: [], clouds: null, mist: null };
  440.   const noCull = (o) => { o.frustumCulled = false; return o; };
  441.  
  442.   // Terrain colors
  443.   const cGrassA = new THREE.Color("#3c7038"), cGrassB = new THREE.Color("#78a443"), cMeadow = new THREE.Color("#8cb84e");
  444.   const cForest = new THREE.Color("#2c4f2e"), cRock = new THREE.Color("#5d5874"), cRockDark = new THREE.Color("#403c58");
  445.   const cSnow = new THREE.Color("#dcd6ec");
  446.   function terrainColor(x, z, h, ny, out) {
  447.     const n = fbm(x * 0.02, z * 0.02, 2);
  448.     out.copy(cGrassA).lerp(cGrassB, n);
  449.     const side = Math.abs(x - pathX(z));
  450.     if (z < -150 && z > -1150) out.lerp(cMeadow, 0.55 * (1 - smooth(60, 260, side)) * n);
  451.     const forestN = fbm(x * 0.005 + 11, z * 0.005 - 4, 3);
  452.     out.lerp(cForest, 0.7 * smooth(0.47, 0.6, forestN) * smooth(40, 160, side));
  453.     if (z > -90 && Math.abs(x) < 700) out.lerp(cForest, 0.5 * smooth(-90, -30, z));
  454.     const steep = 1 - ny;
  455.     const rockAmt = Math.max(smooth(0.28, 0.5, steep), smooth(620, 820, h + n * 120));
  456.     out.lerp(n > 0.5 ? cRock : cRockDark, rockAmt);
  457.     out.lerp(cSnow, smooth(1250, 1400, h + n * 160) * smooth(0.25, 0.45, ny));
  458.     return out;
  459.   }
  460.   function normalY(x, z) {
  461.     const e = 4;
  462.     const dx = H(x + e, z) - H(x - e, z), dz = H(x, z + e) - H(x, z - e);
  463.     return (2 * e) / Math.hypot(dx, 2 * e, dz);
  464.   }
  465.  
  466.   function buildWorld() {
  467.     const tmpC = new THREE.Color();
  468.     const cc = [0, 0, 0];
  469.  
  470.     // ---------------- Warped terrain: one mesh, each tile carries its chunk center ----------------
  471.     {
  472.       const SUB = 6, step = TILE / SUB;
  473.       const vPer = (SUB + 1) * (SUB + 1);
  474.       const nT = NTX * NTZ;
  475.       const pos = new Float32Array(nT * vPer * 3), col = new Float32Array(nT * vPer * 3), cen = new Float32Array(nT * vPer * 3);
  476.       const idx = new Uint32Array(nT * SUB * SUB * 6);
  477.       let v = 0, k = 0;
  478.       for (let j = 0; j < NTZ; j++) for (let i = 0; i < NTX; i++) {
  479.        const x0 = RX0 + i * TILE, z0 = RZ0 + j * TILE;
  480.        const o = (j * NTX + i) * 3;
  481.        const base = v;
  482.        for (let b = 0; b <= SUB; b++) for (let a = 0; a <= SUB; a++) {
  483.          const x = x0 + a * step, z = z0 + b * step, h = H(x, z);
  484.          pos[v * 3] = x; pos[v * 3 + 1] = h; pos[v * 3 + 2] = z;
  485.          terrainColor(x, z, h, normalY(x, z), tmpC);
  486.          col[v * 3] = tmpC.r; col[v * 3 + 1] = tmpC.g; col[v * 3 + 2] = tmpC.b;
  487.          cen[v * 3] = tileCenters[o]; cen[v * 3 + 1] = tileCenters[o + 1]; cen[v * 3 + 2] = tileCenters[o + 2];
  488.          v++;
  489.        }
  490.        for (let b = 0; b < SUB; b++) for (let a = 0; a < SUB; a++) {
  491.          const p = base + b * (SUB + 1) + a, q = p + 1, r = p + SUB + 1, s = r + 1;
  492.          idx[k++] = p; idx[k++] = r; idx[k++] = q;
  493.          idx[k++] = q; idx[k++] = r; idx[k++] = s;
  494.        }
  495.      }
  496.      const g = new THREE.BufferGeometry();
  497.      g.setAttribute("position", new THREE.BufferAttribute(pos, 3));
  498.      g.setAttribute("color", new THREE.BufferAttribute(col, 3));
  499.      g.setAttribute("aCenter", new THREE.BufferAttribute(cen, 3));
  500.      g.setIndex(new THREE.BufferAttribute(idx, 1));
  501.      g.computeVertexNormals();
  502.      scene.add(noCull(new THREE.Mesh(g, warpify(flat(0xffffff, { vertexColors: true })))));
  503.    }
  504.  
  505.    // ---------------- Outer terrain (mountains), not warped ----------------
  506.    {
  507.      const N = 300, S = 4800, A = 0.22, CZ = -600;
  508.      const map = (u) => S * (A * u + (1 - A) * u * u * u);
  509.       const verts = new Float32Array((N + 1) * (N + 1) * 3);
  510.       let k = 0;
  511.       for (let j = 0; j <= N; j++) {
  512.        const z = CZ + map(-1 + 2 * j / N);
  513.        for (let i = 0; i <= N; i++) {
  514.          const x = map(-1 + 2 * i / N);
  515.          verts[k++] = x; verts[k++] = H(x, z); verts[k++] = z;
  516.        }
  517.      }
  518.      const idx = [];
  519.      for (let j = 0; j < N; j++) for (let i = 0; i < N; i++) {
  520.        const a = j * (N + 1) + i, b = a + 1, c = a + N + 1, d = c + 1;
  521.        const x0 = verts[a * 3], z0 = verts[a * 3 + 2], x1 = verts[d * 3], z1 = verts[d * 3 + 2];
  522.        // skip cells fully inside the warped region
  523.        if (x0 >= RX0 + 5 && x1 <= RX1 - 5 && z0 >= RZ0 + 5 && z1 <= RZ1 - 5) continue;
  524.         idx.push(a, c, b, b, c, d);
  525.       }
  526.       const g = new THREE.BufferGeometry();
  527.       g.setAttribute("position", new THREE.BufferAttribute(verts, 3));
  528.       g.setIndex(idx);
  529.       g.computeVertexNormals();
  530.       const nrm = g.attributes.normal.array, cols = new Float32Array(verts.length);
  531.       for (let v = 0; v < verts.length; v += 3) {
  532.        terrainColor(verts[v], verts[v + 2], verts[v + 1], nrm[v + 1], tmpC);
  533.        cols[v] = tmpC.r; cols[v + 1] = tmpC.g; cols[v + 2] = tmpC.b;
  534.      }
  535.      g.setAttribute("color", new THREE.BufferAttribute(cols, 3));
  536.      scene.add(new THREE.Mesh(g, flat(0xffffff, { vertexColors: true })));
  537.    }
  538.  
  539.    // ---------------- Path (each piece uses the chunk center of the tile it sits on) ----------------
  540.    {
  541.      const pos = [], col = [], cen = [];
  542.      const stone = [new THREE.Color("#d8c9a4"), new THREE.Color("#bfae8a"), new THREE.Color("#e6dcc0"), new THREE.Color("#a99a7c")];
  543.      let prev = null;
  544.      for (let z = PATH_START; z >= PATH_END; z -= 2) {
  545.         const x = pathX(z);
  546.         const tx = pathX(z - 1) - x, tz = -1, len = Math.hypot(tx, tz);
  547.         const nx = -tz / len, nz = tx / len;
  548.         const w = 1.6 + 0.35 * Math.sin(z * 0.21) + 0.25 * (hash2(z | 0, 7) - 0.5);
  549.         const L = [x + nx * w, 0, z + nz * w], R = [x - nx * w, 0, z - nz * w];
  550.         L[1] = H(L[0], L[2]) + 0.3; R[1] = H(R[0], R[2]) + 0.3;
  551.         if (prev) {
  552.           centerOf(x, z + 1, cc);
  553.           const c1 = stone[hash2(z | 0, 1) * 4 | 0], c2 = stone[hash2(z | 0, 2) * 4 | 0];
  554.           pos.push(...prev.L, ...prev.R, ...L, ...prev.R, ...R, ...L);
  555.           for (let i = 0; i < 3; i++) col.push(c1.r, c1.g, c1.b);
  556.          for (let i = 0; i < 3; i++) col.push(c2.r, c2.g, c2.b);
  557.          for (let i = 0; i < 6; i++) cen.push(cc[0], cc[1], cc[2]);
  558.        }
  559.        prev = { L, R };
  560.      }
  561.      const g = new THREE.BufferGeometry();
  562.      g.setAttribute("position", new THREE.Float32BufferAttribute(pos, 3));
  563.      g.setAttribute("color", new THREE.Float32BufferAttribute(col, 3));
  564.      g.setAttribute("aCenter", new THREE.Float32BufferAttribute(cen, 3));
  565.      g.computeVertexNormals();
  566.      const mat = warpify(flat(0xffffff, { vertexColors: true, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2, side: THREE.DoubleSide }));
  567.      scene.add(noCull(new THREE.Mesh(g, mat)));
  568.    }
  569.  
  570.    // ---------------- Trees and bushes ----------------
  571.    const coniferGeo = mergeParts([
  572.      { geo: new THREE.CylinderGeometry(0.35, 0.5, 3.2, 5, 1, true), color: "#4a3527", matrix: at(0, 1.6, 0) },
  573.      { geo: new THREE.ConeGeometry(3.3, 6, 7, 1, true), color: "#2c5536", matrix: at(0, 5.2, 0) },
  574.      { geo: new THREE.ConeGeometry(2.6, 5, 7, 1, true), color: "#356640", matrix: at(0, 8.2, 0, 0.4) },
  575.      { geo: new THREE.ConeGeometry(1.8, 4.2, 7, 1, true), color: "#417449", matrix: at(0, 11, 0, 0.8) }
  576.    ]);
  577.    const roundGeo = mergeParts([
  578.      { geo: new THREE.CylinderGeometry(0.35, 0.5, 3.4, 5, 1, true), color: "#523b2a", matrix: at(0, 1.7, 0) },
  579.      { geo: new THREE.IcosahedronGeometry(3.4, 0), color: "#4f8a3c", matrix: at(0, 5.6, 0, 0, 1, 0.9, 1) },
  580.      { geo: new THREE.IcosahedronGeometry(2.2, 0), color: "#5f9a44", matrix: at(1.2, 7.4, 0.6, 0.7) }
  581.    ]);
  582.    const MAX_CON = 22000, MAX_ROUND = 12000;
  583.    const conCenters = new Float32Array(MAX_CON * 3), roundCenters = new Float32Array(MAX_ROUND * 3);
  584.    coniferGeo.setAttribute("aCenter", new THREE.InstancedBufferAttribute(conCenters, 3));
  585.    roundGeo.setAttribute("aCenter", new THREE.InstancedBufferAttribute(roundCenters, 3));
  586.    const conifers = new THREE.InstancedMesh(coniferGeo, warpify(flat(0xffffff, { vertexColors: true })), MAX_CON);
  587.    const rounds = new THREE.InstancedMesh(roundGeo, warpify(flat(0xffffff, { vertexColors: true })), MAX_ROUND);
  588.    let nCon = 0, nRound = 0;
  589.    const m4 = new THREE.Matrix4(), q = new THREE.Quaternion(), e3 = new THREE.Euler(), p3 = new THREE.Vector3(), s3 = new THREE.Vector3();
  590.    const tint = new THREE.Color();
  591.    function place(mesh, centers, idx, x, y, z, s, lean = 0) {
  592.      e3.set((rand() - 0.5) * lean, rand() * Math.PI * 2, (rand() - 0.5) * lean);
  593.      q.setFromEuler(e3); p3.set(x, y, z); s3.set(s * (0.9 + rand() * 0.2), s, s * (0.9 + rand() * 0.2));
  594.      m4.compose(p3, q, s3);
  595.      mesh.setMatrixAt(idx, m4);
  596.      const b = 0.78 + rand() * 0.4;
  597.      tint.setRGB(b * (0.95 + rand() * 0.1), b, b * (0.9 + rand() * 0.15));
  598.      mesh.setColorAt(idx, tint);
  599.      centerOf(x, z, cc);
  600.      centers[idx * 3] = cc[0]; centers[idx * 3 + 1] = cc[1]; centers[idx * 3 + 2] = cc[2];
  601.    }
  602.    function treeDensity(x, z) {
  603.      const side = Math.abs(x - pathX(z));
  604.      if (z < PATH_START + 60 && z > PATH_END - 20 && side < 7) return 0;
  605.       if (Math.hypot(x - CASTLE.x, z - CASTLE.z) < 120) return 0;
  606.      let d = 0.035;
  607.      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))));
  608.       const n = fbm(x * 0.005 + 11, z * 0.005 - 4, 3);
  609.       d = Math.max(d, smooth(0.47, 0.6, n) * 0.55 * smooth(40, 160, side));
  610.       if (z < -240 && z > -950 && side < 220) d *= 0.3;
  611.       return d;
  612.     }
  613.     const STEP = 10;
  614.     for (let gz = RZ1 - 5; gz > RZ0 + 5 && (nCon < MAX_CON || nRound < MAX_ROUND); gz -= STEP) {
  615.       for (let gx = RX0 + 5; gx < RX1 - 5; gx += STEP) {
  616.        const x = gx + (rand() - 0.5) * STEP * 0.95, z = gz + (rand() - 0.5) * STEP * 0.95;
  617.        if (!inRegion(x, z) || rand() > treeDensity(x, z)) continue;
  618.         const h = H(x, z);
  619.         if (h > 780) continue;
  620.         if (Math.abs(H(x + 3, z) - h) > 2.6 || Math.abs(H(x, z + 3) - h) > 2.6) continue;
  621.         const side = Math.abs(x - pathX(z));
  622.         const roundChance = side < 250 && z < -60 ? 0.35 : 0.07;
  623.        if (rand() < roundChance) {
  624.          if (nRound < MAX_ROUND) place(rounds, roundCenters, nRound++, x, h - 0.4, z, 0.8 + rand() * 0.7, 0.08);
  625.        } else if (nCon < MAX_CON) {
  626.          place(conifers, conCenters, nCon++, x, h - 0.4, z, 0.8 + rand() * 1.1, 0.06);
  627.        }
  628.      }
  629.    }
  630.    for (let z = PATH_START + 40; z > PATH_END; z -= 1.6) {
  631.       for (const sgn of [-1, 1]) {
  632.         if (rand() > 0.55) continue;
  633.         const off = 2.6 + Math.pow(rand(), 1.6) * 14;
  634.         const x = pathX(z) + sgn * off, zz = z + (rand() - 0.5) * 1.5;
  635.         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);
  636.      }
  637.    }
  638.    conifers.count = nCon; rounds.count = nRound;
  639.    for (const m of [conifers, rounds]) {
  640.      m.instanceMatrix.needsUpdate = true;
  641.      if (m.instanceColor) m.instanceColor.needsUpdate = true;
  642.      m.geometry.attributes.aCenter.needsUpdate = true;
  643.      noCull(m); scene.add(m);
  644.    }
  645.  
  646.    // ---------------- Village ----------------
  647.    const houseGeo = mergeParts([
  648.      { geo: new THREE.BoxGeometry(8, 8, 6), color: "#e6dbc0", matrix: at(0, 1, 0) },
  649.      { geo: new THREE.ConeGeometry(6.4, 4.2, 4), color: "#a5432c", matrix: at(0, 7.1, 0, Math.PI / 4, 1, 1, 0.78) },
  650.      { geo: new THREE.BoxGeometry(1, 3, 1), color: "#6c5a4c", matrix: at(2.2, 7.6, 1) },
  651.      { geo: new THREE.BoxGeometry(1.4, 2.2, 0.2), color: "#4a3527", matrix: at(0, -1.9, 3.05) }
  652.    ]);
  653.    const houseCenters = new Float32Array(90 * 3);
  654.    houseGeo.setAttribute("aCenter", new THREE.InstancedBufferAttribute(houseCenters, 3));
  655.    const houses = new THREE.InstancedMesh(houseGeo, warpify(flat(0xffffff, { vertexColors: true })), 90);
  656.    const placed = [];
  657.    let tries = 0, nHouse = 0;
  658.    while (nHouse < 90 && tries++ < 5000) {
  659.      const z = -250 - Math.pow(rand(), 1.3) * 720;
  660.      const side = (rand() < 0.5 ? -1 : 1) * (13 + Math.pow(rand(), 1.7) * 230);
  661.      const x = pathX(z) + side;
  662.      const h = H(x, z);
  663.      if (Math.abs(H(x + 5, z) - h) > 2.2 || Math.abs(H(x, z + 5) - h) > 2.2) continue;
  664.       if (placed.some((p) => Math.hypot(p[0] - x, p[1] - z) < 17)) continue;
  665.      placed.push([x, z]);
  666.      const facePath = Math.atan2(pathX(z) - x, 0) + (rand() - 0.5) * 0.8;
  667.      const s = 0.85 + rand() * 0.45;
  668.      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));
  669.      houses.setMatrixAt(nHouse, m4);
  670.      const b = 0.85 + rand() * 0.25;
  671.      tint.setRGB(b, b * (0.93 + rand() * 0.08), b * (0.88 + rand() * 0.12));
  672.      houses.setColorAt(nHouse, tint);
  673.      centerOf(x, z, cc);
  674.      houseCenters.set(cc, nHouse * 3);
  675.      nHouse++;
  676.    }
  677.    houses.count = nHouse;
  678.    scene.add(noCull(houses));
  679.  
  680.    // Windmill
  681.    {
  682.      const wz = -430, wx = pathX(wz) - 58, wh = H(wx, wz);
  683.      const c = new THREE.Vector3(...centerOf(wx, wz, cc));
  684.      const g = new THREE.Group();
  685.      g.position.set(wx, wh - 1, wz);
  686.      const tower = new THREE.Mesh(new THREE.CylinderGeometry(2.6, 4, 16, 8), warpify(flat(0xe0d4b8), c));
  687.      tower.position.y = 8; g.add(noCull(tower));
  688.      const cap = new THREE.Mesh(new THREE.ConeGeometry(3.4, 4, 8), warpify(flat(0x7a3a2a), c));
  689.      cap.position.y = 18; g.add(noCull(cap));
  690.      const hub = new THREE.Group(); hub.position.set(0, 15.5, 3.4); g.add(hub);
  691.      const bladeMat = warpify(flat(0xcfc2a8), c), sparMat = warpify(flat(0x5a4636), c);
  692.      for (let i = 0; i < 4; i++) {
  693.        const arm = new THREE.Group(); arm.rotation.z = i * Math.PI / 2;
  694.        const blade = new THREE.Mesh(new THREE.BoxGeometry(2.2, 11, 0.25), bladeMat);
  695.        blade.position.y = 6.2; arm.add(noCull(blade));
  696.        const spar = new THREE.Mesh(new THREE.BoxGeometry(0.35, 12, 0.35), sparMat);
  697.        spar.position.y = 6; arm.add(noCull(spar));
  698.        hub.add(arm);
  699.      }
  700.      g.rotation.y = 0.35;
  701.      scene.add(g);
  702.      anim.windmill = hub;
  703.    }
  704.  
  705.    // ---------------- Castle (one chunk together with its crag) ----------------
  706.    {
  707.      let minTop = castleCenter.y;
  708.      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));
  709.      const S = 0.5;
  710.      const wall = "#3e3c58", wallLight = "#4a4766", roof = "#27263e";
  711.      const parts = [];
  712.      const base = minTop - 4;
  713.      parts.push({ geo: new THREE.CylinderGeometry(70 * S, 74 * S, 40 * S, 10, 1, true), color: wall, matrix: at(0, base + 20 * S, 0) });
  714.      parts.push({ geo: new THREE.CylinderGeometry(70 * S, 70 * S, 1, 10), color: wallLight, matrix: at(0, base + 40 * S, 0) });
  715.      for (let i = 0; i < 6; i++) {
  716.        const a = i / 6 * Math.PI * 2 + 0.3;
  717.        const tx = Math.cos(a) * 71 * S, tz = Math.sin(a) * 71 * S, hgt = (52 + (i % 3) * 8) * S;
  718.        parts.push({ geo: new THREE.CylinderGeometry(7 * S, 7.6 * S, hgt, 8), color: wallLight, matrix: at(tx, base + hgt / 2, tz) });
  719.        parts.push({ geo: new THREE.ConeGeometry(9 * S, 22 * S, 8), color: roof, matrix: at(tx, base + hgt + 11 * S, tz) });
  720.      }
  721.      for (let i = 0; i < 4; i++) {
  722.        const a = i / 4 * Math.PI * 2 + Math.PI / 4;
  723.        const tx = Math.cos(a) * 34 * S, tz = Math.sin(a) * 34 * S, hgt = (78 + (i % 2) * 14) * S;
  724.        parts.push({ geo: new THREE.CylinderGeometry(9 * S, 10 * S, hgt, 8), color: wall, matrix: at(tx, base + hgt / 2, tz) });
  725.        parts.push({ geo: new THREE.ConeGeometry(11.5 * S, 32 * S, 8), color: roof, matrix: at(tx, base + hgt + 16 * S, tz) });
  726.      }
  727.      const keepH = 118 * S;
  728.      parts.push({ geo: new THREE.BoxGeometry(32 * S, keepH, 32 * S), color: wallLight, matrix: at(0, base + keepH / 2, 0, 0.2) });
  729.      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) });
  730.      for (let i = 0; i < 4; i++) {
  731.        const a = 0.2 + i * Math.PI / 2 + Math.PI / 4;
  732.        const tx = Math.sin(a) * 22 * S, tz = Math.cos(a) * 22 * S;
  733.        parts.push({ geo: new THREE.CylinderGeometry(4 * S, 4 * S, 22 * S, 6), color: wall, matrix: at(tx, base + keepH + 6 * S, tz) });
  734.        parts.push({ geo: new THREE.ConeGeometry(5.2 * S, 16 * S, 6), color: roof, matrix: at(tx, base + keepH + 25 * S, tz) });
  735.      }
  736.      const castle = new THREE.Mesh(mergeParts(parts), warpify(flat(0xffffff, { vertexColors: true }), castleCenter));
  737.      castle.position.set(CASTLE.x, 0, CASTLE.z);
  738.      scene.add(noCull(castle));
  739.  
  740.      const winGeo = new THREE.BoxGeometry(2.4 * S, 4.2 * S, 0.4);
  741.      const wins = new THREE.InstancedMesh(winGeo, warpify(new THREE.MeshBasicMaterial({ color: 0xffcf6a }), castleCenter), 160);
  742.      let nw = 0;
  743.      const addWin = (x, y, z, ry) => { if (nw < 160) wins.setMatrixAt(nw++, at(CASTLE.x + x, y, CASTLE.z + z, ry)); };
  744.      for (let f = 0; f < 4; f++) {
  745.        const ry = 0.2 + f * Math.PI / 2;
  746.        for (let r = 0; r < 9; r++) for (let c = -1; c <= 1; c++) {
  747.          if (rand() > 0.5) continue;
  748.           const lx = c * 8.5 * S, ly = base + (20 + r * 11) * S, lz = 16.05 * S;
  749.           addWin(Math.sin(ry) * lz + Math.cos(ry) * lx, ly, Math.cos(ry) * lz - Math.sin(ry) * lx, ry);
  750.         }
  751.       }
  752.       for (let i = 0; i < 4; i++) {
  753.        const a = i / 4 * Math.PI * 2 + Math.PI / 4;
  754.        for (let r = 0; r < 5; r++) {
  755.          if (rand() > 0.6) continue;
  756.           const ang = a + (rand() - 0.5) * 1.2 + Math.PI * 0.75;
  757.           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;
  758.           addWin(rx, base + (22 + r * 12) * S, rz, -ang + Math.PI / 2);
  759.         }
  760.       }
  761.       wins.count = nw;
  762.       scene.add(noCull(wins));
  763.  
  764.       // Birds circling the towers and over the village
  765.       const wingGeo = new THREE.BufferGeometry();
  766.       wingGeo.setAttribute("position", new THREE.Float32BufferAttribute([0, 0, -1, 0, 0, 1, 1, 0, 0.2], 3));
  767.       function makeBird(size, mat) {
  768.         const b = new THREE.Group();
  769.         const l = noCull(new THREE.Mesh(wingGeo, mat)), r = noCull(new THREE.Mesh(wingGeo, mat));
  770.         l.scale.setScalar(size); r.scale.set(-size, size, size);
  771.         b.add(l, r); b.userData = { l, r };
  772.         return b;
  773.       }
  774.       const castleBirdMat = warpify(new THREE.MeshBasicMaterial({ color: 0x1c1a2c, side: THREE.DoubleSide }), castleCenter);
  775.       for (let i = 0; i < 14; i++) {
  776.        const b = makeBird(1.6 + rand() * 0.8, castleBirdMat);
  777.        b.userData.center = new THREE.Vector3(CASTLE.x, base + (130 + rand() * 90) * S, CASTLE.z);
  778.        b.userData.radius = 25 + rand() * 45; b.userData.speed = (0.25 + rand() * 0.2) * (rand() < 0.5 ? -1 : 1);
  779.        b.userData.phase = rand() * Math.PI * 2; b.userData.flap = 3 + rand() * 2;
  780.        scene.add(b); anim.birds.push(b);
  781.      }
  782.      const vz = -520, vx = pathX(vz) + 20;
  783.      const villageBirdMat = warpify(new THREE.MeshBasicMaterial({ color: 0x1c1a2c, side: THREE.DoubleSide }), new THREE.Vector3(...centerOf(vx, vz, cc)));
  784.      for (let i = 0; i < 8; i++) {
  785.        const b = makeBird(1.8 + rand() * 0.8, villageBirdMat);
  786.        b.userData.center = new THREE.Vector3(vx, H(vx, vz) + 45 + rand() * 25, vz);
  787.        b.userData.radius = 50 + rand() * 60; b.userData.speed = (0.18 + rand() * 0.1) * (rand() < 0.5 ? -1 : 1);
  788.        b.userData.phase = rand() * Math.PI * 2; b.userData.flap = 5 + rand() * 3;
  789.        scene.add(b); anim.birds.push(b);
  790.      }
  791.  
  792.      // Mist ring around the crag
  793.      const mist = new THREE.Group();
  794.      mist.position.set(CASTLE.x, 0, CASTLE.z);
  795.      const puffGeo = new THREE.IcosahedronGeometry(1, 1);
  796.      const mistMat = warpify(flat(0xcfc7e2, { emissive: 0x2a2440 }), castleCenter);
  797.      for (let i = 0; i < 24; i++) {
  798.        const a = i / 24 * Math.PI * 2 + rand() * 0.2, r = 75 + rand() * 30;
  799.        const p = noCull(new THREE.Mesh(puffGeo, mistMat));
  800.        p.position.set(Math.cos(a) * r, castleCenter.y - 70 + rand() * 20, Math.sin(a) * r);
  801.        p.scale.set(20 + rand() * 14, 6 + rand() * 5, 15 + rand() * 10);
  802.        p.rotation.y = rand() * 3;
  803.        mist.add(p);
  804.      }
  805.      scene.add(mist);
  806.      anim.mist = mist;
  807.    }
  808.  
  809.    // ---------------- Clouds (sky layer, not warped) ----------------
  810.    const clouds = new THREE.Group();
  811.    clouds.position.set(RING.x, 0, RING.z);
  812.    const cloudGeo = new THREE.IcosahedronGeometry(1, 1);
  813.    const cloudMat = flat(0xbfb6d6, { emissive: 0x26203c });
  814.    for (let i = 0; i < 34; i++) {
  815.      const a = rand() * Math.PI * 2, r = 2200 + rand() * 2200, y = 900 + rand() * 700;
  816.      const cl = new THREE.Group();
  817.      cl.position.set(Math.cos(a) * r, y, Math.sin(a) * r);
  818.      cl.rotation.y = rand() * Math.PI;
  819.      const nP = 4 + (rand() * 4 | 0);
  820.      for (let j = 0; j < nP; j++) {
  821.        const p = new THREE.Mesh(cloudGeo, cloudMat);
  822.        p.position.set((rand() - 0.5) * 260, (rand() - 0.5) * 30, (rand() - 0.5) * 120);
  823.        p.scale.set(70 + rand() * 90, 26 + rand() * 30, 55 + rand() * 60);
  824.        cl.add(p);
  825.      }
  826.      clouds.add(cl);
  827.    }
  828.    scene.add(clouds);
  829.    anim.clouds = clouds;
  830.  }
  831.  
  832.  // ---------------------------------------------------------------------------
  833.  // Low-resolution render target and composite pass
  834.  // ---------------------------------------------------------------------------
  835.  let rt = null, lowW = 320, lowH = 180;
  836.  const post = new THREE.ShaderMaterial({
  837.    depthTest: false, depthWrite: false,
  838.    uniforms: {
  839.      tColor: { value: null }, tDepth: { value: null },
  840.      lowRes: { value: new THREE.Vector2(320, 180) },
  841.      cNear: { value: camera.near }, cFar: { value: camera.far },
  842.      useDither: { value: 1 }, useOutline: { value: 1 }, levels: { value: 14 }
  843.    },
  844.    vertexShader: `
  845.      varying vec2 vUv;
  846.      void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }`,
  847.    fragmentShader: `
  848.      uniform sampler2D tColor; uniform sampler2D tDepth;
  849.      uniform vec2 lowRes; uniform float cNear; uniform float cFar;
  850.      uniform float useDither; uniform float useOutline; uniform float levels;
  851.      varying vec2 vUv;
  852.      float linDepth(float d) { float z = d * 2.0 - 1.0; return (2.0 * cNear * cFar) / (cFar + cNear - z * (cFar - cNear)); }
  853.      float bayer2(vec2 a) { a = floor(a); return fract(dot(a, vec2(0.5, a.y * 0.75))); }
  854.      float bayer4(vec2 a) { return bayer2(0.5 * a) * 0.25 + bayer2(a); }
  855.      float depthAt(vec2 px) {
  856.        float d = texture2D(tDepth, (px + 0.5) / lowRes).x;
  857.        return d < 0.99999 ? linDepth(d) : cFar * 10.0;
  858.      }
  859.      void main() {
  860.        vec2 px = floor(vUv * lowRes);
  861.        vec2 uv = (px + 0.5) / lowRes;
  862.        vec3 col = texture2D(tColor, uv).rgb;
  863.        float d = texture2D(tDepth, uv).x;
  864.        if (useOutline > 0.5 && d < 0.99999) {
  865.          float lz = linDepth(d);
  866.           float far = max(max(depthAt(px + vec2(1.0, 0.0)), depthAt(px - vec2(1.0, 0.0))),
  867.                           max(depthAt(px + vec2(0.0, 1.0)), depthAt(px - vec2(0.0, 1.0))));
  868.           if (far > lz * 1.15 + 1.0) col *= 0.52;
  869.         }
  870.         if (useDither > 0.5) {
  871.           float b = bayer4(px);
  872.           col = floor(clamp(col, 0.0, 1.0) * levels + 0.5 + (b - 0.5) * 0.55) / levels;
  873.         }
  874.         gl_FragColor = vec4(col, 1.0);
  875.       }`
  876.   });
  877.   const postScene = new THREE.Scene();
  878.   postScene.add(noCull(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), post)));
  879.   const postCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
  880.  
  881.   const PW_OPTIONS = ["160", "240", "320", "426", "640", "full"];
  882.   let pixelSetting = "320";
  883.  
  884.   function resize() {
  885.     const w = Math.max(1, window.innerWidth), h = Math.max(1, window.innerHeight);
  886.     renderer.setSize(w, h, false);
  887.     camera.aspect = w / h;
  888.     camera.updateProjectionMatrix();
  889.     lowW = pixelSetting === "full" ? w : Math.min(parseInt(pixelSetting, 10), w);
  890.     lowH = Math.max(1, Math.round(lowW * h / w));
  891.     if (rt) { rt.depthTexture.dispose(); rt.dispose(); }
  892.     rt = new THREE.WebGLRenderTarget(lowW, lowH, {
  893.       minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter, generateMipmaps: false, depthBuffer: true
  894.     });
  895.     rt.depthTexture = new THREE.DepthTexture(lowW, lowH);
  896.     rt.depthTexture.type = THREE.UnsignedIntType;
  897.     post.uniforms.tColor.value = rt.texture;
  898.     post.uniforms.tDepth.value = rt.depthTexture;
  899.     post.uniforms.lowRes.value.set(lowW, lowH);
  900.     $("res").textContent = `${lowW} × ${lowH}`;
  901.   }
  902.   window.addEventListener("resize", resize);
  903.  
  904.   // ---------------------------------------------------------------------------
  905.   // Controls
  906.   // ---------------------------------------------------------------------------
  907.   const keys = Object.create(null);
  908.   let yaw = 0, pitch = 0.08, locked = false;
  909.   const startZ = (() => { const m = /z=(-?\d+)/.exec(location.hash); return m ? Math.max(PATH_END + 60, Math.min(PATH_START - 20, +m[1])) : 60; })();
  910.   const pos = new THREE.Vector3(pathX(startZ), 0, startZ);
  911.   let pathZ = startZ;
  912.   const opts = { auto: true, tint: false, dither: true, outline: true, speed: 6, haze: 30 };
  913.  
  914.   canvas.addEventListener("click", () => { if (!locked && canvas.requestPointerLock) canvas.requestPointerLock(); });
  915.   document.addEventListener("pointerlockchange", () => {
  916.     locked = document.pointerLockElement === canvas;
  917.     $("hint").classList.toggle("hidden", locked);
  918.   });
  919.   document.addEventListener("mousemove", (e) => {
  920.     if (!locked) return;
  921.     yaw -= e.movementX * 0.0022;
  922.     pitch = Math.max(-1.45, Math.min(1.45, pitch - e.movementY * 0.0022));
  923.   });
  924.  
  925.   function applyWarp() {
  926.     uWarp.value.set(warp.D, warp.W, warp.c, warp.on);
  927.     uTint.value = opts.tint ? 1 : 0;
  928.   }
  929.   function syncUI() {
  930.     $("optAuto").checked = opts.auto;
  931.     $("optTint").checked = opts.tint;
  932.     $("optWarp").checked = !!warp.on;
  933.     $("optDither").checked = opts.dither;
  934.     $("optOutline").checked = opts.outline;
  935.     $("pw").value = pixelSetting;
  936.     post.uniforms.useDither.value = opts.dither ? 1 : 0;
  937.     post.uniforms.useOutline.value = opts.outline ? 1 : 0;
  938.     applyWarp();
  939.   }
  940.   function setAuto(on) {
  941.     opts.auto = on;
  942.     if (on) pathZ = Math.min(PATH_START - 20, Math.max(PATH_END + 50, pos.z));
  943.     syncUI();
  944.   }
  945.  
  946.   const MOVE_KEYS = ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"];
  947.   window.addEventListener("keydown", (e) => {
  948.     const tag = e.target && e.target.tagName;
  949.     if ((tag === "SELECT" || tag === "INPUT") && !locked && !MOVE_KEYS.includes(e.code) && !e.code.startsWith("Shift")) return;
  950.     keys[e.code] = true;
  951.     if (e.repeat) return;
  952.     if (e.code === "KeyF") { opts.tint = !opts.tint; syncUI(); }
  953.     else if (e.code === "KeyV") { warp.on = warp.on ? 0 : 1; syncUI(); }
  954.     else if (e.code === "KeyT") setAuto(!opts.auto);
  955.     else if (e.code === "KeyG") { opts.dither = !opts.dither; syncUI(); }
  956.     else if (e.code === "KeyO") { opts.outline = !opts.outline; syncUI(); }
  957.     else if (e.code === "KeyH") $("panel").classList.toggle("hidden");
  958.     else if (e.code === "KeyR") { pathZ = 60; pos.set(pathX(60), 0, 60); yaw = 0; pitch = 0.08; }
  959.     else if (e.code === "KeyP") {
  960.       pixelSetting = PW_OPTIONS[(PW_OPTIONS.indexOf(pixelSetting) + 1) % PW_OPTIONS.length];
  961.       syncUI(); resize();
  962.     }
  963.     if (MOVE_KEYS.includes(e.code) && opts.auto) setAuto(false);
  964.   });
  965.   window.addEventListener("keyup", (e) => { keys[e.code] = false; });
  966.   window.addEventListener("blur", () => { for (const k in keys) keys[k] = false; });
  967.  
  968.   $("optAuto").addEventListener("change", (e) => setAuto(e.target.checked));
  969.   $("optTint").addEventListener("change", (e) => { opts.tint = e.target.checked; syncUI(); });
  970.   $("optWarp").addEventListener("change", (e) => { warp.on = e.target.checked ? 1 : 0; syncUI(); });
  971.   $("optDither").addEventListener("change", (e) => { opts.dither = e.target.checked; syncUI(); });
  972.   $("optOutline").addEventListener("change", (e) => { opts.outline = e.target.checked; syncUI(); });
  973.   $("pw").addEventListener("change", (e) => { pixelSetting = e.target.value; resize(); e.target.blur(); });
  974.   $("speed").addEventListener("input", (e) => { opts.speed = parseFloat(e.target.value); $("speedOut").textContent = `${opts.speed} m/s`; });
  975.   $("haze").addEventListener("input", (e) => {
  976.     opts.haze = parseInt(e.target.value, 10);
  977.     $("hazeOut").textContent = String(opts.haze);
  978.     scene.fog.density = 0.0000037 * opts.haze;
  979.   });
  980.   $("dStart").addEventListener("input", (e) => { warp.D = +e.target.value; $("dStartOut").textContent = `${warp.D} m`; applyWarp(); });
  981.   $("dWidth").addEventListener("input", (e) => { warp.W = +e.target.value; $("dWidthOut").textContent = `${warp.W} m`; applyWarp(); });
  982.   $("drift").addEventListener("input", (e) => { warp.c = +e.target.value / 100; $("driftOut").textContent = `${e.target.value}%`; applyWarp(); });
  983.  
  984.   // ---------------------------------------------------------------------------
  985.   // Loop
  986.   // ---------------------------------------------------------------------------
  987.   const clock = new THREE.Clock();
  988.   let frames = 0, fpsT = 0, hudT = 0, time = 0;
  989.   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; };
  990.  const fmtDist = (d) => d >= 1000 ? `${(d / 1000).toFixed(2)} km` : `${Math.round(d)} m`;
  991.  
  992.   function step(dt) {
  993.     const v = opts.speed * (keys.ShiftLeft || keys.ShiftRight ? 3 : 1);
  994.     if (opts.auto) {
  995.       pathZ -= v * dt;
  996.       if (pathZ < PATH_END + 40) pathZ = PATH_START - 60;
  997.      pos.x = pathX(pathZ); pos.z = pathZ;
  998.      const ahead = 45;
  999.      const dx = pathX(pathZ - ahead) - pos.x, dz = -ahead;
  1000.      const target = Math.atan2(-dx, -dz);
  1001.      yaw += angDiff(target, yaw) * Math.min(1, dt * (locked ? 0.6 : 1.5));
  1002.      if (!locked) pitch += (0.08 - pitch) * Math.min(1, dt * 1.5);
  1003.    } else {
  1004.      let f = 0, s = 0;
  1005.      if (keys.KeyW || keys.ArrowUp) f += 1;
  1006.      if (keys.KeyS || keys.ArrowDown) f -= 1;
  1007.      if (keys.KeyD || keys.ArrowRight) s += 1;
  1008.      if (keys.KeyA || keys.ArrowLeft) s -= 1;
  1009.      const len = Math.hypot(f, s) || 1;
  1010.      f /= len; s /= len;
  1011.      const sy = Math.sin(yaw), cy = Math.cos(yaw);
  1012.      pos.x += (-sy * f + cy * s) * v * dt;
  1013.      pos.z += (-cy * f - sy * s) * v * dt;
  1014.      pos.x = Math.max(RX0 + 20, Math.min(RX1 - 20, pos.x));
  1015.      pos.z = Math.max(RZ0 + 20, Math.min(RZ1 - 20, pos.z));
  1016.    }
  1017.    pos.y = H(pos.x, pos.z) + 1.7;
  1018.    camera.position.copy(pos);
  1019.    camera.rotation.set(pitch, yaw, 0);
  1020.    camera.updateMatrixWorld();
  1021.    sky.position.copy(pos);
  1022.  
  1023.    time += dt;
  1024.    if (anim.windmill) anim.windmill.rotation.z -= dt * 0.9;
  1025.    if (anim.clouds) anim.clouds.rotation.y += dt * 0.004;
  1026.    if (anim.mist) anim.mist.rotation.y += dt * 0.03;
  1027.    for (const b of anim.birds) {
  1028.      const u = b.userData;
  1029.      const a = u.phase + time * u.speed;
  1030.      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);
  1031.      b.rotation.y = -a + (u.speed > 0 ? 0 : Math.PI);
  1032.       const flap = Math.sin(time * u.flap + u.phase) * 0.6;
  1033.       u.l.rotation.z = flap; u.r.rotation.z = -flap;
  1034.     }
  1035.   }
  1036.  
  1037.   function frame() {
  1038.     const dt = Math.min(clock.getDelta(), 0.1);
  1039.     step(dt);
  1040.     renderer.setRenderTarget(rt);
  1041.     renderer.render(scene, camera);
  1042.     renderer.setRenderTarget(null);
  1043.     renderer.render(postScene, postCam);
  1044.  
  1045.     frames++; fpsT += dt; hudT += dt;
  1046.     if (fpsT >= 0.5) { $("fps").textContent = `${Math.round(frames / fpsT)} fps`; frames = 0; fpsT = 0; }
  1047.     if (hudT >= 0.2) {
  1048.       hudT = 0;
  1049.       const d = pos.distanceTo(castleCenter);
  1050.       $("cReal").textContent = fmtDist(d);
  1051.       $("cDrawn").textContent = warp.on ? fmtDist(warpF(d)) : "same";
  1052.     }
  1053.     requestAnimationFrame(frame);
  1054.   }
  1055.  
  1056.   setTimeout(() => {
  1057.     try {
  1058.       buildWorld();
  1059.     } catch (err) {
  1060.       console.error(err);
  1061.       $("loading").innerHTML = '<div class="error">The world failed to build. Reload the page to try again.</div>';
  1062.       return;
  1063.     }
  1064.     resize();
  1065.     syncUI();
  1066.     scene.fog.density = 0.0000037 * opts.haze;
  1067.     $("loading").classList.add("hidden");
  1068.     clock.getDelta();
  1069.     requestAnimationFrame(frame);
  1070.   }, 40);
  1071. })();
  1072. </script>
  1073. </body>
  1074. </html>
  1075.  
Advertisement
Add Comment
Please, Sign In to add comment