Add satellite ground texture (ESRI World Imagery, Passau)
Pulled a 5x4 tile mosaic of the Passau region (zoom 10) from ESRI's free ArcGIS World Imagery service, stitched + resized to 1024x768 JPEG (277 KB). Added to Dockerfile as ground.jpg. Shader side: - New uniform uGround (texture unit 1) - horizonMask: rd.y in [-0.20, -0.02] -> ground fades in from 0 to 1 as we look further down - gUv derived from rd.xz so looking forward/down scrolls across the satellite image (parallax-like feel) - Distant ground fades toward uBg via a 'fog' factor so the horizon blends naturally with the sky gradient Loading is async — clouds render before ground texture arrives. Falls back to a black sample if ground.jpg fails to load.
This commit is contained in:
1 parent
2e745c26ad
commit
6323beccf9
4 files changed
+56
-1
No files matched your search
Vendored
+55
-1
@@ -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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user