rustial-renderer-wgpu 0.0.1

Pure WGPU renderer for the rustial 2.5D map engine
Documentation
// Fill-extrusion shader with configurable directional lighting, shadows, and fog.

struct Uniforms {
    view_proj:           mat4x4<f32>,
    fog_color:           vec4<f32>,
    eye_pos:             vec4<f32>,
    fog_params:          vec4<f32>,   // (start, end, density, 0)
    hillshade_highlight: vec4<f32>,
    hillshade_shadow:    vec4<f32>,
    hillshade_accent:    vec4<f32>,
    hillshade_light:     vec4<f32>,
    ambient_color:       vec4<f32>,   // (r, g, b, lighting_enabled)
    directional_dir:     vec4<f32>,   // (x, y, z, 0) — toward light
    directional_color:   vec4<f32>,   // (r, g, b, 0)
};

struct ShadowParams {
    light_matrix_0: mat4x4<f32>,
    light_matrix_1: mat4x4<f32>,
    // [intensity, texel_size, normal_offset, cascade_split]
    shadow_config:  vec4<f32>,
    // [dir.x, dir.y, dir.z, shadow_enabled]
    shadow_dir:     vec4<f32>,
};

struct VertexInput {
    @location(0) position: vec3<f32>,
    @location(1) normal:   vec3<f32>,
    @location(2) color:    vec4<f32>,
};

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) normal:    vec3<f32>,
    @location(1) color:     vec4<f32>,
    @location(2) world_pos: vec3<f32>,
};

@group(0) @binding(0)
var<uniform> u: Uniforms;

@group(1) @binding(0)
var<uniform> shadow_params: ShadowParams;

@group(1) @binding(1)
var shadow_map_0: texture_depth_2d;

@group(1) @binding(2)
var shadow_map_1: texture_depth_2d;

@group(1) @binding(3)
var shadow_sampler: sampler_comparison;

// ---------------------------------------------------------------------------
// Shadow sampling
// ---------------------------------------------------------------------------

fn shadow_coords(light_matrix: mat4x4<f32>, world_pos: vec3<f32>) -> vec3<f32> {
    let light_clip = light_matrix * vec4<f32>(world_pos, 1.0);
    let ndc = light_clip.xyz / light_clip.w;
    return vec3<f32>(ndc.x * 0.5 + 0.5, -ndc.y * 0.5 + 0.5, ndc.z);
}

fn compute_shadow(world_pos: vec3<f32>, normal: vec3<f32>) -> f32 {
    let shadow_enabled = shadow_params.shadow_dir.w;
    if shadow_enabled < 0.5 {
        return 0.0;
    }

    let intensity = shadow_params.shadow_config.x;
    if intensity < 0.001 {
        return 0.0;
    }

    let normal_offset_scale = shadow_params.shadow_config.z;
    let sun_dir = normalize(shadow_params.shadow_dir.xyz);
    let ndotl = max(dot(normal, sun_dir), 0.0);
    let offset_scale = normal_offset_scale * (1.0 - ndotl);
    let offset_pos = world_pos + normal * offset_scale;

    // Try cascade 0.
    let coords0 = shadow_coords(shadow_params.light_matrix_0, offset_pos);
    if coords0.x >= 0.0 && coords0.x <= 1.0 && coords0.y >= 0.0 && coords0.y <= 1.0
        && coords0.z >= 0.0 && coords0.z <= 1.0 {
        let shadow = textureSampleCompare(shadow_map_0, shadow_sampler, coords0.xy, coords0.z);
        return (1.0 - shadow) * intensity;
    }

    // Cascade 1 with edge fade.
    let coords1 = shadow_coords(shadow_params.light_matrix_1, offset_pos);
    if coords1.x >= 0.0 && coords1.x <= 1.0 && coords1.y >= 0.0 && coords1.y <= 1.0
        && coords1.z >= 0.0 && coords1.z <= 1.0 {
        let edge = max(
            max(abs(coords1.x * 2.0 - 1.0), abs(coords1.y * 2.0 - 1.0)),
            0.0
        );
        let fade = smoothstep(0.75, 1.0, edge);
        let shadow = textureSampleCompare(shadow_map_1, shadow_sampler, coords1.xy, coords1.z);
        return (1.0 - shadow) * intensity * (1.0 - fade);
    }

    return 0.0;
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
    var out: VertexOutput;
    out.clip_position = u.view_proj * vec4<f32>(in.position, 1.0);
    out.normal    = in.normal;
    out.color     = in.color;
    out.world_pos = in.position;
    return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let n = normalize(in.normal);
    let lighting_on = u.ambient_color.w;

    // --- Configurable directional light ---
    let sun_dir = normalize(u.directional_dir.xyz);
    let ndotl = max(dot(n, sun_dir), 0.0);
    // Soft fill light from perpendicular direction.
    let fill_dir = normalize(vec3<f32>(sun_dir.y, -sun_dir.x, max(0.25, sun_dir.z * 0.6)));
    let fill = max(dot(n, fill_dir), 0.0);

    // Ambient + directional diffuse + fill.
    let ambient = u.ambient_color.rgb;
    let diffuse = u.directional_color.rgb * ndotl;
    let fill_contrib = u.directional_color.rgb * fill * 0.24;

    // Shadow attenuation — darkens the directional contribution.
    let shadow = compute_shadow(in.world_pos, n);
    let shadow_factor = 1.0 - shadow;

    let lit = clamp(
        in.color.rgb * (ambient + (diffuse + fill_contrib) * shadow_factor),
        vec3<f32>(0.0),
        vec3<f32>(1.0),
    );

    // When lighting is disabled (flat mode), use the base colour directly.
    let final_rgb = mix(in.color.rgb, lit, lighting_on);

    // --- Horizon fog ---
    let dx = in.world_pos.x - u.eye_pos.x;
    let dy = in.world_pos.y - u.eye_pos.y;
    let ground_dist = sqrt(dx * dx + dy * dy);
    let fog_start = u.fog_params.x;
    let fog_end   = u.fog_params.y;
    let density   = u.fog_params.z;
    let fog_t = clamp(
        (ground_dist - fog_start) / max(fog_end - fog_start, 0.001),
        0.0, 1.0,
    ) * density;
    let fog_mix = fog_t * 0.7;

    let blended_rgb = mix(final_rgb, u.fog_color.rgb, fog_mix);
    let blended_a   = in.color.a * (1.0 - fog_mix);
    return vec4<f32>(blended_rgb, blended_a);
}