gizmo-renderer 0.8.0

A custom ECS and physics engine aimed for realistic simulations.
Documentation
// G-Buffer geometry pass.
// Writes opaque PBR surfaces to three MRTs; unlit/skybox objects are discarded here
// and drawn in a subsequent forward pass.

fn inverse_transpose_3x3(m: mat3x3<f32>) -> mat3x3<f32> {
    let cross01 = cross(m[0], m[1]);
    let cross12 = cross(m[1], m[2]);
    let cross20 = cross(m[2], m[0]);
    let inv_det = 1.0 / dot(m[2], cross01);
    return mat3x3<f32>(cross12 * inv_det, cross20 * inv_det, cross01 * inv_det);
}

struct SceneUniforms {
    view_proj:      mat4x4<f32>,
    camera_pos:     vec4<f32>,
    sun_direction:  vec4<f32>,
    sun_color:      vec4<f32>,
    lights:         array<vec4<f32>, 40>, // 10 * LightData (4 vec4 each) — not used in G-pass
    light_view_proj: array<mat4x4<f32>, 4>,
    cascade_splits:  vec4<f32>,
    camera_forward:  vec4<f32>,
    cascade_params:  vec4<f32>,
    num_lights: u32,
    _pad: vec3<u32>,
};

struct SkeletonData {
    joints: array<mat4x4<f32>, 128>,
};

// Per-material scalar params (group 1, binding 6). Mirrors `gpu_types::MaterialParams`.
struct MaterialParams {
    // xyz = emissive factor (linear), w = normal-map scale.
    emissive_and_normal_scale: vec4<f32>,
    // x = occlusion (AO) strength; y = UV rotation (radians); zw = UV offset.
    occlusion_uv_rot_offset: vec4<f32>,
    // xy = UV scale; zw reserved.
    uv_scale: vec4<f32>,
};

// KHR_texture_transform: scale, then rotate, then translate the UV
// (matrix = translation * rotation * scale, per the extension's reference).
fn apply_uv_transform(uv: vec2<f32>, offset: vec2<f32>, rot: f32, scale: vec2<f32>) -> vec2<f32> {
    let s = uv * scale;
    let c = cos(rot);
    let sn = sin(rot);
    let rotated = vec2<f32>(c * s.x - sn * s.y, sn * s.x + c * s.y);
    return rotated + offset;
}

struct InstanceData {
    model_matrix_0: vec4<f32>,
    model_matrix_1: vec4<f32>,
    model_matrix_2: vec4<f32>,
    model_matrix_3: vec4<f32>,
    albedo_color:   vec4<f32>,
    pbr:            vec4<f32>,  // x=roughness, y=metallic, z=unlit_flag
};

struct VertexInput {
    @location(0) position:      vec3<f32>,
    @location(1) color:         vec3<f32>,
    @location(2) normal:        vec3<f32>,
    @location(3) tex_coords:    vec2<f32>,
    @location(4) joint_indices: vec4<u32>,
    @location(5) joint_weights: vec4<f32>,
    @location(6) tangent:       vec4<f32>,
};

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) color:         vec3<f32>,
    @location(1) normal:        vec3<f32>,
    @location(2) tex_coords:    vec2<f32>,
    @location(3) world_position: vec3<f32>,
    @location(4) inst_albedo:   vec4<f32>,
    @location(5) inst_pbr:      vec4<f32>,
    @location(6) world_tangent:  vec4<f32>,
};

@group(0) @binding(0) var<uniform> scene: SceneUniforms;
@group(1) @binding(0) var t_diffuse:  texture_2d<f32>;
@group(1) @binding(1) var s_diffuse:  sampler;
@group(1) @binding(2) var t_normal:   texture_2d<f32>;
@group(1) @binding(3) var t_mr:       texture_2d<f32>;  // g=roughness, b=metallic
@group(1) @binding(4) var t_emissive: texture_2d<f32>;
@group(1) @binding(5) var t_ao:       texture_2d<f32>;  // r=occlusion
@group(1) @binding(6) var<uniform> material: MaterialParams;
@group(3) @binding(0) var<uniform> skeleton: SkeletonData;
@group(4) @binding(0) var<storage, read> instances: array<InstanceData>;

