| 1 | //! CPU picture of one skinned pose. This is the high-resolution color pass |
| 2 | //! the render button can finish today. |
| 3 | |
| 4 | use crate::preview::{ |
| 5 | eye_offset, mul, perspective, sample_skin_matrices, view_matrix, Mat4, PreviewCharacter, |
| 6 | PreviewControls, |
| 7 | }; |
| 8 | |
| 9 | pub fn raster_body( |
| 10 | character: &PreviewCharacter, |
| 11 | controls: &PreviewControls, |
| 12 | width: u32, |
| 13 | height: u32, |
| 14 | ) -> Result<Vec<u8>, String> { |
| 15 | if width == 0 || height == 0 { |
| 16 | return Err("de afbeelding heeft geen pixels".to_string()); |
| 17 | } |
| 18 | if character.indices.len() < 3 || character.positions.is_empty() { |
| 19 | return Err("het lichaam heeft geen mesh".to_string()); |
| 20 | } |
| 21 | let width = width as usize; |
| 22 | let height = height as usize; |
| 23 | let bones = sample_skin_matrices(character, &controls.clip_name, controls.time); |
| 24 | let target = [0.0, 0.9, 0.0]; |
| 25 | let offset = eye_offset(controls.yaw_deg, controls.pitch_deg, controls.distance); |
| 26 | let eye = [ |
| 27 | target[0] + offset[0], |
| 28 | target[1] + offset[1], |
| 29 | target[2] + offset[2], |
| 30 | ]; |
| 31 | let view_proj = mul( |
| 32 | perspective(38.0, width as f32 / height as f32, 0.05, 40.0), |
| 33 | view_matrix(eye, target), |
| 34 | ); |
| 35 | let mut color = [18u8, 22, 28, 255].repeat(width * height); |
| 36 | let mut depth = vec![f32::INFINITY; width * height]; |
| 37 | let positions = skinned_positions(character, &bones); |
| 38 | for triangle in character.indices.chunks_exact(3) { |
| 39 | let a = project(&view_proj, positions[triangle[0] as usize], width, height); |
| 40 | let b = project(&view_proj, positions[triangle[1] as usize], width, height); |
| 41 | let c = project(&view_proj, positions[triangle[2] as usize], width, height); |
| 42 | if !a.on_screen && !b.on_screen && !c.on_screen { |
| 43 | continue; |
| 44 | } |
| 45 | let min_x = a.x.min(b.x).min(c.x).floor().max(0.0) as usize; |
| 46 | let max_x = a.x.max(b.x).max(c.x).ceil().min(width as f32 - 1.0) as usize; |
| 47 | let min_y = a.y.min(b.y).min(c.y).floor().max(0.0) as usize; |
| 48 | let max_y = a.y.max(b.y).max(c.y).ceil().min(height as f32 - 1.0) as usize; |
| 49 | let area = edge(a.x, a.y, b.x, b.y, c.x, c.y); |
| 50 | if area.abs() < 1e-5 { |
| 51 | continue; |
| 52 | } |
| 53 | let shade = shade_triangle( |
| 54 | positions[triangle[0] as usize], |
| 55 | positions[triangle[1] as usize], |
| 56 | positions[triangle[2] as usize], |
| 57 | character.base_color, |
| 58 | ); |
| 59 | for y in min_y..=max_y { |
| 60 | for x in min_x..=max_x { |
| 61 | let px = x as f32 + 0.5; |
| 62 | let py = y as f32 + 0.5; |
| 63 | let w0 = edge(b.x, b.y, c.x, c.y, px, py) / area; |
| 64 | let w1 = edge(c.x, c.y, a.x, a.y, px, py) / area; |
| 65 | let w2 = edge(a.x, a.y, b.x, b.y, px, py) / area; |
| 66 | if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 { |
| 67 | continue; |
| 68 | } |
| 69 | let z = w0 * a.z + w1 * b.z + w2 * c.z; |
| 70 | let index = y * width + x; |
| 71 | if z < depth[index] { |
| 72 | depth[index] = z; |
| 73 | let pixel = &mut color[index * 4..index * 4 + 4]; |
| 74 | pixel[0] = shade[0]; |
| 75 | pixel[1] = shade[1]; |
| 76 | pixel[2] = shade[2]; |
| 77 | pixel[3] = 255; |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | Ok(color) |
| 83 | } |
| 84 | |
| 85 | struct Projected { |
| 86 | x: f32, |
| 87 | y: f32, |
| 88 | z: f32, |
| 89 | on_screen: bool, |
| 90 | } |
| 91 | |
| 92 | fn project(view_proj: &Mat4, point: [f32; 3], width: usize, height: usize) -> Projected { |
| 93 | let clip = transform_point(view_proj, point); |
| 94 | let on_screen = clip[3] > 0.05; |
| 95 | let inv = 1.0 / clip[3]; |
| 96 | let ndc_x = clip[0] * inv; |
| 97 | let ndc_y = clip[1] * inv; |
| 98 | Projected { |
| 99 | x: (ndc_x * 0.5 + 0.5) * width as f32, |
| 100 | y: (1.0 - (ndc_y * 0.5 + 0.5)) * height as f32, |
| 101 | z: clip[2] * inv, |
| 102 | on_screen, |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | fn transform_point(matrix: &Mat4, point: [f32; 3]) -> [f32; 4] { |
| 107 | let [x, y, z] = point; |
| 108 | [ |
| 109 | matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12], |
| 110 | matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13], |
| 111 | matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14], |
| 112 | matrix[3] * x + matrix[7] * y + matrix[11] * z + matrix[15], |
| 113 | ] |
| 114 | } |
| 115 | |
| 116 | fn edge(ax: f32, ay: f32, bx: f32, by: f32, cx: f32, cy: f32) -> f32 { |
| 117 | (cx - ax) * (by - ay) - (cy - ay) * (bx - ax) |
| 118 | } |
| 119 | |
| 120 | fn shade_triangle(a: [f32; 3], b: [f32; 3], c: [f32; 3], base: [f32; 4]) -> [u8; 3] { |
| 121 | let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; |
| 122 | let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]]; |
| 123 | let normal = [ |
| 124 | ab[1] * ac[2] - ab[2] * ac[1], |
| 125 | ab[2] * ac[0] - ab[0] * ac[2], |
| 126 | ab[0] * ac[1] - ab[1] * ac[0], |
| 127 | ]; |
| 128 | let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt(); |
| 129 | let light: [f32; 3] = [-0.25, 0.8, 0.45]; |
| 130 | let light_len = (light[0] * light[0] + light[1] * light[1] + light[2] * light[2]).sqrt(); |
| 131 | let mut ndot = 0.55; |
| 132 | if length > 1e-6 { |
| 133 | let dot = (normal[0] * light[0] + normal[1] * light[1] + normal[2] * light[2]).abs() |
| 134 | / (length * light_len); |
| 135 | ndot = 0.35 + 0.65 * dot; |
| 136 | } |
| 137 | [ |
| 138 | (base[0] * ndot * 255.0).clamp(0.0, 255.0) as u8, |
| 139 | (base[1] * ndot * 255.0).clamp(0.0, 255.0) as u8, |
| 140 | (base[2] * ndot * 255.0).clamp(0.0, 255.0) as u8, |
| 141 | ] |
| 142 | } |
| 143 | |
| 144 | fn skinned_positions(character: &PreviewCharacter, bones: &[Mat4]) -> Vec<[f32; 3]> { |
| 145 | character |
| 146 | .positions |
| 147 | .iter() |
| 148 | .enumerate() |
| 149 | .map(|(index, position)| { |
| 150 | if bones.is_empty() || character.weights.len() != character.positions.len() { |
| 151 | return *position; |
| 152 | } |
| 153 | let weights = character.weights[index]; |
| 154 | let joints = character.joints.get(index).copied().unwrap_or([0, 0, 0, 0]); |
| 155 | let mut mixed = [0.0; 16]; |
| 156 | for slot in 0..4 { |
| 157 | let weight = weights[slot]; |
| 158 | if weight == 0.0 { |
| 159 | continue; |
| 160 | } |
| 161 | let joint = joints[slot] as usize; |
| 162 | let bone = bones |
| 163 | .get(joint) |
| 164 | .copied() |
| 165 | .unwrap_or_else(crate::preview::identity); |
| 166 | for value in 0..16 { |
| 167 | mixed[value] += bone[value] * weight; |
| 168 | } |
| 169 | } |
| 170 | let point = transform_point(&mixed, *position); |
| 171 | [point[0], point[1], point[2]] |
| 172 | }) |
| 173 | .collect() |
| 174 | } |
| 175 | |
| 176 | #[cfg(test)] |
| 177 | mod tests { |
| 178 | use super::*; |
| 179 | use crate::preview::load_gltf_file; |
| 180 | use std::path::PathBuf; |
| 181 | |
| 182 | #[test] |
| 183 | fn male_walk_still_has_a_visible_body() { |
| 184 | let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) |
| 185 | .join("tests/fixtures/bodies/test-body-male.glb"); |
| 186 | let character = load_gltf_file(&path).unwrap(); |
| 187 | let controls = PreviewControls::default(); |
| 188 | let image = raster_body(&character, &controls, 96, 128).unwrap(); |
| 189 | let body = image |
| 190 | .chunks_exact(4) |
| 191 | .filter(|pixel| pixel[0] > 40 || pixel[2] > 40) |
| 192 | .filter(|pixel| pixel[0] != 18 || pixel[1] != 22 || pixel[2] != 28) |
| 193 | .count(); |
| 194 | assert!(body > 30, "body pixels {body}"); |
| 195 | } |
| 196 | } |