//! CPU picture of one skinned pose. This is the high-resolution color pass //! the render button can finish today. use crate::preview::{ eye_offset, mul, perspective, sample_skin_matrices, view_matrix, Mat4, PreviewCharacter, PreviewControls, }; pub fn raster_body( character: &PreviewCharacter, controls: &PreviewControls, width: u32, height: u32, ) -> Result, String> { if width == 0 || height == 0 { return Err("de afbeelding heeft geen pixels".to_string()); } if character.indices.len() < 3 || character.positions.is_empty() { return Err("het lichaam heeft geen mesh".to_string()); } let width = width as usize; let height = height as usize; let bones = sample_skin_matrices(character, &controls.clip_name, controls.time); let target = [0.0, 0.9, 0.0]; let offset = eye_offset( controls.yaw_deg, controls.pitch_deg, controls.distance * crate::preview::FREE_DISTANCE_FACTOR, ); let eye = [ target[0] + offset[0], target[1] + offset[1], target[2] + offset[2], ]; let view_proj = mul( perspective( crate::preview::free_fov_deg(), width as f32 / height as f32, 0.05, 100.0, ), view_matrix(eye, target), ); let mut color = [18u8, 22, 28, 255].repeat(width * height); let mut depth = vec![f32::INFINITY; width * height]; let positions = skinned_positions(character, &bones); for triangle in character.indices.chunks_exact(3) { let a = project(&view_proj, positions[triangle[0] as usize], width, height); let b = project(&view_proj, positions[triangle[1] as usize], width, height); let c = project(&view_proj, positions[triangle[2] as usize], width, height); if !a.on_screen && !b.on_screen && !c.on_screen { continue; } let min_x = a.x.min(b.x).min(c.x).floor().max(0.0) as usize; let max_x = a.x.max(b.x).max(c.x).ceil().min(width as f32 - 1.0) as usize; let min_y = a.y.min(b.y).min(c.y).floor().max(0.0) as usize; let max_y = a.y.max(b.y).max(c.y).ceil().min(height as f32 - 1.0) as usize; let area = edge(a.x, a.y, b.x, b.y, c.x, c.y); if area.abs() < 1e-5 { continue; } let shade = shade_triangle( positions[triangle[0] as usize], positions[triangle[1] as usize], positions[triangle[2] as usize], character.base_color, ); for y in min_y..=max_y { for x in min_x..=max_x { let px = x as f32 + 0.5; let py = y as f32 + 0.5; let w0 = edge(b.x, b.y, c.x, c.y, px, py) / area; let w1 = edge(c.x, c.y, a.x, a.y, px, py) / area; let w2 = edge(a.x, a.y, b.x, b.y, px, py) / area; if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 { continue; } let z = w0 * a.z + w1 * b.z + w2 * c.z; let index = y * width + x; if z.is_finite() && (-1.0..=1.0).contains(&z) && z < depth[index] { depth[index] = z; let pixel = &mut color[index * 4..index * 4 + 4]; pixel[0] = shade[0]; pixel[1] = shade[1]; pixel[2] = shade[2]; pixel[3] = 255; } } } } Ok(color) } struct Projected { x: f32, y: f32, z: f32, on_screen: bool, } fn project(view_proj: &Mat4, point: [f32; 3], width: usize, height: usize) -> Projected { let clip = transform_point(view_proj, point); let on_screen = clip[3] > 0.05; let inv = 1.0 / clip[3]; let ndc_x = clip[0] * inv; let ndc_y = clip[1] * inv; Projected { x: (ndc_x * 0.5 + 0.5) * width as f32, y: (1.0 - (ndc_y * 0.5 + 0.5)) * height as f32, z: clip[2] * inv, on_screen, } } fn transform_point(matrix: &Mat4, point: [f32; 3]) -> [f32; 4] { let [x, y, z] = point; [ matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12], matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13], matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14], matrix[3] * x + matrix[7] * y + matrix[11] * z + matrix[15], ] } fn edge(ax: f32, ay: f32, bx: f32, by: f32, cx: f32, cy: f32) -> f32 { (cx - ax) * (by - ay) - (cy - ay) * (bx - ax) } fn shade_triangle(a: [f32; 3], b: [f32; 3], c: [f32; 3], base: [f32; 4]) -> [u8; 3] { let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]]; let normal = [ ab[1] * ac[2] - ab[2] * ac[1], ab[2] * ac[0] - ab[0] * ac[2], ab[0] * ac[1] - ab[1] * ac[0], ]; let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt(); let light: [f32; 3] = [-0.25, 0.8, 0.45]; let light_len = (light[0] * light[0] + light[1] * light[1] + light[2] * light[2]).sqrt(); let mut ndot = 0.55; if length > 1e-6 { let dot = (normal[0] * light[0] + normal[1] * light[1] + normal[2] * light[2]).abs() / (length * light_len); ndot = 0.35 + 0.65 * dot; } [ (base[0] * ndot * 255.0).clamp(0.0, 255.0) as u8, (base[1] * ndot * 255.0).clamp(0.0, 255.0) as u8, (base[2] * ndot * 255.0).clamp(0.0, 255.0) as u8, ] } fn skinned_positions(character: &PreviewCharacter, bones: &[Mat4]) -> Vec<[f32; 3]> { character .positions .iter() .enumerate() .map(|(index, position)| { if bones.is_empty() || character.weights.len() != character.positions.len() { return *position; } let weights = character.weights[index]; let joints = character.joints.get(index).copied().unwrap_or([0, 0, 0, 0]); let mut mixed = [0.0; 16]; for slot in 0..4 { let weight = weights[slot]; if weight == 0.0 { continue; } let joint = joints[slot] as usize; let bone = bones .get(joint) .copied() .unwrap_or_else(crate::preview::identity); for value in 0..16 { mixed[value] += bone[value] * weight; } } let point = transform_point(&mixed, *position); [point[0], point[1], point[2]] }) .collect() } #[cfg(test)] mod tests { use super::*; use crate::preview::load_gltf_file; use std::path::PathBuf; #[test] fn front_surface_wins_independent_of_triangle_submission_order() { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/bodies/test-body-male.glb"); let mut character = load_gltf_file(&path).unwrap(); character.positions = vec![ [-0.5, 0.4, 0.25], [0.5, 0.4, 0.25], [0., 1.5, 0.25], [-0.5, 0.4, -0.25], [0.5, 0.4, -0.25], [0., 1.5, -0.95], ]; character.joint_nodes.clear(); character.inverse_binds.clear(); character.clips.clear(); character.base_color = [1., 1., 1., 1.]; let controls = PreviewControls::default(); character.indices = vec![0, 1, 2]; let front = raster_body(&character, &controls, 96, 128).unwrap(); character.indices = vec![3, 4, 5]; let back = raster_body(&character, &controls, 96, 128).unwrap(); character.indices = vec![0, 1, 2, 3, 4, 5]; let first = raster_body(&character, &controls, 96, 128).unwrap(); character.indices = vec![3, 4, 5, 0, 1, 2]; let reversed = raster_body(&character, &controls, 96, 128).unwrap(); assert_eq!(first, reversed); let pixel = (64 * 96 + 48) * 4; assert_ne!(&front[pixel..pixel + 4], &back[pixel..pixel + 4]); assert_eq!(&first[pixel..pixel + 4], &front[pixel..pixel + 4]); } #[test] fn male_walk_still_has_a_visible_body() { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/bodies/test-body-male.glb"); let character = load_gltf_file(&path).unwrap(); let controls = PreviewControls::default(); let image = raster_body(&character, &controls, 96, 128).unwrap(); let body = image .chunks_exact(4) .filter(|pixel| pixel[0] > 40 || pixel[2] > 40) .filter(|pixel| pixel[0] != 18 || pixel[1] != 22 || pixel[2] != 28) .count(); assert!(body > 30, "body pixels {body}"); } }