kiss3d 0.42.0

Keep it simple, stupid, 2D and 3D graphics engine for Rust.
Documentation
// Default material shader for kiss3d
// Implements Cook-Torrance PBR with texture support, instancing, and multi-light

const PI: f32 = 3.14159265359;
const MAX_LIGHTS: u32 = 8u;

// Light type constants
const LIGHT_TYPE_POINT: u32 = 0u;
const LIGHT_TYPE_DIRECTIONAL: u32 = 1u;
const LIGHT_TYPE_SPOT: u32 = 2u;

// Single light data structure
struct LightData {
    position: vec3<f32>,
    light_type: u32,
    direction: vec3<f32>,
    intensity: f32,
    color: vec3<f32>,
    inner_cone_cos: f32,
    outer_cone_cos: f32,
    attenuation_radius: f32,
    _padding: vec2<f32>,
}

// Bind group 0: Frame uniforms (view, projection, lights)
struct FrameUniforms {
    view: mat4x4<f32>,
    proj: mat4x4<f32>,
    lights: array<LightData, MAX_LIGHTS>,
    num_lights: u32,
    ambient_intensity: f32,
    _padding: vec2<f32>,
}

@group(0) @binding(0)
var<uniform> frame: FrameUniforms;

// Bind group 1: Object uniforms (transform, scale, color, PBR properties)
struct ObjectUniforms {
    transform: mat4x4<f32>,
    ntransform: mat3x3<f32>,
    scale: mat3x3<f32>,
    color: vec4<f32>,
    metallic: f32,
    roughness: f32,
    _pad0: vec2<f32>,
    emissive: vec4<f32>,
    has_normal_map: f32,
    has_metallic_roughness_map: f32,
    has_ao_map: f32,
    has_emissive_map: f32,
}

@group(1) @binding(0)
var<uniform> object: ObjectUniforms;

// Bind group 2: Albedo texture and sampler
@group(2) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(2) @binding(1)
var s_diffuse: sampler;

// Bind group 3: PBR texture maps
@group(3) @binding(0)
var t_normal: texture_2d<f32>;
@group(3) @binding(1)
var s_normal: sampler;
@group(3) @binding(2)
var t_metallic_roughness: texture_2d<f32>;
@group(3) @binding(3)
var s_metallic_roughness: sampler;
@group(3) @binding(4)
var t_ao: texture_2d<f32>;
@group(3) @binding(5)
var s_ao: sampler;
@group(3) @binding(6)
var t_emissive: texture_2d<f32>;
@group(3) @binding(7)
var s_emissive: sampler;

// Vertex input
struct VertexInput {
    @location(0) position: vec3<f32>,
    @location(1) tex_coord: vec2<f32>,
    @location(2) normal: vec3<f32>,
}

// Instance input
struct InstanceInput {
    @location(3) inst_tra: vec3<f32>,
    @location(4) inst_color: vec4<f32>,
    @location(5) inst_def_0: vec3<f32>,
    @location(6) inst_def_1: vec3<f32>,
    @location(7) inst_def_2: vec3<f32>,
}

// Vertex output / Fragment input
struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) tex_coord: vec2<f32>,
    @location(1) world_normal: vec3<f32>,
    @location(2) world_pos: vec3<f32>,
    @location(3) vert_color: vec4<f32>,
    @location(4) view_pos: vec3<f32>,
}

// === PBR BRDF Functions ===

// Normal Distribution Function (GGX/Trowbridge-Reitz)
fn distribution_ggx(N: vec3<f32>, H: vec3<f32>, roughness: f32) -> f32 {
    let a = roughness * roughness;
    let a2 = a * a;
    let NdotH = max(dot(N, H), 0.0);
    let NdotH2 = NdotH * NdotH;

    var denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;

    return a2 / max(denom, 0.0001);
}

// Geometry function (Smith's Schlick-GGX)
fn geometry_schlick_ggx(NdotV: f32, roughness: f32) -> f32 {
    let r = (roughness + 1.0);
    let k = (r * r) / 8.0;  // Direct lighting

    return NdotV / (NdotV * (1.0 - k) + k);
}