@vertex
fn vs_main(@builtin(instance_index) instance_idx: u32, input: VertexInput) -> VertexOutput {
    var out: VertexOutput;
    out.color     = input.color;
    out.tex_coords = input.tex_coords;

    let inst  = instances[instance_idx];
    let model = mat4x4<f32>(
        inst.model_matrix_0, inst.model_matrix_1,
        inst.model_matrix_2, inst.model_matrix_3,
    );

    var skin_mat = mat4x4<f32>(
        vec4<f32>(1.0, 0.0, 0.0, 0.0), vec4<f32>(0.0, 1.0, 0.0, 0.0),
        vec4<f32>(0.0, 0.0, 1.0, 0.0), vec4<f32>(0.0, 0.0, 0.0, 1.0),
    );
    if (input.joint_weights.x + input.joint_weights.y + input.joint_weights.z + input.joint_weights.w > 0.0) {
        skin_mat =
            input.joint_weights.x * skeleton.joints[input.joint_indices.x] +
            input.joint_weights.y * skeleton.joints[input.joint_indices.y] +
            input.joint_weights.z * skeleton.joints[input.joint_indices.z] +
            input.joint_weights.w * skeleton.joints[input.joint_indices.w];
    }

    let skinned_pos  = skin_mat * vec4<f32>(input.position, 1.0);
    let world_pos    = model    * vec4<f32>(skinned_pos.xyz, 1.0);
    out.world_position = world_pos.xyz;

    // Normal skin uzayında inverse-transpose ile taşınır (non-uniform bone scale/shear
    // doğru; rigid/uniform'da no-op çünkü fragment'ta normalize edilir). Tangent ise
    // bir yön olarak doğrudan matrisle taşınır (inverse-transpose DEĞİL).
    let skin_normal_mat = inverse_transpose_3x3(mat3x3<f32>(skin_mat[0].xyz, skin_mat[1].xyz, skin_mat[2].xyz));
    let skinned_normal = skin_normal_mat * input.normal;
    let normal_mat     = inverse_transpose_3x3(mat3x3<f32>(model[0].xyz, model[1].xyz, model[2].xyz));
    out.normal = normal_mat * skinned_normal;

    let skinned_tangent = skin_mat * vec4<f32>(input.tangent.xyz, 0.0);
    out.world_tangent = vec4<f32>(normal_mat * skinned_tangent.xyz, input.tangent.w);

    out.inst_albedo = inst.albedo_color;
    out.inst_pbr    = inst.pbr;
    out.clip_position = scene.view_proj * world_pos;
    return out;
}

// G-Buffer output (4 MRTs fit the 32 bytes/sample color-attachment budget: 4+8+8+8=28):
//   RT0  albedo_metallic  Rgba8Unorm   — rgb=albedo,  a=metallic
//   RT1  normal_roughness Rgba16Float  — rgb=normal,  a=roughness
//   RT2  world_position   Rgba16Float  — rgb=pos,     a=written-flag + packed subsurface/anisotropy
//   RT3  world_tangent    Rgba16Float  — rgb=tangent, a=handedness + packed clear-coat
struct GBufferOut {
    @location(0) albedo_metallic:  vec4<f32>,
    @location(1) normal_roughness: vec4<f32>,
    @location(2) world_position:   vec4<f32>,
    @location(3) world_tangent:    vec4<f32>,
};

