1/** 2 * Horizon background â sky cubemap (Rioki-style ray lookup) + procedural ocean below. 3 * Inspired by Rioki's GLSL skybox (https://www.rioki.org/2013/03/07/glsl-skybox.html) 4 * and tuxalin/water-shader (https://github.com/tuxalin/water-shader) â MIT. 5 * 6 * Sky: procedural volumetric clouds (animated). Optional static cubemap if 7 * HORIZON_USE_CUBEMAP_SKY is true (see cubemap.png strip paths below). 8 * Water normal layers: water.jpg + water2.jpg (tiled, parallax-scrolled) 9 */ 10(function (global) { 11 'use strict'; 12 13 /** Tried in order â use whichever filename you upload. */ 14 var CUBEMAP_STRIP_PATHS = [ 15 '/images/backgrounds/horizon/cubemap.png', 16 '/images/backgrounds/horizon/cubemap.webp', 17 '/images/backgrounds/horizon/cubemap.jpg', 18 '/images/backgrounds/horizon/cubemap-strip.webp', 19 '/images/backgrounds/horizon/cubemap-strip.jpg', 20 '/images/backgrounds/horizon/cubemap-strip.png' 21 ]; 22 23 /** Strip slot index â Three.js face index (px, nx, py, ny, pz, nz). Reorder if seams look wrong. */ 24 var STRIP_SLOT_TO_CUBE_FACE = [0, 1, 2, 3, 4, 5]; 25 26 /** Cap GPU face size when slicing huge strips (e.g. 16MB sources). 0 = no cap. */ 27 var STRIP_MAX_FACE_PX = 2048; 28 29 /** Half-angle FOV (tangent). ~0.53 = original; ~0.62 = slightly zoomed out. */ 30 var HORIZON_TAN_HALF_FOV = 0.62; 31 /** Slightly narrow vertical spread (matches old 0.72 vs 1.0 xy feel). */ 32 var HORIZON_VERTICAL_SCALE = 0.88; 33 34 var WATER_NORMAL_PATHS = { 35 layer1: '/images/backgrounds/horizon/water.jpg', 36 layer2: '/images/backgrounds/horizon/water2.jpg' 37 }; 38 /** Set false to sample water2.jpg as the fast parallax layer (must tile seamlessly). */ 39 var WATER_LAYER2_USES_SEAMLESS_FALLBACK = false; 40 41 /** false = animated procedural volumetric clouds; true = static cubemap strip. */ 42 var HORIZON_USE_CUBEMAP_SKY = false;
43 44 /** Cubemap / sky brightness multiplier (reflections use the same sampleSky path). */ 45 var HORIZON_SKY_EXPOSURE = 0.92; 46 var HORIZON_WATER_SKY_REFLECT = 0.97; 47 /** How much normals bend reflections (1 = full; lower = calmer). Textures stay sharp. */ 48 var HORIZON_NORMAL_INFLUENCE = 0.64; 49 /** Procedural swell tilt layered on top of texture normals. */ 50 var HORIZON_WAVE_SWELL = 2.65; 51 /** Rolling wave height on the water plane. */ 52 var HORIZON_WAVE_DISPLACE = 1.05; 53 /** Gerstner horizontal pull (0 = sine height only, ~0.6 = rolling swell). */ 54 var HORIZON_WAVE_STEEPNESS = 0.58; 55 56 var CUBEMAP_FACE_PATHS = [ 57 '/images/backgrounds/horizon/px.webp', 58 '/images/backgrounds/horizon/nx.webp', 59 '/images/backgrounds/horizon/py.webp', 60 '/images/backgrounds/horizon/ny.webp', 61 '/images/backgrounds/horizon/pz.webp', 62 '/images/backgrounds/horizon/nz.webp' 63 ]; 64 65 var CUBEMAP_FACE_FALLBACK_EXTS = ['.jpg', '.png']; 66 67 function applyCubemapColorSpace(tex) { 68 if (tex && THREE.sRGBEncoding !== undefined) { 69 tex.encoding = THREE.sRGBEncoding; 70 } 71 return tex; 72 } 73 74 function loadImage(url) { 75 return new Promise(function (resolve) { 76 var img = new Image(); 77 img.crossOrigin = 'anonymous'; 78 img.onload = function () { resolve(img); }; 79 img.onerror = function () { resolve(null); }; 80 img.src = url; 81 }); 82 } 83 84 function loadFirstImage(paths) { 85 var chain = Promise.resolve(null); 86 for (var i = 0; i < paths.length; i++) { 87 (function (url) { 88 chain = chain.then(function (prev) { 89 if (prev) return prev; 90 return loadImage(url); 91 }); 92 })(paths[i]); 93 } 94 return chain; 95 } 96 97 function cubeTextureFromStripImage(img) { 98 var slots = 6; 99 var faceW = Math.floor(img.width / slots); 100 var faceH = img.height; 101 if (faceW < 1 || faceW * slots !== img.width) { 102 console.warn('[horizon] Cubemap strip width should divide evenly into 6 faces'); 103 return null; 104 } 105 if (Math.abs(faceW - faceH) > 2) { 106 console.warn('[horizon] Cubemap strip faces should be square (height = one face width)'); 107 } 108 109 var outSize = faceW; 110 if (STRIP_MAX_FACE_PX > 0 && outSize > STRIP_MAX_FACE_PX) { 111 outSize = STRIP_MAX_FACE_PX; 112 } 113 114 var faceCanvases = []; 115 for (var s = 0; s < slots; s++) { 116 var canvas = document.createElement('canvas'); 117 canvas.width = outSize; 118 canvas.height = outSize; 119 var ctx = canvas.getContext('2d'); 120 if (ctx) { 121 ctx.drawImage(img, s * faceW, 0, faceW, faceH, 0, 0, outSize, outSize); 122 } 123 faceCanvases.push(canvas); 124 } 125 126 var ordered = []; 127 for (var f = 0; f < 6; f++) { 128 var slot = STRIP_SLOT_TO_CUBE_FACE[f]; 129 ordered.push(faceCanvases[slot >= 0 && slot < 6 ? slot : f]); 130 } 131 132 var tex = new THREE.CubeTexture(ordered); 133 tex.needsUpdate = true; 134 return applyCubemapColorSpace(tex); 135 } 136 137 function loadCubemapFromStrip() { 138 return loadFirstImage(CUBEMAP_STRIP_PATHS).then(function (img) { 139 if (!img || typeof THREE === 'undefined') return null; 140 return cubeTextureFromStripImage(img); 141 }); 142 } 143 144 function loadCubemapFromFaces() { 145 return new Promise(function (resolve) { 146 if (typeof THREE === 'undefined' || !THREE.CubeTextureLoader) { 147 resolve(null); 148 return; 149 } 150 151 function tryPaths(paths) { 152 return new Promise(function (res) { 153 var loader = new THREE.CubeTextureLoader(); 154 loader.load( 155 paths, 156 function (tex) { res(applyCubemapColorSpace(tex)); }, 157 undefined, 158 function () { res(null); } 159 ); 160 }); 161 } 162 163 tryPaths(CUBEMAP_FACE_PATHS).then(function (tex) { 164 if (tex) { 165 resolve(tex); 166 return; 167 } 168 var i = 0; 169 function nextExt() { 170 if (i >= CUBEMAP_FACE_FALLBACK_EXTS.length) { 171 resolve(null); 172 return; 173 } 174 var ext = CUBEMAP_FACE_FALLBACK_EXTS[i]; 175 i += 1; 176 var alt = CUBEMAP_FACE_PATHS.map(function (p) { 177 return p.replace(/\.webp$/i, ext); 178 }); 179 tryPaths(alt).then(function (altTex) { 180 if (altTex) resolve(altTex); 181 else nextExt(); 182 }); 183 } 184 nextExt(); 185 }); 186 }); 187 } 188 189 function applyCubemapToMaterial(tex) { 190 if (!state.material || !tex) return; 191 var prev = state.material.uniforms.uSkybox.value; 192 if (prev && prev.dispose) prev.dispose(); 193 state.material.uniforms.uSkybox.value = tex; 194 state.material.uniforms.uUseCubemap.value = 1; 195 } 196 197 function ensureCubemapLoaded() { 198 if (state.material && state.material.uniforms.uUseCubemap.value > 0.5) { 199 return Promise.resolve(state.material.uniforms.uSkybox.value); 200 } 201 return loadCubemap().then(function (tex) { 202 if (tex) applyCubemapToMaterial(tex); 203 return tex; 204 }); 205 } 206 207 function configureWaterNormalTexture(tex) { 208 if (!tex) return; 209 tex.wrapS = THREE.RepeatWrapping; 210 tex.wrapT = THREE.RepeatWrapping; 211 tex.minFilter = THREE.LinearFilter; 212 tex.magFilter = THREE.LinearFilter; 213 tex.generateMipmaps = false;
214 if (THREE.LinearEncoding !== undefined) { 215 tex.encoding = THREE.LinearEncoding; 216 } 217 } 218 219 function loadWaterNormalTextures() { 220 if (typeof THREE === 'undefined') { 221 return Promise.resolve(false); 222 } 223 if (assetCache.water1) { 224 return Promise.resolve(applyWaterTexturesToMaterial([ 225 assetCache.water1, 226 assetCache.water2 227 ])); 228 } 229 return preloadWaterNormalImages().then(function (images) { 230 return applyWaterTexturesToMaterial(images); 231 }); 232 } 233 234 function ensureWaterNormalsLoaded() { 235 if (state.material && state.material.uniforms.uUseWaterNormals.value > 0.5) { 236 return Promise.resolve(true); 237 } 238 return loadWaterNormalTextures(); 239 } 240 241 function ensureHorizonAssetsLoaded() { 242 var tasks = [ensureWaterNormalsLoaded()]; 243 if (HORIZON_USE_CUBEMAP_SKY) { 244 tasks.push(ensureCubemapLoaded()); 245 } else if (state.material) { 246 state.material.uniforms.uUseCubemap.value = 0; 247 } 248 return Promise.all(tasks); 249 } 250 251 function loadCubemap() { 252 return loadCubemapFromStrip().then(function (stripTex) { 253 if (stripTex) return stripTex; 254 return loadCubemapFromFaces(); 255 }); 256 } 257 258 var VERT = [ 259 'uniform float uAspect;', 260 'uniform float uTanFov;', 261 'uniform float uVerticalScale;', 262 'varying vec3 vRayDir;', 263 'void main() {', 264 ' vec2 uv = position.xy;', 265 ' vRayDir = normalize(vec3(', 266 ' uv.x * uAspect * uTanFov,', 267 ' uv.y * uTanFov * uVerticalScale,', 268 ' 1.0', 269 ' ));', 270 ' gl_Position = vec4(position.xy, 0.0, 1.0);', 271 '}' 272 ].join('\n'); 273 274 var FRAG = [ 275 'precision highp float;', 276 'uniform float uTime;', 277 'uniform float uFadeIn;', 278 'uniform float uHorizon;', 279 'uniform float uUseCubemap;', 280 'uniform samplerCube uSkybox;', 281 'uniform sampler2D uWaterNormal1;', 282 'uniform sampler2D uWaterNormal2;', 283 'uniform float uUseWaterNormals;', 284 'uniform float uUseWaterNormal2;', 285 'uniform float uSkyExposure;', 286 'uniform float uWaterSkyReflect;', 287 'uniform float uNormalInfluence;', 288 'uniform float uWaveSwell;', 289 'uniform float uWaveDisplace;', 290 'uniform float uWaveSteepness;', 291 'uniform float uShaderQuality;', 292 'uniform vec2 uResolution;', 293 'varying vec3 vRayDir;', 294 295 'float hash(vec2 p) {', 296 ' return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);', 297 '}', 298 299 'float noise(vec2 p) {', 300 ' vec2 i = floor(p);', 301 ' vec2 f = fract(p);', 302 ' f = f * f * (3.0 - 2.0 * f);', 303 ' float a = hash(i);', 304 ' float b = hash(i + vec2(1.0, 0.0));', 305 ' float c = hash(i + vec2(0.0, 1.0));', 306 ' float d = hash(i + vec2(1.0, 1.0));', 307 ' return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);', 308 '}', 309 310 'float fbm(vec2 p) {', 311 ' float v = 0.0;', 312 ' float a = 0.5;', 313 ' mat2 rot = mat2(0.8, -0.6, 0.6, 0.8);', 314 ' for (int i = 0; i < 5; i++) {', 315 ' v += a * noise(p);', 316 ' p = rot * p * 2.02 + 17.0;', 317 ' a *= 0.5;', 318 ' }', 319 ' return v;', 320 '}', 321 322 'float hash31(vec3 p) {', 323 ' p = fract(p * 0.1031);', 324 ' p += dot(p, p.yzx + 33.33);', 325 ' return fract((p.x + p.y) * p.z);', 326 '}', 327 328 'float noise3(vec3 p) {', 329 ' vec3 i = floor(p);', 330 ' vec3 f = fract(p);', 331 ' f = f * f * (3.0 - 2.0 * f);', 332 ' float n000 = hash31(i);', 333 ' float n100 = hash31(i + vec3(1.0, 0.0, 0.0));', 334 ' float n010 = hash31(i + vec3(0.0, 1.0, 0.0));', 335 ' float n110 = hash31(i + vec3(1.0, 1.0, 0.0));', 336 ' float n001 = hash31(i + vec3(0.0, 0.0, 1.0));', 337 ' float n101 = hash31(i + vec3(1.0, 0.0, 1.0));', 338 ' float n011 = hash31(i + vec3(0.0, 1.0, 1.0));', 339 ' float n111 = hash31(i + vec3(1.0, 1.0, 1.0));', 340 ' float nx00 = mix(n000, n100, f.x);', 341 ' float nx10 = mix(n010, n110, f.x);', 342 ' float nx01 = mix(n001, n101, f.x);', 343 ' float nx11 = mix(n011, n111, f.x);', 344 ' float nxy0 = mix(nx00, nx10, f.y);', 345 ' float nxy1 = mix(nx01, nx11, f.y);', 346 ' return mix(nxy0, nxy1, f.z);', 347 '}', 348 349 'float fbm3(vec3 p) {', 350 ' float v = 0.0;', 351 ' float a = 0.5;', 352 ' for (int i = 0; i < 4; i++) {', 353 ' v += a * noise3(p);', 354 ' p = p * 2.04 + vec3(17.0, 23.0, 11.0);', 355 ' a *= 0.5;', 356 ' }', 357 ' return v;', 358 '}', 359 360 'vec3 horizonSunDir() {', 361 ' return normalize(vec3(0.28, 0.78, 0.18));', 362 '}', 363 364 'float cloudDensity(vec3 p) {', 365 ' float wind = uTime * 0.022;', 366 ' p.xz += vec2(wind, wind * 0.55);', 367 ' p.x += sin(uTime * 0.06 + p.z * 0.35) * 0.06;', 368 ' float shape = fbm3(p * 0.48);', 369 ' float detail = fbm3(p * 1.55 + vec3(0.0, uTime * 0.035, 0.0)) * 0.38;', 370 ' float d = shape + detail;', 371 ' float heightMask = smoothstep(0.05, 1.4, p.y);', 372 ' d *= heightMask;', 373 ' return smoothstep(0.28, 0.58, d);', 374 '}', 375 376 'float skyCloudBillboards(vec3 rd) {', 377 ' if (rd.y < 0.04) return 0.0;', 378 ' vec2 uv = rd.xz / max(rd.y + 0.14, 0.1);', 379 ' uv += vec2(uTime * 0.014, uTime * 0.007);', 380 ' float base = fbm(uv * 0.72 + vec2(uTime * 0.003, 0.0));', 381 ' float detail = fbm(uv * 1.85 + vec2(-uTime * 0.006, uTime * 0.004));', 382 ' float billow = smoothstep(0.34, 0.62, base * 0.72 + detail * 0.38);', 383 ' billow *= smoothstep(0.05, 0.72, rd.y);', 384 ' return billow;', 385 '}', 386 387 'vec3 skyGradient(vec3 rd) {', 388 ' vec3 sunDir = horizonSunDir();', 389 ' float t = clamp(rd.y * 0.5 + 0.5, 0.0, 1.0);', 390 ' vec3 zenith = vec3(0.16, 0.38, 0.72);', 391 ' vec3 horizonCol = vec3(0.82, 0.68, 0.58);', 392 ' vec3 base = mix(horizonCol, zenith, pow(t, 0.72));', 393 ' float sunDisk = pow(max(dot(rd, sunDir), 0.0), 160.0);', 394 ' float sunHalo = pow(max(dot(rd, sunDir), 0.0), 10.0);', 395 ' base += vec3(1.0, 0.92, 0.8) * (sunDisk * 0.75 + sunHalo * 0.06);', 396 ' return base;', 397 '}', 398 399 'vec3 volumetricSky(vec3 rd) {', 400 ' vec3 sunDir = horizonSunDir();', 401 ' vec3 col = skyGradient(rd);', 402 ' vec3 cloudLit = vec3(0.94, 0.96, 1.0);', 403 ' vec3 cloudShade = vec3(0.52, 0.6, 0.74);',
404 ' float billows = skyCloudBillboards(rd) * clamp(uShaderQuality * 1.35, 0.0, 1.0);', 405 ' col = mix(col, cloudLit, billows * 0.88);', 406 ' if (rd.y <= 0.02 || uShaderQuality < 0.22) return col;', 407 ' float transmittance = 1.0;', 408 ' float marchT = 0.55;', 409 ' int marchSteps = int(mix(2.0, 5.0, clamp(uShaderQuality, 0.0, 1.0) + 0.001));', 410 ' for (int i = 0; i < 5; i++) {', 411 ' if (i >= marchSteps) break;', 412 ' vec3 p = rd * marchT + vec3(0.0, 0.18, 0.0);', 413 ' float dens = cloudDensity(p);', 414 ' float lightAmt = 0.42 + 0.58 * max(dot(normalize(p + vec3(0.001)), sunDir), 0.0);', 415 ' float shadow = cloudDensity(p + sunDir * 0.38);', 416 ' lightAmt *= 1.0 - shadow * 0.5;', 417 ' vec3 scatter = mix(cloudShade, cloudLit, lightAmt);', 418 ' col = mix(col, scatter, dens * transmittance * 0.38);', 419 ' transmittance *= 1.0 - dens * 0.16;', 420 ' marchT += 0.32;', 421 ' }', 422 ' return col;', 423 '}', 424 425 'vec3 proceduralSky(vec3 rd) {', 426 ' return volumetricSky(normalize(rd));', 427 '}', 428 429 'vec3 sampleSky(vec3 rd) {', 430 ' vec3 c = proceduralSky(normalize(rd));', 431 ' if (uUseCubemap > 0.5) {', 432 ' c = textureCube(uSkybox, vec3(rd.x, rd.y, -rd.z)).rgb;', 433 ' c = pow(c, vec3(1.0 / 2.2));', 434 ' }', 435 ' return c * uSkyExposure;', 436 '}', 437 438 'void gerstnerWave(vec2 xz, float t, vec2 dir, float amp, float waveLen, float speed, float steep,', 439 ' inout vec3 disp, inout vec3 tanX, inout vec3 tanZ) {', 440 ' float k = 6.2831853 / max(waveLen, 0.35);', 441 ' float w = k * (dot(dir, xz) - speed * t);', 442 ' float s = sin(w);', 443 ' float c = cos(w);', 444 ' float qa = steep * amp;', 445 ' disp.x += dir.x * qa * c;', 446 ' disp.y += amp * s;', 447 ' disp.z += dir.y * qa * c;', 448 ' float wa = k * amp;', 449 ' float qwa = steep * wa;', 450 ' tanX += vec3(-qwa * dir.x * dir.x * c, dir.x * wa * s, -qwa * dir.x * dir.y * c);', 451 ' tanZ += vec3(-qwa * dir.x * dir.y * c, dir.y * wa * s, -qwa * dir.y * dir.y * c);', 452 '}', 453 454 'void gerstnerOcean(vec2 xz, float t, out vec3 disp, out vec3 n, out float slope) {', 455 ' disp = vec3(0.0);', 456 ' vec3 tanX = vec3(1.0, 0.0, 0.0);', 457 ' vec3 tanZ = vec3(0.0, 0.0, 1.0);', 458 ' float steep = clamp(uWaveSteepness, 0.0, 1.15);', 459 ' gerstnerWave(xz, t, normalize(vec2(0.84, 0.54)), 0.46, 10.5, 0.62, steep, disp, tanX, tanZ);', 460 ' gerstnerWave(xz, t, normalize(vec2(-0.48, 0.88)), 0.30, 6.8, 0.76, steep * 0.92, disp, tanX, tanZ);', 461 ' gerstnerWave(xz, t, normalize(vec2(0.32, -0.95)), 0.18, 4.1, 1.02, steep * 0.78, disp, tanX, tanZ);', 462 ' gerstnerWave(xz, t, normalize(vec2(-0.68, -0.73)), 0.09, 2.0, 1.28, steep * 0.62, disp, tanX, tanZ);', 463 ' gerstnerWave(xz, t, normalize(vec2(0.12, 0.99)), 0.045, 1.1, 1.55, steep * 0.45, disp, tanX, tanZ);', 464 ' n = normalize(cross(tanZ, tanX));', 465 ' slope = 1.0 - clamp(n.y, 0.0, 1.0);', 466 '}', 467 468 'vec3 proceduralWaveNormal(vec2 xz) {', 469 ' vec3 disp;', 470 ' vec3 n;', 471 ' float slope;', 472 ' gerstnerOcean(xz, uTime, disp, n, slope);', 473 ' return normalize(vec3(n.xy * uWaveSwell, max(n.z, 0.12)));', 474 '}', 475 476 'vec3 unpackNormalMap(vec3 rgb) {', 477 ' return normalize(rgb * 2.0 - 1.0);', 478 '}', 479 480 'vec3 applyNormalInfluence(vec3 n, float influence) {', 481 ' return normalize(mix(vec3(0.0, 1.0, 0.0), n, influence));', 482 '}', 483 484 'vec3 waterNormalFromTextures(vec2 xz, vec3 rd) {', 485 ' vec2 parallax = rd.xz / max(abs(rd.y), 0.06);', 486 ' vec2 base = xz * 0.11;', 487 ' vec2 uv1 = base * 1.15 + parallax * 0.04 + vec2(uTime * 0.011, uTime * 0.006);', 488 ' vec2 uv2 = base * 2.35 + parallax * 0.095 + vec2(-uTime * 0.019, uTime * 0.014);', 489 ' vec3 n1 = unpackNormalMap(texture2D(uWaterNormal1, uv1).rgb);', 490 ' vec3 n2From1 = unpackNormalMap(texture2D(uWaterNormal1, uv2).rgb);', 491 ' vec3 n2From2 = unpackNormalMap(texture2D(uWaterNormal2, uv2).rgb);', 492 ' vec3 n2 = mix(n2From1, n2From2, step(0.5, uUseWaterNormal2));', 493 ' return normalize(vec3(n1.xy + n2.xy, n1.z * n2.z));', 494 '}', 495 496 'vec3 waveNormal(vec2 xz, vec3 rd) {', 497 ' vec3 proc = proceduralWaveNormal(xz);', 498 ' vec3 n = proc;', 499 ' if (uUseWaterNormals > 0.5) {', 500 ' vec3 texN = waterNormalFromTextures(xz, rd);', 501 ' n = normalize(vec3(texN.xy + proc.xy * uWaveSwell, texN.z * proc.z));', 502 ' }', 503 ' return applyNormalInfluence(n, uNormalInfluence);', 504 '}', 505 506 'vec3 shadeWater(vec3 rd, vec3 eye) {', 507 ' vec3 col = sampleSky(rd);', 508 ' if (rd.y < -0.001) {', 509 ' float t = -eye.y / rd.y;', 510 ' if (t > 0.0) {', 511 ' vec3 hit = eye + rd * t;', 512 ' vec2 xz = hit.xz * 0.35;', 513 ' float grazing = smoothstep(-0.3, -0.012, rd.y);', 514 ' float distFade = smoothstep(2.5, 20.0, length(xz));', 515 ' float horizonFade = clamp(max(grazing, distFade * 0.88), 0.0, 1.0);', 516 ' vec3 gerstnerDisp;', 517 ' vec3 procN;', 518 ' float waveSlope;', 519 ' gerstnerOcean(xz, uTime, gerstnerDisp, procN, waveSlope);', 520 ' float dispFade = 1.0 - grazing * 0.72;', 521 ' vec2 xzWaves = xz + gerstnerDisp.xz * uWaveSteepness * dispFade;', 522 ' hit.y += gerstnerDisp.y * uWaveDisplace * dispFade;', 523 ' gerstnerOcean(xzWaves, uTime, gerstnerDisp, procN, waveSlope);', 524 ' procN = normalize(vec3(procN.xy * uWaveSwell, max(procN.z, 0.1)));', 525 ' vec3 n = waveNormal(xzWaves, rd);', 526 ' n = normalize(mix(n, procN, 0.55));', 527 ' n = normalize(mix(n, vec3(0.0, 1.0, 0.0), horizonFade * 0.76));', 528 ' vec3 refl = reflect(rd, n);', 529 ' vec3 skyRefl = sampleSky(refl);', 530 ' vec3 mirrorSky = sampleSky(normalize(vec3(rd.x, max(-rd.y, 0.001), rd.z)));', 531 ' skyRefl = mix(skyRefl, mirrorSky, horizonFade * 0.82);', 532 ' float fresnel = pow(1.0 - max(dot(-rd, n), 0.0), 2.25);', 533 ' fresnel = clamp(fresnel + 0.4, 0.0, 1.0);', 534 ' fresnel = mix(fresnel, 1.0, horizonFade * 0.96);', 535 ' vec3 shallow = mirrorSky * 0.38 + vec3(0.03, 0.04, 0.05);', 536 ' vec3 deep = mirrorSky * 0.16 + vec3(0.01, 0.025, 0.04);', 537 ' float depth = clamp(length(xz) * 0.055, 0.0, 1.0);', 538 ' vec3 waterBody = mix(shallow, deep, depth * 0.55);', 539 ' vec3 sunDir = horizonSunDir();', 540 ' float swellShade = clamp(dot(procN, sunDir) * 0.42 + 0.94, 0.82, 1.14);', 541 ' float crest = smoothstep(0.22, 0.58, waveSlope + procN.y * 0.35);', 542 ' float trough = smoothstep(0.42, 0.08, procN.y);', 543 ' float spec = pow(max(dot(refl, sunDir), 0.0), 68.0);', 544 ' spec *= (1.0 + crest * 2.8) * (1.0 - horizonFade * 0.85);', 545 ' col = mix(waterBody, skyRefl, fresnel * uWaterSkyReflect);', 546 ' col *= mix(1.0, swellShade, (1.0 - horizonFade) * 0.62);', 547 ' col = mix(col, mirrorSky, horizonFade * 0.55);', 548 ' col += skyRefl * fresnel * 0.07 * (1.0 - horizonFade);', 549 ' col += vec3(1.0, 0.95, 0.85) * spec * 0.58;', 550 ' col += mix(mirrorSky, vec3(1.0), 0.42) * crest * 0.16 * (1.0 - horizonFade);', 551 ' col *= mix(1.0, 0.9, trough * 0.35 * (1.0 - horizonFade));', 552 ' }', 553 ' }', 554 ' return col;', 555 '}', 556 557 'void main() {', 558 ' vec2 uv = gl_FragCoord.xy / uResolution;', 559 ' vec3 rd = normalize(vRayDir);', 560 ' vec3 eye = vec3(0.0, 1.65, -2.4);', 561 ' vec3 skyCol = sampleSky(rd);', 562 ' vec3 waterCol = shadeWater(rd, eye);', 563 ' float blend = smoothstep(uHorizon - 0.045, uHorizon + 0.014, uv.y);', 564 ' vec3 col = mix(waterCol, skyCol, blend);', 565 ' float horizonGlow = exp(-abs(uv.y - uHorizon) * 34.0) * 0.045;', 566 ' col += vec3(1.0, 0.88, 0.72) * horizonGlow;', 567 ' gl_FragColor = vec4(col, uFadeIn);', 568 '}' 569 ].join('\n'); 570 571 var state = { 572 renderer: null, 573 material: null, 574 scene: null, 575 camera: null, 576 raf: 0, 577 running: false, 578 overlay: null, 579 warmupFrames: 0, 580 targetPixelRatio: 1, 581 tuning: null 582 }; 583 584 var assetCache = { 585 water1: null, 586 water2: null, 587 waterPromise: null 588 }; 589 590 function qualityToShaderUniform(quality) { 591 var q = Math.max(0, Math.min(100, Number(quality) || 40)); 592 return q / 100; 593 } 594 595 function resolutionScaleToRatio(scale) { 596 var s = Math.max(25, Math.min(100, Number(scale) || 45)); 597 return s / 100; 598 } 599 600 function readHorizonTuningValues() { 601 return state.tuning || readHorizonTuning(); 602 } 603 604 function computeTargetPixelRatio() { 605 var tuning = readHorizonTuningValues(); 606 var dpr; 607 if (global.PupwifeViewportSize && typeof global.PupwifeViewportSize.pixelRatio === 'function') { 608 dpr = global.PupwifeViewportSize.pixelRatio(2); 609 } else { 610 dpr = Math.min(window.devicePixelRatio || 1, 2); 611 } 612 return Math.max(0.5, dpr * resolutionScaleToRatio(tuning.resolutionScale)); 613 } 614 615 function shouldDeferWaterLayer2() { 616 var tuning = readHorizonTuningValues(); 617 return Number(tuning.shaderQuality) < 55; 618 } 619 620 function yieldToMain() { 621 return new Promise(function (resolve) { 622 if (typeof requestAnimationFrame === 'function') { 623 requestAnimationFrame(function () { 624 setTimeout(resolve, 0); 625 }); 626 } else { 627 setTimeout(resolve, 0); 628 } 629 }); 630 } 631 632 function textureFromImage(img) { 633 if (!img || typeof THREE === 'undefined') return null; 634 var tex = new THREE.Texture(img); 635 tex.needsUpdate = true; 636 configureWaterNormalTexture(tex); 637 return tex; 638 } 639 640 function preloadWaterNormalImages() { 641 if (assetCache.waterPromise) return assetCache.waterPromise; 642 var deferLayer2 = shouldDeferWaterLayer2(); 643 assetCache.waterPromise = loadImage(WATER_NORMAL_PATHS.layer1).then(function (layer1) { 644 if (layer1) assetCache.water1 = layer1; 645 if (deferLayer2 || WATER_LAYER2_USES_SEAMLESS_FALLBACK) { 646 return [layer1, null]; 647 } 648 return loadImage(WATER_NORMAL_PATHS.layer2).then(function (layer2) { 649 if (layer2) assetCache.water2 = layer2; 650 return [layer1, layer2]; 651 }); 652 }); 653 return assetCache.waterPromise; 654 } 655 656 function preloadWaterLayer2IfNeeded() { 657 if (assetCache.water2 || WATER_LAYER2_USES_SEAMLESS_FALLBACK) { 658 return Promise.resolve(assetCache.water2); 659 } 660 return loadImage(WATER_NORMAL_PATHS.layer2).then(function (layer2) { 661 if (layer2) { 662 assetCache.water2 = layer2; 663 if (state.material && state.material.uniforms.uUseWaterNormals.value > 0.5) { 664 applyWaterTexturesToMaterial([assetCache.water1, layer2]); 665 } 666 } 667 return layer2; 668 }); 669 } 670 671 function applyWaterTexturesToMaterial(textures) { 672 if (!state.material || !textures[0]) return false;
673 var tex1 = textureFromImage(textures[0]); 674 var tex2 = textures[1] ? textureFromImage(textures[1]) : null; 675 if (!tex1) return false; 676 var u = state.material.uniforms; 677 if (u.uWaterNormal1.value && u.uWaterNormal1.value.dispose) { 678 u.uWaterNormal1.value.dispose(); 679 } 680 if (u.uWaterNormal2.value && u.uWaterNormal2.value.dispose) { 681 u.uWaterNormal2.value.dispose(); 682 } 683 u.uWaterNormal1.value = tex1; 684 u.uWaterNormal2.value = tex2 || tex1; 685 u.uUseWaterNormals.value = 1; 686 u.uUseWaterNormal2.value = tex2 ? 1 : 0; 687 scheduleWaterMipmapBuild(tex1); 688 if (tex2) scheduleWaterMipmapBuild(tex2); 689 return true; 690 } 691 692 function scheduleWaterMipmapBuild(tex) { 693 if (!tex) return; 694 var build = function () { 695 if (!tex || !state.material) return; 696 tex.generateMipmaps = true; 697 tex.minFilter = THREE.LinearMipmapLinearFilter; 698 tex.needsUpdate = true; 699 }; 700 if (typeof requestIdleCallback === 'function') { 701 requestIdleCallback(build, { timeout: 1200 }); 702 } else { 703 setTimeout(build, 48); 704 } 705 } 706 707 function hideCausticsCanvas() { 708 var overlay = document.getElementById('webgl-overlay'); 709 if (!overlay) return; 710 overlay.querySelectorAll('canvas').forEach(function (canvas) { 711 if (canvas.dataset.siteBg !== 'horizon') { 712 canvas.style.display = 'none'; 713 } 714 }); 715 } 716 717 function hide() { 718 if (state.renderer && state.renderer.domElement) { 719 state.renderer.domElement.style.display = 'none'; 720 } 721 } 722 723 function stopLoop() { 724 state.running = false; 725 if (state.raf) { 726 cancelAnimationFrame(state.raf); 727 state.raf = 0; 728 } 729 } 730 731 function destroy(removeCanvas) { 732 stopLoop(); 733 if (state.renderer) { 734 if (state.material) { 735 var u = state.material.uniforms; 736 if (u.uSkybox.value && u.uSkybox.value.dispose) u.uSkybox.value.dispose(); 737 if (u.uWaterNormal1.value && u.uWaterNormal1.value.dispose) u.uWaterNormal1.value.dispose(); 738 if (u.uWaterNormal2.value && u.uWaterNormal2.value.dispose) u.uWaterNormal2.value.dispose(); 739 state.material.dispose(); 740 } 741 if (removeCanvas !== false && state.renderer.domElement.parentNode) { 742 state.renderer.domElement.parentNode.removeChild(state.renderer.domElement); 743 } 744 state.renderer.dispose(); 745 } 746 state.renderer = null; 747 state.material = null; 748 state.scene = null; 749 state.camera = null; 750 delete global.__webglHorizonInitialized; 751 delete global.__horizonBackgroundReady; 752 } 753 754 function readViewportCss() { 755 if (global.PupwifeViewportSize && typeof global.PupwifeViewportSize.css === 'function') { 756 return global.PupwifeViewportSize.css(); 757 } 758 return { width: global.innerWidth, height: global.innerHeight }; 759 } 760 761 function resize() { 762 if (!state.renderer || !state.material) return; 763 var vp = readViewportCss(); 764 var w = vp.width; 765 var h = vp.height; 766 state.targetPixelRatio = computeTargetPixelRatio(); 767 state.renderer.setSize(w, h); 768 if (state.warmupFrames >= 4) { 769 state.renderer.setPixelRatio(state.targetPixelRatio); 770 } 771 state.material.uniforms.uResolution.value.set(w, h); 772 state.material.uniforms.uAspect.value = w / Math.max(h, 1); 773 var horizon = 0.38 + (h / Math.max(w, 1)) * 0.04; 774 state.material.uniforms.uHorizon.value = Math.min(0.52, Math.max(0.32, horizon)); 775 } 776 777 function tick(startTime) { 778 if (!state.running || !state.material || !state.renderer) return; 779 var t = (performance.now() - startTime) * 0.001; 780 state.material.uniforms.uTime.value = t; 781 if (state.material.uniforms.uFadeIn.value < 1) { 782 state.material.uniforms.uFadeIn.value = Math.min(1, state.material.uniforms.uFadeIn.value + 0.012); 783 } 784 if (state.warmupFrames < 4) { 785 var ramp = 0.35 + state.warmupFrames * 0.22; 786 state.renderer.setPixelRatio(Math.max(1, state.targetPixelRatio * ramp)); 787 state.warmupFrames += 1; 788 } 789 state.renderer.render(state.scene, state.camera); 790 state.raf = requestAnimationFrame(function () { tick(startTime); }); 791 } 792 793 function warmCompileShader() { 794 if (!state.renderer || !state.scene || !state.camera) return Promise.resolve(); 795 state.renderer.setPixelRatio(1); 796 state.renderer.setSize(2, 2, false); 797 state.renderer.render(state.scene, state.camera); 798 return yieldToMain(); 799 } 800 801 function buildScene() {
802 var overlay = document.getElementById('webgl-overlay'); 803 if (!overlay || overlay.dataset.disableCanvas === 'true') { 804 return Promise.resolve(); 805 } 806 if (typeof THREE === 'undefined') { 807 console.warn('webglHorizonScene: THREE is not loaded'); 808 return Promise.resolve(); 809 } 810 811 hideCausticsCanvas(); 812 813 var existing = overlay.querySelector('canvas[data-site-bg="horizon"]'); 814 if (existing && state.renderer && state.renderer.domElement === existing) { 815 global.__webglHorizonInitialized = true; 816 existing.style.display = ''; 817 state.running = true; 818 tick(performance.now()); 819 markHorizonReady(); 820 return finishHorizonAssets(); 821 } 822 823 destroy(false); 824 825 global.__webglHorizonInitialized = true; 826 state.overlay = overlay; 827 state.warmupFrames = 0; 828 state.tuning = readHorizonTuning(); 829 state.targetPixelRatio = computeTargetPixelRatio(); 830 831 var initVp = readViewportCss(); 832 var scene = new THREE.Scene(); 833 var camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); 834 var renderer = new THREE.WebGLRenderer({ alpha: true, antialias: false, powerPreference: 'high-performance' }); 835 renderer.setClearColor(0x000000, 0); 836 renderer.setPixelRatio(1); 837 renderer.domElement.dataset.siteBg = 'horizon'; 838 overlay.appendChild(renderer.domElement); 839 840 var material = new THREE.ShaderMaterial({ 841 transparent: true, 842 depthWrite: false, 843 uniforms: { 844 uTime: { value: 0 }, 845 uFadeIn: { value: 0 }, 846 uHorizon: { value: 0.42 }, 847 uUseCubemap: { value: 0 }, 848 uSkybox: { value: null }, 849 uWaterNormal1: { value: null }, 850 uWaterNormal2: { value: null }, 851 uUseWaterNormals: { value: 0 }, 852 uUseWaterNormal2: { value: 0 }, 853 uSkyExposure: { value: HORIZON_SKY_EXPOSURE }, 854 uWaterSkyReflect: { value: HORIZON_WATER_SKY_REFLECT }, 855 uNormalInfluence: { value: HORIZON_NORMAL_INFLUENCE }, 856 uWaveSwell: { value: HORIZON_WAVE_SWELL }, 857 uWaveDisplace: { value: HORIZON_WAVE_DISPLACE }, 858 uWaveSteepness: { value: HORIZON_WAVE_STEEPNESS }, 859 uShaderQuality: { value: qualityToShaderUniform(readHorizonTuning().shaderQuality) }, 860 uResolution: { value: new THREE.Vector2(initVp.width, initVp.height) }, 861 uAspect: { value: initVp.width / Math.max(initVp.height, 1) }, 862 uTanFov: { value: HORIZON_TAN_HALF_FOV }, 863 uVerticalScale: { value: HORIZON_VERTICAL_SCALE } 864 }, 865 vertexShader: VERT, 866 fragmentShader: FRAG 867 }); 868 869 var mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material); 870 scene.add(mesh); 871 872 state.scene = scene; 873 state.camera = camera; 874 state.renderer = renderer; 875 state.material = material; 876 applyHorizonTuning(readHorizonTuning()); 877 878 resize(); 879 if (!state.__resizeBound) { 880 state.__resizeBound = true; 881 window.addEventListener('resize', resize); 882 } 883 884 return warmCompileShader().then(function () { 885 resize(); 886 state.running = true; 887 tick(performance.now()); 888 markHorizonReady(); 889 return finishHorizonAssets(); 890 }); 891 } 892 893 function init() { 894 if (global.__webglHorizonInitialized && state.renderer) { 895 hideCausticsCanvas(); 896 if (state.renderer.domElement) state.renderer.domElement.style.display = ''; 897 state.running = true; 898 tick(performance.now()); 899 markHorizonReady(); 900 return finishHorizonAssets(); 901 } 902 903 return yieldToMain().then(buildScene); 904 } 905 906 function markHorizonReady() { 907 if (global.__horizonBackgroundReady) return;
908 global.__horizonBackgroundReady = true; 909 var queue = global.__horizonBackgroundWhenReadyQueue; 910 if (queue && queue.length) { 911 for (var i = 0; i < queue.length; i++) { 912 try { queue[i](); } catch (e) { /* ignore */ } 913 } 914 global.__horizonBackgroundWhenReadyQueue = null; 915 } 916 try { 917 global.dispatchEvent(new CustomEvent('pupwife:horizon-background-ready')); 918 } catch (err) { 919 global.dispatchEvent(new Event('pupwife:horizon-background-ready')); 920 } 921 } 922 923 function whenReady() { 924 if (global.__horizonBackgroundReady) return Promise.resolve(); 925 return new Promise(function (resolve) { 926 if (global.__horizonBackgroundReady) { 927 resolve(); 928 return; 929 } 930 global.__horizonBackgroundWhenReadyQueue = global.__horizonBackgroundWhenReadyQueue || []; 931 global.__horizonBackgroundWhenReadyQueue.push(resolve); 932 global.addEventListener('pupwife:horizon-background-ready', function onReady() { 933 global.removeEventListener('pupwife:horizon-background-ready', onReady); 934 resolve(); 935 }); 936 }); 937 } 938 939 function completeHorizonInit() { 940 return finishHorizonAssets(); 941 } 942 943 function finishHorizonAssets() { 944 return ensureHorizonAssetsLoaded().then(function () { 945 return yieldToMain(); 946 }).then(function () { 947 if (state.renderer && state.scene && state.camera) { 948 resize(); 949 state.renderer.render(state.scene, state.camera); 950 } 951 if (shouldDeferWaterLayer2()) { 952 return preloadWaterLayer2IfNeeded().catch(function () { return null; }); 953 } 954 }); 955 } 956 957 function readHorizonTuning() { 958 if (global.SiteBackgroundTuning && typeof global.SiteBackgroundTuning.readTuning === 'function') { 959 return global.SiteBackgroundTuning.readTuning().horizon; 960 } 961 return { 962 skyExposure: HORIZON_SKY_EXPOSURE, 963 waterSkyReflect: HORIZON_WATER_SKY_REFLECT, 964 normalInfluence: HORIZON_NORMAL_INFLUENCE, 965 waveSwell: HORIZON_WAVE_SWELL, 966 waveDisplace: HORIZON_WAVE_DISPLACE, 967 waveSteepness: HORIZON_WAVE_STEEPNESS, 968 fieldOfView: HORIZON_TAN_HALF_FOV, 969 verticalScale: HORIZON_VERTICAL_SCALE, 970 shaderQuality: 40, 971 resolutionScale: 45 972 }; 973 } 974 975 function applyHorizonTuning(tuning) { 976 if (global.SiteBackgroundTuning && typeof global.SiteBackgroundTuning.mergeSettings === 'function') { 977 tuning = global.SiteBackgroundTuning.mergeSettings({ horizon: tuning }).horizon; 978 } 979 state.tuning = tuning; 980 if (!state.material || !state.material.uniforms) return; 981 var u = state.material.uniforms; 982 u.uSkyExposure.value = tuning.skyExposure; 983 u.uWaterSkyReflect.value = tuning.waterSkyReflect; 984 u.uNormalInfluence.value = tuning.normalInfluence; 985 u.uWaveSwell.value = tuning.waveSwell; 986 u.uWaveDisplace.value = tuning.waveDisplace; 987 u.uWaveSteepness.value = tuning.waveSteepness; 988 u.uTanFov.value = tuning.fieldOfView; 989 u.uVerticalScale.value = tuning.verticalScale; 990 u.uShaderQuality.value = qualityToShaderUniform(tuning.shaderQuality); 991 resize(); 992 } 993 994 function preload() { 995 var tasks = [preloadWaterNormalImages()]; 996 if (HORIZON_USE_CUBEMAP_SKY) { 997 tasks.push(loadCubemap()); 998 } 999 return Promise.all(tasks).catch(function () { return null; }); 1000 } 1001 1002 global.HorizonBackground = { 1003 init: init, 1004 preload: preload, 1005 whenReady: whenReady, 1006 applyTuning: applyHorizonTuning, 1007 destroy: destroy, 1008 stop: stopLoop, 1009 hide: hide 1010 }; 1011})(typeof window !== 'undefined' ? window : this);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.