fn geometry_smith(N: vec3<f32>, V: vec3<f32>, L: vec3<f32>, roughness: f32) -> f32 {
    let NdotV = max(dot(N, V), 0.0);
    let NdotL = max(dot(N, L), 0.0);
    let ggx2 = geometry_schlick_ggx(NdotV, roughness);
    let ggx1 = geometry_schlick_ggx(NdotL, roughness);

    return ggx1 * ggx2;
}

// Fresnel (Schlick approximation)
fn fresnel_schlick(cos_theta: f32, F0: vec3<f32>) -> vec3<f32> {
    return F0 + (1.0 - F0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0);
}

// Attenuation functions
fn calculate_point_attenuation(dist: f32, radius: f32) -> f32 {
    // Smooth falloff that reaches zero at the attenuation radius
    let normalized_dist = clamp(dist / radius, 0.0, 1.0);
    let attenuation = 1.0 - normalized_dist * normalized_dist;
    return attenuation * attenuation;
}

fn calculate_spot_attenuation(
    L: vec3<f32>,
    spot_direction: vec3<f32>,
    dist: f32,
    inner_cone_cos: f32,
    outer_cone_cos: f32,
    radius: f32
) -> f32 {
    // Angular attenuation
    let cos_angle = dot(-L, spot_direction);
    let angular_attenuation = clamp(
        (cos_angle - outer_cone_cos) / max(inner_cone_cos - outer_cone_cos, 0.0001),
        0.0,
        1.0
    );

    // Distance attenuation
    let dist_attenuation = calculate_point_attenuation(dist, radius);

    return angular_attenuation * angular_attenuation * dist_attenuation;
}

// === Vertex Shader ===

@vertex
fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput {
    var out: VertexOutput;

    // Build deformation matrix from instance data
    let deformation = mat3x3<f32>(
        instance.inst_def_0,
        instance.inst_def_1,
        instance.inst_def_2
    );

    // Transform position
    let scaled_pos = object.scale * vertex.position;
    let deformed_pos = deformation * scaled_pos;
    let model_pos = object.transform * vec4<f32>(deformed_pos, 1.0);
    let world_pos = vec4<f32>(instance.inst_tra, 0.0) + model_pos;

    out.clip_position = frame.proj * frame.view * world_pos;
    out.world_pos = world_pos.xyz;

    // Transform normal to world space
    out.world_normal = normalize(deformation * object.ntransform * vertex.normal);

    // View-space position for lighting calculations
    let view_pos = frame.view * world_pos;
    out.view_pos = view_pos.xyz / view_pos.w;

    out.tex_coord = vertex.tex_coord;
    out.vert_color = instance.inst_color;

    return out;
}