@fragment
fn fs_main(in: VertexOutput) -> GBufferOut {
    // Skip unlit / skybox objects — they are drawn in a forward pass
    if (in.inst_pbr.z > 0.5) { discard; }

    // KHR_texture_transform: transform the incoming UV once, reuse for every map.
    let uv = apply_uv_transform(
        in.tex_coords,
        material.occlusion_uv_rot_offset.zw,
        material.occlusion_uv_rot_offset.y,
        material.uv_scale.xy,
    );

    let tex_color   = textureSample(t_diffuse, s_diffuse, uv);
    let final_alpha = in.inst_albedo.a * tex_color.a;
    // if (final_alpha < 0.5) { discard; }

    // ── Geometric tangent basis (TBN) ─────────────────────────────────────
    var raw_normal = in.normal;
    if (length(raw_normal) < 0.001) { raw_normal = vec3<f32>(0.0, 1.0, 0.0); }
    let geo_N = normalize(raw_normal);

    var raw_tangent = in.world_tangent.xyz;
    if (length(raw_tangent) < 0.001) {
        if (abs(geo_N.x) > 0.9) {
            raw_tangent = cross(vec3<f32>(0.0, 1.0, 0.0), geo_N);
        } else {
            raw_tangent = cross(vec3<f32>(1.0, 0.0, 0.0), geo_N);
        }
    }
    // Gram-Schmidt re-orthogonalise the tangent against the (interpolated) normal.
    let T = normalize(raw_tangent - geo_N * dot(geo_N, raw_tangent));
    let B = cross(geo_N, T) * sign(in.world_tangent.w);

    // ── Normal map (tangent-space → world) ────────────────────────────────
    // Flat-default map (0.5,0.5,1.0) → (0,0,1) → geometric normal unchanged.
    let normal_scale = material.emissive_and_normal_scale.w;
    var ts_normal = textureSample(t_normal, s_diffuse, uv).xyz * 2.0 - 1.0;
    ts_normal = vec3<f32>(ts_normal.xy * normal_scale, max(ts_normal.z, 1e-4));
    ts_normal = normalize(ts_normal);
    let N = normalize(T * ts_normal.x + B * ts_normal.y + geo_N * ts_normal.z);

    // ── Metallic-roughness map (glTF: factor × texture) ───────────────────
    // White-default map → sampled g=b=1 → scalar factors preserved.
    let mr = textureSample(t_mr, s_diffuse, uv);
    let roughness = clamp(in.inst_pbr.x * mr.g, 0.05, 1.0);
    let metallic  = clamp(in.inst_pbr.y * mr.b, 0.0, 1.0);

    // ── Ambient occlusion (glTF: 1 + strength·(ao-1)) ─────────────────────
    // White-default map (r=1) → ao=1 → no darkening.
    let ao_strength = material.occlusion_uv_rot_offset.x;
    let ao = 1.0 + ao_strength * (textureSample(t_ao, s_diffuse, uv).r - 1.0);

    // ── Emissive (glTF: emissiveFactor × texture) ─────────────────────────
    // White-default map × zero factor → no emission.
    let emissive = material.emissive_and_normal_scale.xyz
                 * textureSample(t_emissive, s_diffuse, uv).rgb;

    // Base albedo, modulated by AO, then emissive added.
    // NOTE (deferred, MRT-budget): the G-buffer has no free channel for AO or
    // emissive, so AO is folded into albedo and emissive is added to albedo.
    // Emissive therefore reads as extra diffuse energy (lit + LDR-clamped) rather
    // than as a true unlit HDR glow — see report for the trade-off.
    var albedo = in.inst_albedo.rgb * tex_color.rgb;
    albedo = albedo * ao + emissive;

    // Unpack anisotropy, clear_coat, and subsurface from in.inst_pbr.w (packed_params)
    let subsurface_raw = floor(in.inst_pbr.w / 1000000.0) / 100.0;
    let rem_packed = in.inst_pbr.w - floor(in.inst_pbr.w / 1000000.0) * 1000000.0;
    let clear_coat_raw = floor(rem_packed / 1000.0) / 1000.0;
    let anisotropy_raw = (rem_packed - floor(rem_packed / 1000.0) * 1000.0) / 1000.0;

    let packed_tangent_w = sign(in.world_tangent.w) * (0.01 + 0.99 * clear_coat_raw);

    var out: GBufferOut;
    out.albedo_metallic  = vec4<f32>(albedo, metallic);
    out.normal_roughness = vec4<f32>(N, roughness);
    out.world_position   = vec4<f32>(in.world_position, (0.5 + 0.49 * anisotropy_raw) + 100.0 * subsurface_raw);
    out.world_tangent    = vec4<f32>(T, packed_tangent_w);
    return out;
}