scena 1.7.1

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
struct QuadVertex {
    @location(0) side_along: vec2<f32>,
};

struct StrokeInstance {
    @location(1) start: vec3<f32>,
    @location(2) end: vec3<f32>,
    @location(3) color: vec4<f32>,
    @location(4) width_px: f32,
};

struct VertexOut {
    @builtin(position) position: vec4<f32>,
    @location(0) color: vec4<f32>,
};

struct LightingUniform {
    directional_light_direction_intensity: vec4<f32>,
    directional_light_color_count: vec4<f32>,
    directional_shadow_control: vec4<f32>,
    point_light_position_intensity: vec4<f32>,
    point_light_color_range: vec4<f32>,
    spot_light_position_intensity: vec4<f32>,
    spot_light_direction_cones: vec4<f32>,
    spot_light_cone_range: vec4<f32>,
    spot_light_color_range: vec4<f32>,
    environment_diffuse_intensity: vec4<f32>,
    environment_specular_intensity: vec4<f32>,
};

struct CameraUniform {
    view_from_world: mat4x4<f32>,
    clip_from_view: mat4x4<f32>,
    clip_from_world: mat4x4<f32>,
    light_from_world: mat4x4<f32>,
    camera_position_exposure: vec4<f32>,
    viewport_near_far: vec4<f32>,
    color_management: vec4<f32>,
    lighting: LightingUniform,
};

struct DrawUniform {
    world_from_model: mat4x4<f32>,
    normal_from_model: mat4x4<f32>,
    tint: vec4<f32>,
};

struct ClippedSegment {
    start: vec4<f32>,
    end: vec4<f32>,
    visible: f32,
};

@group(0) @binding(0)
var<uniform> camera: CameraUniform;

@group(2) @binding(0)
var<uniform> draw: DrawUniform;

@vertex
fn vs_main(quad: QuadVertex, segment: StrokeInstance) -> VertexOut {
    let start_world = draw.world_from_model * vec4<f32>(segment.start, 1.0);
    let end_world = draw.world_from_model * vec4<f32>(segment.end, 1.0);
    let clipped = clip_segment_to_near(
        camera.clip_from_world * start_world,
        camera.clip_from_world * end_world,
    );
    let start_ndc = clipped.start.xy / max(clipped.start.w, 0.0001);
    let end_ndc = clipped.end.xy / max(clipped.end.w, 0.0001);
    let delta = end_ndc - start_ndc;
    let length_px = length(delta * camera.viewport_near_far.xy * 0.5);
    let direction = select(vec2<f32>(1.0, 0.0), normalize(delta), length_px > 0.001);
    let normal = vec2<f32>(-direction.y, direction.x);
    let half_width = max(segment.width_px, 0.001) * 0.5;
    let offset_ndc = normal * quad.side_along.x * half_width * vec2<f32>(
        2.0 / max(camera.viewport_near_far.x, 1.0),
        2.0 / max(camera.viewport_near_far.y, 1.0),
    );
    var position = select(clipped.start, clipped.end, quad.side_along.y > 0.5);
    position.x = position.x + offset_ndc.x * position.w;
    position.y = position.y + offset_ndc.y * position.w;
    if clipped.visible < 0.5 || length_px <= 0.001 {
        position = vec4<f32>(2.0, 2.0, 0.0, 1.0);
    }
    var out: VertexOut;
    out.position = position;
    out.color = segment.color * draw.tint;
    return out;
}

@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
    let color_management_mode = camera.color_management.x;
    let rgb = apply_tonemapper(in.color.rgb * camera.camera_position_exposure.w, color_management_mode);
    return vec4<f32>(encode_post_target_rgb(rgb), in.color.a);
}

fn clip_segment_to_near(start: vec4<f32>, end: vec4<f32>) -> ClippedSegment {
    var a = start;
    var b = end;
    if a.z < 0.0 && b.z < 0.0 {
        return ClippedSegment(a, b, 0.0);
    }
    if a.z < 0.0 {
        let t = clamp((0.0 - a.z) / max(b.z - a.z, 0.000001), 0.0, 1.0);
        a = mix(a, b, t);
    }
    if b.z < 0.0 {
        let t = clamp((0.0 - b.z) / max(a.z - b.z, 0.000001), 0.0, 1.0);
        b = mix(b, a, t);
    }
    return ClippedSegment(a, b, 1.0);
}

fn apply_tonemapper(color: vec3<f32>, color_management_mode: f32) -> vec3<f32> {
    if color_management_mode < 0.5 {
        return clamp(color, vec3<f32>(0.0), vec3<f32>(1.0));
    }
    return aces_tonemap(color);
}

fn aces_tonemap(color: vec3<f32>) -> vec3<f32> {
    let a = 2.51;
    let b = 0.03;
    let c = 2.43;
    let d = 0.59;
    let e = 0.14;
    return clamp((color * (a * color + vec3<f32>(b))) / (color * (c * color + vec3<f32>(d)) + vec3<f32>(e)), vec3<f32>(0.0), vec3<f32>(1.0));
}

fn encode_post_target_rgb(color: vec3<f32>) -> vec3<f32> {
    if camera.color_management.y <= 0.5 {
        return color;
    }
    return vec3<f32>(
        linear_to_srgb_channel(color.r),
        linear_to_srgb_channel(color.g),
        linear_to_srgb_channel(color.b),
    );
}

fn linear_to_srgb_channel(channel: f32) -> f32 {
    let value = clamp(channel, 0.0, 1.0);
    if value <= 0.0031308 {
        return value * 12.92;
    }
    return 1.055 * pow(value, 1.0 / 2.4) - 0.055;
}