BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / still.rs

still.rs

still.rs 8.5 KiB Download Raw
1//! CPU picture of one skinned pose. This is the high-resolution color pass
2//! the render button can finish today.
3
4use crate::preview::{
5 eye_offset, mul, perspective, sample_skin_matrices, view_matrix, Mat4, PreviewCharacter,
6 PreviewControls,
7};
8
9pub 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.is_finite() && (-1.0..=1.0).contains(&z) && 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
85struct Projected {
86 x: f32,
87 y: f32,
88 z: f32,
89 on_screen: bool,
90}
91
92fn 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
106fn 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
116fn 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
120fn 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
144fn 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)]
177mod tests {
178 use super::*;
179 use crate::preview::load_gltf_file;
180 use std::path::PathBuf;
181
182 #[test]
183 fn front_surface_wins_independent_of_triangle_submission_order() {
184 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
185 .join("tests/fixtures/bodies/test-body-male.glb");
186 let mut character = load_gltf_file(&path).unwrap();
187 character.positions = vec![
188 [-0.5, 0.4, 0.25],
189 [0.5, 0.4, 0.25],
190 [0., 1.5, 0.25],
191 [-0.5, 0.4, -0.25],
192 [0.5, 0.4, -0.25],
193 [0., 1.5, -0.95],
194 ];
195 character.joint_nodes.clear();
196 character.inverse_binds.clear();
197 character.clips.clear();
198 character.base_color = [1., 1., 1., 1.];
199 let controls = PreviewControls::default();
200 character.indices = vec![0, 1, 2];
201 let front = raster_body(&character, &controls, 96, 128).unwrap();
202 character.indices = vec![3, 4, 5];
203 let back = raster_body(&character, &controls, 96, 128).unwrap();
204 character.indices = vec![0, 1, 2, 3, 4, 5];
205 let first = raster_body(&character, &controls, 96, 128).unwrap();
206 character.indices = vec![3, 4, 5, 0, 1, 2];
207 let reversed = raster_body(&character, &controls, 96, 128).unwrap();
208 assert_eq!(first, reversed);
209 let pixel = (64 * 96 + 48) * 4;
210 assert_ne!(&front[pixel..pixel + 4], &back[pixel..pixel + 4]);
211 assert_eq!(&first[pixel..pixel + 4], &front[pixel..pixel + 4]);
212 }
213
214 #[test]
215 fn male_walk_still_has_a_visible_body() {
216 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
217 .join("tests/fixtures/bodies/test-body-male.glb");
218 let character = load_gltf_file(&path).unwrap();
219 let controls = PreviewControls::default();
220 let image = raster_body(&character, &controls, 96, 128).unwrap();
221 let body = image
222 .chunks_exact(4)
223 .filter(|pixel| pixel[0] > 40 || pixel[2] > 40)
224 .filter(|pixel| pixel[0] != 18 || pixel[1] != 22 || pixel[2] != 28)
225 .count();
226 assert!(body > 30, "body pixels {body}");
227 }
228}