diff --git a/Dockerfile b/Dockerfile index 57898ab..9ba5f6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ COPY config.js /usr/share/nginx/html/config.js COPY i18n.js /usr/share/nginx/html/i18n.js COPY background.html /usr/share/nginx/html/background.html COPY noise.png /usr/share/nginx/html/noise.png +COPY ground.jpg /usr/share/nginx/html/ground.jpg COPY vendor/ /usr/share/nginx/html/vendor/ EXPOSE 58881 CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/ground.jpg b/ground.jpg new file mode 100644 index 0000000..4d27cd3 Binary files /dev/null and b/ground.jpg differ diff --git a/ground.png b/ground.png new file mode 100644 index 0000000..1a31df1 Binary files /dev/null and b/ground.png differ diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index 5348fb2..21d7fde 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -53,6 +53,7 @@ var FRAG_SRC = [ 'precision highp float;', 'uniform vec2 uResolution;', 'uniform float uTime;', + 'uniform sampler2D uGround;', 'uniform vec3 uSky;', 'uniform vec3 uCloud;', 'uniform vec3 uCloudShadow;', @@ -194,6 +195,18 @@ var FRAG_SRC = [ // remain visible against a deep but not void-black sky. 'float skyDarkness = nightFactor * 0.55;', 'col = mix(col, col * (1.0 - skyDarkness), 1.0 - cloud);', + '// Horizon: when looking down (rd.y < 0), sample the satellite + // ground texture based on ray direction. Use the horizontal + // component as a UV — the further off-axis, the more the + // texture scrolls. Adds parallax-like feel even though the + // ground is a flat image. + 'float gmask = smoothstep(-0.02, -0.20, rd.y);', + 'vec2 gUv = vec2(rd.x * 0.5 + 0.5, -rd.y * 0.5 + 0.3);', + 'vec3 groundCol = texture2D(uGround, gUv).rgb;', + // Distance fog: distant ground fades toward sky colour. + 'float fog = clamp(-rd.y * 1.5, 0.0, 1.0);', + 'groundCol = mix(groundCol, uBg, fog * 0.6);', + 'col = mix(col, groundCol, gmask);', 'gl_FragColor = vec4(col, 1.0);', '}' ].join('\n'); @@ -216,6 +229,7 @@ function init(canvas) { var aPos = gl.getAttribLocation(prog, 'aPos'); var uRes = gl.getUniformLocation(prog, 'uResolution'); var uTime = gl.getUniformLocation(prog, 'uTime'); + var uGround = gl.getUniformLocation(prog, 'uGround'); var uSky = gl.getUniformLocation(prog, 'uSky'); var uCloud = gl.getUniformLocation(prog, 'uCloud'); var uCloudShadow = gl.getUniformLocation(prog, 'uCloudShadow'); @@ -228,13 +242,37 @@ function init(canvas) { return { gl: gl, prog: prog, buf: buf, aPos: aPos, - uRes: uRes, uTime: uTime, uSky: uSky, uCloud: uCloud, + uRes: uRes, uTime: uTime, uGround: uGround, uSky: uSky, uCloud: uCloud, uCloudShadow: uCloudShadow, uLight: uLight, uBg: uBg, uSpeed: uSpeed, uDensity: uDensity, uDayProgress: uDayProgress, uSunAlt: uSunAlt }; } +// Load an image and upload it as a CLAMP_TO_EDGE wrapped texture +// (ground shouldn't tile — it's a real photo). +function loadImageTexture(gl, src) { + return new Promise(function(resolve, reject) { + var img = new Image(); + img.crossOrigin = 'anonymous'; + img.onload = function() { + var tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + resolve(tex); + }; + img.onerror = function() { + reject(new Error('clouds3d: failed to load image: ' + src)); + }; + img.src = src; + }); +} + function hex(rgb) { return [((rgb >> 16) & 255) / 255, ((rgb >> 8) & 255) / 255, @@ -272,6 +310,17 @@ window.clouds3dRender = function(target) { if (!canvas.__state) { canvas.__state = init(canvas); if (!canvas.__state) return; + // Load the satellite ground texture asynchronously. The + // ground isn't strictly needed for the clouds-only view, so + // frames render normally before it loads — the shader just + // samples a black texture in the meantime. + canvas.__groundReady = false; + loadImageTexture(s.gl, 'ground.jpg').then(function(tex) { + canvas.__ground = tex; + canvas.__groundReady = true; + }).catch(function(err) { + console.warn(err.message); + }); } var s = canvas.__state; @@ -285,6 +334,11 @@ window.clouds3dRender = function(target) { gl.enableVertexAttribArray(s.aPos); gl.vertexAttribPointer(s.aPos, 2, gl.FLOAT, false, 0, 0); + // Bind ground texture to texture unit 1 (noise is on unit 0). + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, canvas.__ground || null); + gl.uniform1i(s.uGround, 1); + gl.uniform2f(s.uRes, canvas.width, canvas.height); gl.uniform1f(s.uTime, performance.now() / 1000);