kiss3d 0.45.0

Keep it simple, stupid, 2D and 3D graphics engine for Rust.
Documentation
import package::common::{unpack_mat2, unpack_mat3};
// 2D lit material: diffuse + specular shading from dynamic 2D lights, with optional
// normal mapping. Lights live slightly above the plane (their height), so a
// normal-mapped sprite reacts with per-pixel shading; without a normal map the
// surface is flat (+Z) and lights contribute a smooth radial falloff.

const MAX_LIGHTS: u32 = 16u;

struct Light {
    // position.xy, height, kind (0 = point, 1 = spot)
    pos_height: vec4<f32>,
    // color.rgb, intensity
    color_intensity: vec4<f32>,
    // direction.xy, cos(inner_angle), cos(outer_angle)
    dir_cone: vec4<f32>,
    // radius, _, _, _
    radius: vec4<f32>,
}

struct FrameUniforms {
    view_0: vec4<f32>,
    view_1: vec4<f32>,
    view_2: vec4<f32>,
    proj_0: vec4<f32>,
    proj_1: vec4<f32>,
    proj_2: vec4<f32>,
    // ambient.rgb, num_lights
    ambient_count: vec4<f32>,
    lights: array<Light, MAX_LIGHTS>,
}

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

struct ObjectUniforms {
    model_0: vec4<f32>,
    model_1: vec4<f32>,
    model_2: vec4<f32>,
    scale_0: vec4<f32>,
    scale_1: vec4<f32>,
    color: vec4<f32>,
    // specular_strength, shininess, normal_strength, has_normal_map
    params: vec4<f32>,
}

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

@group(2) @binding(0)
var t_albedo: texture_2d<f32>;
@group(2) @binding(1)
var s_albedo: sampler;
@group(2) @binding(2)
var t_normal: texture_2d<f32>;
@group(2) @binding(3)
var s_normal: sampler;

struct VertexInput {
    @location(0) position: vec2<f32>,
    @location(1) tex_coord: vec2<f32>,
}

struct InstanceInput {
    @location(2) inst_tra: vec2<f32>,
    @location(3) inst_color: vec4<f32>,
    @location(4) inst_def_0: vec2<f32>,
    @location(5) inst_def_1: vec2<f32>,
}

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) tex_coord: vec2<f32>,
    @location(1) world: vec2<f32>,
    @location(2) inst_color: vec4<f32>,
}

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

    let view = unpack_mat3(frame.view_0, frame.view_1, frame.view_2);
    let proj = unpack_mat3(frame.proj_0, frame.proj_1, frame.proj_2);
    let model = unpack_mat3(obj.model_0, obj.model_1, obj.model_2);
    let scale = unpack_mat2(obj.scale_0, obj.scale_1);
    let def = mat2x2<f32>(instance.inst_def_0, instance.inst_def_1);

    let deformed = def * (scale * vertex.position);
    let model_pos = model * vec3<f32>(deformed, 1.0);
    let world = vec3<f32>(instance.inst_tra, 0.0) + model_pos;
    var projected = proj * view * world;
    projected.z = 0.0;

    out.clip_position = vec4<f32>(projected, 1.0);
    out.tex_coord = vertex.tex_coord;
    out.world = world.xy;
    out.inst_color = instance.inst_color;
    return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let albedo = textureSample(t_albedo, s_albedo, in.tex_coord) * obj.color * in.inst_color;

    let specular_strength = obj.params.x;
    let shininess = max(obj.params.y, 1.0);
    let normal_strength = obj.params.z;
    let has_normal_map = obj.params.w;

    var n = vec3<f32>(0.0, 0.0, 1.0);
    if (has_normal_map > 0.5) {
        let sampled = textureSample(t_normal, s_normal, in.tex_coord).xyz * 2.0 - vec3<f32>(1.0);
        n = normalize(vec3<f32>(sampled.xy * normal_strength, max(sampled.z, 0.05)));
    }

    let ambient = frame.ambient_count.rgb;
    var lit = ambient * albedo.rgb;

    let frag = vec3<f32>(in.world, 0.0);
    let view_dir = vec3<f32>(0.0, 0.0, 1.0);
    let count = u32(frame.ambient_count.w);

    for (var i = 0u; i < count; i = i + 1u) {
        let light = frame.lights[i];
        let radius = light.radius.x;
        let to_light = vec3<f32>(light.pos_height.xy, light.pos_height.z) - frag;
        let planar_dist = length(to_light.xy);
        if (planar_dist >= radius) {
            continue;
        }
        let l = normalize(to_light);

        // Smooth distance attenuation (quadratic falloff to zero at the radius).
        var atten = clamp(1.0 - planar_dist / radius, 0.0, 1.0);
        atten = atten * atten;

        // Spot cone factor (1 for point lights).
        var cone = 1.0;
        if (light.pos_height.w > 0.5) {
            let spot_dir = normalize(light.dir_cone.xy);
            let frag_dir = normalize(in.world - light.pos_height.xy);
            let cos_ang = dot(frag_dir, spot_dir);
            cone = smoothstep(light.dir_cone.w, light.dir_cone.z, cos_ang);
        }

        let n_dot_l = max(dot(n, l), 0.0);
        let diffuse = n_dot_l * atten * cone;

        var spec = 0.0;
        if (n_dot_l > 0.0 && specular_strength > 0.0) {
            let h = normalize(l + view_dir);
            spec = pow(max(dot(n, h), 0.0), shininess) * specular_strength * atten * cone;
        }

        let radiance = light.color_intensity.rgb * light.color_intensity.w;
        lit += radiance * (diffuse * albedo.rgb + spec);
    }

    return vec4<f32>(lit, albedo.a);
}