henad-app 0.3.0

Desktop and web app for Henad, a parallel agent-based modelling engine.
Documentation
// Instanced agent sprites, one quad per agent expanded in clip space from a world-space centre.
//
// Positions arrive as two `f32` attributes, so the sim's structure-of-arrays (SoA) lanes upload with no repacking.

#import world::{HIDDEN, is_placed, to_clip}

struct Uniforms {
    world: vec2<f32>,
    // Half the sprite size in clip units. Precomputed, since it depends on the target rect.
    half_size: vec2<f32>,
}

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

struct VertexInput {
    @location(0) pos_x: f32,
    @location(1) pos_y: f32,
    @location(2) color: vec4<f32>,
}

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) color: vec4<f32>,
    // Position within the sprite, from -1 to 1 on each axis, so the fragment stage can carve a disc out of the quad.
    @location(1) offset: vec2<f32>,
}

@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexInput) -> VertexOutput {
    // Triangle-strip corner order: (0,0) (1,0) (0,1) (1,1), remapped to -1..1.
    let corner = vec2<f32>(
        f32(vertex_index & 1u),
        f32((vertex_index >> 1u) & 1u),
    ) * 2.0 - 1.0;

    let pos = vec2<f32>(in.pos_x, in.pos_y);
    let center = to_clip(pos, u.world);

    var out: VertexOutput;
    out.clip_position = select(HIDDEN, vec4<f32>(center + corner * u.half_size, 0.0, 1.0), is_placed(pos));
    out.color = in.color;
    out.offset = corner;
    return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    // Round agents read better than squares once sprites overlap. Subpixel quads never reach
    // here, so this costs nothing at scale.
    if dot(in.offset, in.offset) > 1.0 {
        discard;
    }
    return in.color;
}