yaosio

Another Claude attempt at a 2D backdrop of 3D objects.

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