// === Fragment Shader ===

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    // Sample albedo texture and combine with vertex/object color
    let albedo_tex = textureSample(t_diffuse, s_diffuse, in.tex_coord);
    let base_color = in.vert_color * object.color;
    let albedo = (albedo_tex * base_color).rgb;

    // Get PBR parameters - either from textures or uniforms
    var metallic = object.metallic;
    var roughness = object.roughness;

    if object.has_metallic_roughness_map > 0.5 {
        let mr = textureSample(t_metallic_roughness, s_metallic_roughness, in.tex_coord);
        // glTF convention: B = metallic, G = roughness
        metallic = mr.b;
        roughness = mr.g;
    }

    // Clamp roughness to prevent artifacts
    roughness = clamp(roughness, 0.04, 1.0);

    // Get normal - either from normal map or geometry
    var N = normalize(in.world_normal);

    if object.has_normal_map > 0.5 {
        let normal_sample = textureSample(t_normal, s_normal, in.tex_coord).rgb;
        let tangent_normal = normal_sample * 2.0 - 1.0;

        // Generate tangent space (simple method based on normal)
        var tangent: vec3<f32>;
        let c1 = cross(N, vec3<f32>(0.0, 0.0, 1.0));
        let c2 = cross(N, vec3<f32>(0.0, 1.0, 0.0));
        if length(c1) > length(c2) {
            tangent = normalize(c1);
        } else {
            tangent = normalize(c2);
        }
        let bitangent = normalize(cross(N, tangent));
        let TBN = mat3x3<f32>(tangent, bitangent, N);
        N = normalize(TBN * tangent_normal);
    }

    // Transform normal to view space for lighting
    let view_mat3 = mat3x3<f32>(
        frame.view[0].xyz,
        frame.view[1].xyz,
        frame.view[2].xyz
    );
    let N_view = normalize(view_mat3 * N);

    // Sample ambient occlusion
    var ao = 1.0;
    if object.has_ao_map > 0.5 {
        ao = textureSample(t_ao, s_ao, in.tex_coord).r;
    }

    // Sample emissive
    var emissive = object.emissive.rgb;
    if object.has_emissive_map > 0.5 {
        let emissive_sample = textureSample(t_emissive, s_emissive, in.tex_coord).rgb;
        emissive = emissive * emissive_sample;
    }

    // View vector (in view space)
    let V = normalize(-in.view_pos);

    // Calculate reflectance at normal incidence (F0)
    // Dielectric: 0.04, Metal: albedo
    let F0 = mix(vec3<f32>(0.04), albedo, metallic);

    // Accumulate lighting from all lights
    var Lo = vec3<f32>(0.0);

    for (var i = 0u; i < frame.num_lights; i++) {
        let light = frame.lights[i];

        // Calculate light direction and attenuation based on light type
        var L: vec3<f32>;
        var attenuation: f32 = 1.0;
        var light_intensity = light.intensity;

        if light.light_type == LIGHT_TYPE_POINT {
            // Point light: calculate direction from light position to fragment
            let light_pos_view = (frame.view * vec4<f32>(light.position, 1.0)).xyz;
            let light_vec = light_pos_view - in.view_pos;
            let dist = length(light_vec);
            L = normalize(light_vec);
            attenuation = calculate_point_attenuation(dist, light.attenuation_radius);
        } else if light.light_type == LIGHT_TYPE_DIRECTIONAL {
            // Directional light: use light direction directly
            let light_dir_view = normalize(view_mat3 * light.direction);
            L = -light_dir_view;  // Light direction points FROM light, we need TO light
        } else {
            // Spot light: calculate direction and angular attenuation
            let light_pos_view = (frame.view * vec4<f32>(light.position, 1.0)).xyz;
            let light_dir_view = normalize(view_mat3 * light.direction);
            let light_vec = light_pos_view - in.view_pos;
            let dist = length(light_vec);
            L = normalize(light_vec);
            attenuation = calculate_spot_attenuation(
                L,
                light_dir_view,
                dist,
                light.inner_cone_cos,
                light.outer_cone_cos,
                light.attenuation_radius
            );
        }

        // Skip if no contribution
        if attenuation <= 0.0 {
            continue;
        }

        let H = normalize(V + L);

        // Cook-Torrance BRDF
        let NDF = distribution_ggx(N_view, H, roughness);
        let G = geometry_smith(N_view, V, L, roughness);
        let F = fresnel_schlick(max(dot(H, V), 0.0), F0);

        // Specular contribution
        let numerator = NDF * G * F;
        let denominator = 4.0 * max(dot(N_view, V), 0.0) * max(dot(N_view, L), 0.0) + 0.0001;
        let specular = numerator / denominator;

        // Energy conservation: diffuse + specular = 1
        let kS = F;
        var kD = vec3<f32>(1.0) - kS;
        kD *= 1.0 - metallic;  // Metals have no diffuse

        // Lighting calculation
        let NdotL_raw = dot(N_view, L);
        let NdotL = max(NdotL_raw, 0.0);

        // Wrapped diffuse (half-Lambert) for softer shadows on back-facing triangles
        let wrap = 0.2;
        let NdotL_wrapped = (NdotL_raw * (1.0 - wrap) + wrap);
        let diffuse_wrap = max(NdotL_wrapped, 0.0);

        // Combine lighting with light color
        let radiance = light.color * light_intensity * attenuation;
        let diffuse_contrib = kD * albedo / PI * diffuse_wrap;
        let specular_contrib = specular * NdotL;
        Lo += (diffuse_contrib + specular_contrib) * radiance;
    }

    // Ambient lighting using configurable intensity from frame uniforms
    let ambient = vec3<f32>(frame.ambient_intensity) * albedo * ao;

    // Final color
    var color = ambient + Lo + emissive;

    return vec4<f32>(color, albedo_tex.a * base_color.a);
}