Skip to main content

banished_gfx/
skin.rs

1//! GPU skinning shared by the viewer and the game: the `ModelSkinLayout` vertex attributes, the
2//! bone palette uniform and its WGSL. The original's `NormalSkin_*vs` programs blend four bones
3//! from a palette of transforms (`tc_transform`, `c41`, four rows per bone).
4
5use banished_formats::skeleton::Transform;
6
7/// Bones per palette: the original's `tc_transform` is 208 constants from `c41`, four per bone
8/// (`NormalSkin_vs`), so 52; the largest shipped skeleton has 22, a mod's may have more.
9pub const MAX_BONES: usize = 52;
10/// Bytes of one palette (`MAX_BONES` column-major 4x4 matrices); a multiple of 256 so palettes
11/// can be addressed with dynamic uniform offsets.
12pub const PALETTE_BYTES: u64 = (MAX_BONES * 64) as u64;
13
14/// WGSL for the palette at `@group(2) @binding(0)` and the four-bone blend.
15pub const SKIN_WGSL: &str = r#"
16const MAX_BONES: u32 = 52u;
17struct Skin {
18    bones: array<mat4x4<f32>, MAX_BONES>,
19};
20@group(2) @binding(0) var<uniform> skin: Skin;
21
22fn skin_matrix(weights: vec4<f32>, bones: vec4<u32>) -> mat4x4<f32> {
23    return skin.bones[bones.x] * weights.x + skin.bones[bones.y] * weights.y
24         + skin.bones[bones.z] * weights.z + skin.bones[bones.w] * weights.w;
25}
26
27// A skinned point with w = 1. The shipped weights sum to 253..255/255, so the blended matrix's w
28// is not 1; the original blends `R·p + t` per bone and projects the result as a point.
29fn skin_point(skin_m: mat4x4<f32>, p: vec3<f32>) -> vec4<f32> {
30    return vec4<f32>((skin_m * vec4<f32>(p, 1.0)).xyz, 1.0);
31}
32"#;
33
34/// `ModelSkinLayout` (32-byte vertices): i16x4 position (/512), i16x4 texcoords, unorm8x4 normal,
35/// 4 unused bytes, unorm8x4 weights, u8x4 bone indices, at shader locations 0..4.
36pub fn skinned_attributes() -> [wgpu::VertexAttribute; 5] {
37    let at = |format, offset, shader_location| wgpu::VertexAttribute {
38        format,
39        offset,
40        shader_location,
41    };
42    use wgpu::VertexFormat::{Sint16x4, Uint8x4, Unorm8x4};
43    [
44        at(Sint16x4, 0, 0),
45        at(Sint16x4, 8, 1),
46        at(Unorm8x4, 16, 2),
47        at(Unorm8x4, 24, 3),
48        at(Uint8x4, 28, 4),
49    ]
50}
51
52/// The palette as `MAX_BONES` matrices (identity padded; extra bones are dropped with a warning).
53pub fn palette_matrices(palette: &[Transform]) -> Vec<[f32; 16]> {
54    if palette.len() > MAX_BONES {
55        log::warn!(
56            "{} bones exceed the palette size {MAX_BONES}",
57            palette.len()
58        );
59    }
60    let mut m: Vec<[f32; 16]> = palette
61        .iter()
62        .take(MAX_BONES)
63        .map(Transform::to_matrix)
64        .collect();
65    m.resize(MAX_BONES, Transform::IDENTITY.to_matrix());
66    m
67}
68
69/// The palette's bind group layout; `dynamic` palettes are addressed per instance by offset.
70pub fn skin_layout(device: &wgpu::Device, dynamic: bool) -> wgpu::BindGroupLayout {
71    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
72        label: Some("skin"),
73        entries: &[wgpu::BindGroupLayoutEntry {
74            binding: 0,
75            visibility: wgpu::ShaderStages::VERTEX,
76            ty: wgpu::BindingType::Buffer {
77                ty: wgpu::BufferBindingType::Uniform,
78                has_dynamic_offset: dynamic,
79                min_binding_size: wgpu::BufferSize::new(PALETTE_BYTES),
80            },
81            count: None,
82        }],
83    })
84}