use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::resources::DeviceResources;
use crate::resources::mesh::mesh_store::MeshId;
use crate::resources::mesh_sidecar::registry::{DeformStage, DeformerDesc, DeformerId};
pub struct SkinnedMeshUpdate {
pub mesh_id: MeshId,
pub positions: Vec<[f32; 3]>,
pub normals: Vec<[f32; 3]>,
}
pub struct SkinnedPoseUpdate {
pub mesh_id: MeshId,
pub instance_id: u32,
pub joint_matrices: Vec<glam::Mat4>,
}
#[derive(Clone)]
pub struct SkinWeights {
pub joint_indices: Vec<[u8; 4]>,
pub joint_weights: Vec<[f32; 4]>,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct PackedSkinVertex {
weights: [f32; 4],
joints_01: u32,
joints_23: u32,
}
const SKIN_WEIGHT_STRIDE_BYTES: u32 = 24;
const SKIN_PALETTE_STRIDE_BYTES: u32 = 64;
pub const DEFORM_PRIORITY_SKINNING: i32 = -1000;
const SKINNING_DEFORMER_NAME: &str = "viewport_skin";
const SKINNING_DEFORMER_BODY: &str = r#"
fn deform(v: DeformVertex, ctx: DeformContext) -> DeformVertex {
var out = v;
let slot = ctx.slot;
if deform_slot_stride(slot) == 0u {
return out;
}
let vi = v.vertex_index;
let w0 = deform_read_f32(slot, vi, 0u);
let w1 = deform_read_f32(slot, vi, 1u);
let w2 = deform_read_f32(slot, vi, 2u);
let w3 = deform_read_f32(slot, vi, 3u);
let j01 = deform_read_u32(slot, vi, 4u);
let j23 = deform_read_u32(slot, vi, 5u);
let j0 = j01 & 0xFFFFu;
let j1 = (j01 >> 16u) & 0xFFFFu;
let j2 = j23 & 0xFFFFu;
let j3 = (j23 >> 16u) & 0xFFFFu;
let m = deform_read_instance_mat4(slot, j0) * w0
+ deform_read_instance_mat4(slot, j1) * w1
+ deform_read_instance_mat4(slot, j2) * w2
+ deform_read_instance_mat4(slot, j3) * w3;
out.position = (m * vec4<f32>(v.position, 1.0)).xyz;
let m3 = mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz);
out.normal = m3 * v.normal;
return out;
}
"#;
#[derive(Clone)]
pub struct SkinningPlugin {
deformer_id: DeformerId,
skinned_meshes: Arc<Mutex<HashMap<MeshId, ()>>>,
}
impl SkinningPlugin {
pub fn install(
resources: &mut DeviceResources,
device: &wgpu::Device,
) -> crate::error::ViewportResult<Self> {
let deformer_id = match resources.deformer_id_by_name(SKINNING_DEFORMER_NAME) {
Some(id) => id,
None => {
let desc = DeformerDesc {
name: SKINNING_DEFORMER_NAME,
stage: DeformStage::ObjectSpace,
priority: DEFORM_PRIORITY_SKINNING,
wgsl_body: SKINNING_DEFORMER_BODY.to_string(),
per_vertex_stride: SKIN_WEIGHT_STRIDE_BYTES,
};
resources.register_internal_deformer(device, desc)?
}
};
Ok(Self {
deformer_id,
skinned_meshes: Arc::new(Mutex::new(HashMap::new())),
})
}
pub fn deformer_id(&self) -> DeformerId {
self.deformer_id
}
pub fn attach_weights(
&self,
resources: &mut DeviceResources,
device: &wgpu::Device,
mesh_id: MeshId,
weights: &SkinWeights,
) {
let packed = pack(weights);
let tight = pack_skin_weights_tight(&packed);
resources.deform.attach_slot(
device,
mesh_id,
self.deformer_id.slot(),
SKIN_WEIGHT_STRIDE_BYTES / 4,
&tight,
);
self.skinned_meshes
.lock()
.expect("skinning marker poisoned")
.insert(mesh_id, ());
}
pub fn begin_upload_weights(
&self,
resources: &mut DeviceResources,
device: &wgpu::Device,
mesh_id: MeshId,
weights: SkinWeights,
) -> crate::resources::JobId {
let device_for_apply = device.clone();
let slot = self.deformer_id.slot();
let marker = self.skinned_meshes.clone();
let mut runner = resources.jobs.lock().expect("upload job runner poisoned");
runner.submit_cpu(move |progress| {
progress.set(0.2);
let packed = pack(&weights);
let tight = pack_skin_weights_tight(&packed);
progress.set(0.9);
Ok(crate::resources::upload_jobs::JobProduct::with_apply(
Box::new(move |resources: &mut DeviceResources| {
resources.deform.attach_slot(
&device_for_apply,
mesh_id,
slot,
SKIN_WEIGHT_STRIDE_BYTES / 4,
&tight,
);
marker
.lock()
.expect("skinning marker poisoned")
.insert(mesh_id, ());
}),
))
})
}
pub fn attach_palette(
&self,
resources: &mut DeviceResources,
device: &wgpu::Device,
queue: &wgpu::Queue,
mesh_id: MeshId,
instance_id: u32,
palette: &[glam::Mat4],
) -> bool {
if !self
.skinned_meshes
.lock()
.expect("skinning marker poisoned")
.contains_key(&mesh_id)
{
return false;
}
let bytes: Vec<[[f32; 4]; 4]> = palette.iter().map(|m| m.to_cols_array_2d()).collect();
resources.deform.attach_slot_instance(
device,
queue,
mesh_id,
instance_id,
self.deformer_id.slot(),
SKIN_PALETTE_STRIDE_BYTES / 4,
bytemuck::cast_slice(&bytes),
);
true
}
pub fn is_skinned_mesh(&self, mesh_id: MeshId) -> bool {
self.skinned_meshes
.lock()
.expect("skinning marker poisoned")
.contains_key(&mesh_id)
}
pub fn detach_weights(
&self,
resources: &mut DeviceResources,
device: &wgpu::Device,
mesh_id: MeshId,
) -> bool {
let removed = resources.detach_deform_slot(device, mesh_id, self.deformer_id.slot());
self.skinned_meshes
.lock()
.expect("skinning marker poisoned")
.remove(&mesh_id);
removed
}
pub fn detach_palette(
&self,
resources: &mut DeviceResources,
device: &wgpu::Device,
queue: &wgpu::Queue,
mesh_id: MeshId,
instance_id: u32,
) -> bool {
resources.detach_deform_slot_instance(
device,
queue,
mesh_id,
instance_id,
self.deformer_id.slot(),
)
}
pub fn uninstall(self, resources: &mut DeviceResources, device: &wgpu::Device) {
let slot = self.deformer_id.slot();
let meshes: Vec<MeshId> = {
let marker = self
.skinned_meshes
.lock()
.expect("skinning marker poisoned");
marker.keys().copied().collect()
};
for mesh_id in meshes {
resources.detach_deform_slot(device, mesh_id, slot);
}
self.skinned_meshes
.lock()
.expect("skinning marker poisoned")
.clear();
}
}
fn pack(weights: &SkinWeights) -> Vec<PackedSkinVertex> {
weights
.joint_indices
.iter()
.zip(weights.joint_weights.iter())
.map(|(j, w)| {
let j0 = j[0] as u32;
let j1 = j[1] as u32;
let j2 = j[2] as u32;
let j3 = j[3] as u32;
PackedSkinVertex {
weights: *w,
joints_01: j0 | (j1 << 16),
joints_23: j2 | (j3 << 16),
}
})
.collect()
}
fn pack_skin_weights_tight(packed: &[PackedSkinVertex]) -> Vec<u8> {
let mut out = Vec::with_capacity(packed.len() * SKIN_WEIGHT_STRIDE_BYTES as usize);
for v in packed {
out.extend_from_slice(bytemuck::bytes_of(&v.weights));
out.extend_from_slice(bytemuck::bytes_of(&v.joints_01));
out.extend_from_slice(bytemuck::bytes_of(&v.joints_23));
}
out
}
#[cfg(test)]
mod async_skin_tests {
use super::SkinWeights;
use super::SkinningPlugin;
use crate::DeviceResources;
use crate::geometry::primitives;
use crate::resources::UploadStatus;
fn try_make_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: None,
force_fallback_adapter: false,
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
}
fn unit_weights(vertex_count: usize) -> SkinWeights {
SkinWeights {
joint_indices: vec![[0u8; 4]; vertex_count],
joint_weights: vec![[1.0, 0.0, 0.0, 0.0]; vertex_count],
}
}
#[test]
fn attach_weights_marks_mesh_skinnable() {
let Some((device, _queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let skinning = SkinningPlugin::install(&mut resources, &device).unwrap();
let plane = primitives::grid_plane(1.0, 1.0, 4, 4);
let mesh_id = resources.upload_mesh_data(&device, &plane).unwrap();
let weights = unit_weights(plane.positions.len());
skinning.attach_weights(&mut resources, &device, mesh_id, &weights);
assert!(skinning.is_skinned_mesh(mesh_id));
}
#[test]
fn detach_weights_unmarks_mesh() {
let Some((device, _queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let skinning = SkinningPlugin::install(&mut resources, &device).unwrap();
let plane = primitives::grid_plane(1.0, 1.0, 4, 4);
let mesh_id = resources.upload_mesh_data(&device, &plane).unwrap();
skinning.attach_weights(
&mut resources,
&device,
mesh_id,
&unit_weights(plane.positions.len()),
);
assert!(skinning.is_skinned_mesh(mesh_id));
assert!(skinning.detach_weights(&mut resources, &device, mesh_id));
assert!(
!skinning.is_skinned_mesh(mesh_id),
"detach must unmark the mesh"
);
assert!(
!skinning.detach_weights(&mut resources, &device, mesh_id),
"detaching again removes nothing"
);
}
#[test]
fn uninstall_detaches_all_skinned_meshes() {
let Some((device, _queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let skinning = SkinningPlugin::install(&mut resources, &device).unwrap();
let plane = primitives::grid_plane(1.0, 1.0, 4, 4);
let a = resources.upload_mesh_data(&device, &plane).unwrap();
let b = resources.upload_mesh_data(&device, &plane).unwrap();
let w = unit_weights(plane.positions.len());
skinning.attach_weights(&mut resources, &device, a, &w);
skinning.attach_weights(&mut resources, &device, b, &w);
let slot = skinning.deformer_id().slot();
let probe = skinning.clone();
skinning.uninstall(&mut resources, &device);
assert!(!probe.is_skinned_mesh(a));
assert!(!probe.is_skinned_mesh(b));
assert!(
!resources.has_deform_slot(a, slot),
"weight slot must be freed"
);
assert!(
!resources.has_deform_slot(b, slot),
"weight slot must be freed"
);
}
#[test]
fn begin_upload_weights_completes() {
let Some((device, queue)) = try_make_device() else {
eprintln!("skipping: no wgpu adapter available");
return;
};
let mut resources = DeviceResources::new(&device, wgpu::TextureFormat::Rgba8UnormSrgb, 1);
let skinning = SkinningPlugin::install(&mut resources, &device).unwrap();
let plane = primitives::grid_plane(1.0, 1.0, 4, 4);
let mesh_id = resources.upload_mesh_data(&device, &plane).unwrap();
let weights = unit_weights(plane.positions.len());
assert!(!skinning.is_skinned_mesh(mesh_id));
let id = skinning.begin_upload_weights(&mut resources, &device, mesh_id, weights);
for _ in 0..200 {
resources.process_uploads(&device, &queue);
match resources.upload_status(id) {
UploadStatus::Ready => break,
UploadStatus::Failed(e) => panic!("upload failed: {e:?}"),
UploadStatus::Pending { .. } => {
std::thread::sleep(std::time::Duration::from_millis(5));
}
UploadStatus::Unknown => panic!("job disappeared"),
}
}
assert!(
skinning.is_skinned_mesh(mesh_id),
"skin weights did not install"
);
}
}