#version 450 layout(location = 0) in vec3 frag_color; layout(location = 1) in vec3 frag_normal; layout(location = 2) in float frag_debug_tint; layout(location = 3) in vec3 frag_world_position; layout(location = 0) out vec4 out_color; // The block is declared identically in cube.vert. A push-constant block is a single object shared by every stage of the pipeline, so the two declarations must agree exactly even where a stage reads only part of it. layout(push_constant) uniform PushConstants { mat4 mvp; // xyz is the chunk's world offset; w is the debug-tint weight, 0.0 for normal rendering and 1.0 for a debug raster pass. vec4 chunk_offset; // xyz is the camera's world position; w is the horizontal distance at which fog reaches full opacity. vec4 fog; // rgb is the sky colour distant geometry fades into, matching the colour attachment's clear value; w is the vertical distance at which fog reaches full opacity. vec4 sky_color; } push_constants; // Direction the light travels, pointing downward and across both horizontal axes so that no cubic face orientation receives exactly the same amount of light as another. A light aligned with an axis would leave two of the three visible faces of a cube indistinguishable. const vec3 LIGHT_DIRECTION = vec3(-0.4, -1.0, -0.3); // Fraction of the albedo retained by a fully unlit face, standing in for bounced light until a global-illumination term exists. Without it, faces turned away from the light collapse to black and their silhouettes disappear against one another. const float AMBIENT = 0.25; // Colour applied to debug raster passes, chosen to contrast with terrain and to remain legible when overlaid on filled geometry. const vec3 DEBUG_COLOR = vec3(1.0, 0.0, 1.0); // Fraction of the fog end distance at which the fade begins. Below it geometry is drawn unfogged, which keeps the fog out of the region the player is actually looking at while leaving enough depth for the ramp to read as gradual rather than as a band. const float FOG_START_FRACTION = 0.6; // Floor on the width of the fade band, guarding the division below against a caller that supplies a fog end distance of zero. const float MIN_FOG_RANGE = 1e-3; // Returns the fog opacity for a surface `distance` from the camera along one axis, given the distance at which that axis reaches full opacity. // // The ramp is linear rather than exponential. Exponential fog approaches full opacity asymptotically without ever reaching it, so geometry stays faintly visible right up to the moment its chunk is unloaded, which is the pop the fog exists to conceal. float fog_ramp(float distance, float end) { float start = end * FOG_START_FRACTION; float range = max(end - start, MIN_FOG_RANGE); return clamp((distance - start) / range, 0.0, 1.0); } void main() { // The interpolated normal is renormalised: the six face normals are unit length and constant across a quad, but interpolation across a triangle is not guaranteed to preserve that. vec3 normal = normalize(frag_normal); // Lambertian term. The light vector is negated because LIGHT_DIRECTION points along the light's travel, whereas the dot product requires the direction from the surface toward the light. The clamp discards the negative half, where the face points away from the light. float diffuse = max(dot(normal, normalize(-LIGHT_DIRECTION)), 0.0); vec3 lit = frag_color * (AMBIENT + (1.0 - AMBIENT) * diffuse); // The debug tint is applied after shading so debug passes draw flat and stay legible over the shaded geometry beneath them. vec3 shaded = mix(lit, DEBUG_COLOR, frag_debug_tint); // The horizontal and vertical extents of the streaming region are ramped independently, because the region is a cylinder rather than a sphere and therefore reaches one frontier well before the other. Fading both against a single distance leaves the nearer frontier unfogged and fully visible. vec3 to_camera = frag_world_position - push_constants.fog.xyz; float fog_horizontal = fog_ramp(length(to_camera.xz), push_constants.fog.w); float fog_vertical = fog_ramp(abs(to_camera.y), push_constants.sky_color.a); // Whichever frontier the surface is closer to determines the fade, so geometry is fully obscured before it crosses either one. float fog_factor = max(fog_horizontal, fog_vertical); // Fog is applied after the debug tint so an overlay recedes together with the geometry it annotates instead of punching through the fade. out_color = vec4(mix(shaded, push_constants.sky_color.rgb, fog_factor), 1.0); }