Fix sky and clouds orientation for Z-up world coordinates

Skybox: replace sphere-mesh approach with a fullscreen triangle that
reconstructs the world-space ray direction analytically via inverse
projection/view matrices. Eliminates clip.w=0 degeneracy at the horizon
and correctly maps altitude to dir.z in the Z-up coordinate system.

Clouds: hemisphere mesh was storing altitude in aPos.y (Y-up convention);
the Z-up view matrix projected this sideways, making clouds appear
vertical. Store altitude in aPos.z and update the fragment shader to
read dir.z as altitude and dir.xy as the horizontal UV plane.
This commit is contained in:
Kelsi 2026-02-21 21:57:16 -08:00
parent 69cf39ba02
commit 4fc3689dcc
9 changed files with 39 additions and 155 deletions

View file

@ -38,10 +38,10 @@ float fbm(vec2 p) {
void main() {
vec3 dir = normalize(vWorldDir);
float altitude = dir.y;
float altitude = dir.z; // Z is up in the Z-up world coordinate system
if (altitude < 0.0) discard;
vec2 uv = dir.xz / (altitude + 0.001);
vec2 uv = dir.xy / (altitude + 0.001); // XY is the horizontal plane
uv += push.windOffset;
float cloud1 = fbm(uv * 0.8);

Binary file not shown.

View file

@ -19,13 +19,30 @@ layout(push_constant) uniform Push {
float timeOfDay;
} push;
layout(location = 0) in vec3 WorldPos;
layout(location = 1) in float Altitude;
layout(location = 0) in vec2 TexCoord;
layout(location = 0) out vec4 outColor;
void main() {
float t = clamp(Altitude, 0.0, 1.0);
// Reconstruct world-space ray direction from screen position.
// TexCoord is [0,1]^2; convert to NDC [-1,1]^2.
float ndcX = TexCoord.x * 2.0 - 1.0;
float ndcY = -(TexCoord.y * 2.0 - 1.0); // flip Y: Vulkan NDC Y-down, but projection already flipped
// Unproject to view space using focal lengths from projection matrix.
// projection[0][0] = 2*near/(right-left) = 1/tan(fovX/2)
// projection[1][1] = 2*near/(top-bottom) (already negated for Vulkan Y-flip)
// We want the original magnitude, so take abs to get the focal length.
vec3 viewDir = vec3(ndcX / projection[0][0],
ndcY / abs(projection[1][1]),
-1.0);
// Rotate to world space: view = R*T, so R^-1 = R^T = transpose(mat3(view))
mat3 invViewRot = transpose(mat3(view));
vec3 worldDir = normalize(invViewRot * viewDir);
// worldDir.z = sin(elevation); +1 = zenith, 0 = horizon, -1 = nadir
float t = clamp(worldDir.z, 0.0, 1.0);
t = pow(t, 1.5);
vec3 sky = mix(push.horizonColor.rgb, push.zenithColor.rgb, t);
float scatter = max(0.0, 1.0 - t * 2.0) * 0.15;

Binary file not shown.

View file

@ -1,27 +1,12 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams; // x=fogStart, y=fogEnd, z=time
vec4 shadowParams; // x=enabled, y=strength
};
// Fullscreen triangle sky — no vertex buffer, no mesh.
// Draws 3 vertices covering the entire screen, depth forced to 1.0 (far plane).
layout(location = 0) in vec3 aPos;
layout(location = 0) out vec3 WorldPos;
layout(location = 1) out float Altitude;
layout(location = 0) out vec2 TexCoord;
void main() {
WorldPos = aPos;
Altitude = aPos.y;
mat4 rotView = mat4(mat3(view)); // strip translation
vec4 pos = projection * rotView * vec4(aPos, 1.0);
gl_Position = pos.xyww; // force far plane
// Produces triangle covering NDC [-1,1]² with depth = 1.0 (far)
TexCoord = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
gl_Position = vec4(TexCoord * 2.0 - 1.0, 1.0, 1.0);
}

Binary file not shown.