BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / still.rs

still.rs

still.rs 8.6 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(
26 controls.yaw_deg,
27 controls.pitch_deg,
28 controls.distance * crate::preview::FREE_DISTANCE_FACTOR,
29 );
30 let eye = [
31 target[0] + offset[0],
32 target[1] + offset[1],
33 target[2] + offset[2],
34 ];
35 let view_proj = mul(
36 perspective(
37 crate::preview::free_fov_deg(),
38 width as f32 / height as f32,
39 0.05,
40 100.0,
41 ),
42 view_matrix(eye, target),
43 );
44 let mut color = [18u8, 22, 28, 255].repeat(width * height);
45 let mut depth = vec![f32::INFINITY; width * height];
46 let positions = skinned_positions(character, &bones);
47 for triangle in character.indices.chunks_exact(3) {
48 let a = project(&view_proj, positions[triangle[0] as usize], width, height);
49 let b = project(&view_proj, positions[triangle[1] as usize], width, height);
50 let c = project(&view_proj, positions[triangle[2] as usize], width, height);
51 if !a.on_screen && !b.on_screen && !c.on_screen {
52 continue;
53 }
54 let min_x = a.x.min(b.x).min(c.x).floor().max(0.0) as usize;
55 let max_x = a.x.max(b.x).max(c.x).ceil().min(width as f32 - 1.0) as usize;
56 let min_y = a.y.min(b.y).min(c.y).floor().max(0.0) as usize;
57 let max_y = a.y.max(b.y).max(c.y).ceil().min(height as f32 - 1.0) as usize;
58 let area = edge(a.x, a.y, b.x, b.y, c.x, c.y);
59 if area.abs() < 1e-5 {
60 continue;
61 }
62 let shade = shade_triangle(
63 positions[triangle[0] as usize],
64 positions[triangle[1] as usize],
65 positions[triangle[2] as usize],
66 character.base_color,
67 );
68 for y in min_y..=max_y {
69 for x in min_x..=max_x {
70 let px = x as f32 + 0.5;
71 let py = y as f32 + 0.5;
72 let w0 = edge(b.x, b.y, c.x, c.y, px, py) / area;
73 let w1 = edge(c.x, c.y, a.x, a.y, px, py) / area;
74 let w2 = edge(a.x, a.y, b.x, b.y, px, py) / area;
75 if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
76 continue;
77 }
78 let z = w0 * a.z + w1 * b.z + w2 * c.z;
79 let index = y * width + x;
80 if z.is_finite() && (-1.0..=1.0).contains(&z) && z < depth[index] {
81 depth[index] = z;
82 let pixel = &mut color[index * 4..index * 4 + 4];
83 pixel[0] = shade[0];
84 pixel[1] = shade[1];
85 pixel[2] = shade[2];
86 pixel[3] = 255;
87 }
88 }
89 }
90 }
91 Ok(color)
92}
93
94struct Projected {
95 x: f32,
96 y: f32,
97 z: f32,
98 on_screen: bool,
99}
100
101fn project(view_proj: &Mat4, point: [f32; 3], width: usize, height: usize) -> Projected {
102 let clip = transform_point(view_proj, point);
103 let on_screen = clip[3] > 0.05;
104 let inv = 1.0 / clip[3];
105 let ndc_x = clip[0] * inv;
106 let ndc_y = clip[1] * inv;
107 Projected {
108 x: (ndc_x * 0.5 + 0.5) * width as f32,
109 y: (1.0 - (ndc_y * 0.5 + 0.5)) * height as f32,
110 z: clip[2] * inv,
111 on_screen,
112 }
113}
114
115fn transform_point(matrix: &Mat4, point: [f32; 3]) -> [f32; 4] {
116 let [x, y, z] = point;
117 [
118 matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
119 matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
120 matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14],
121 matrix[3] * x + matrix[7] * y + matrix[11] * z + matrix[15],
122 ]
123}
124
125fn edge(ax: f32, ay: f32, bx: f32, by: f32, cx: f32, cy: f32) -> f32 {
126 (cx - ax) * (by - ay) - (cy - ay) * (bx - ax)
127}
128
129fn shade_triangle(a: [f32; 3], b: [f32; 3], c: [f32; 3], base: [f32; 4]) -> [u8; 3] {
130 let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
131 let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
132 let normal = [
133 ab[1] * ac[2] - ab[2] * ac[1],
134 ab[2] * ac[0] - ab[0] * ac[2],
135 ab[0] * ac[1] - ab[1] * ac[0],
136 ];
137 let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
138 let light: [f32; 3] = [-0.25, 0.8, 0.45];
139 let light_len = (light[0] * light[0] + light[1] * light[1] + light[2] * light[2]).sqrt();
140 let mut ndot = 0.55;
141 if length > 1e-6 {
142 let dot = (normal[0] * light[0] + normal[1] * light[1] + normal[2] * light[2]).abs()
143 / (length * light_len);
144 ndot = 0.35 + 0.65 * dot;
145 }
146 [
147 (base[0] * ndot * 255.0).clamp(0.0, 255.0) as u8,
148 (base[1] * ndot * 255.0).clamp(0.0, 255.0) as u8,
149 (base[2] * ndot * 255.0).clamp(0.0, 255.0) as u8,
150 ]
151}
152
153fn skinned_positions(character: &PreviewCharacter, bones: &[Mat4]) -> Vec<[f32; 3]> {
154 character
155 .positions
156 .iter()
157 .enumerate()
158 .map(|(index, position)| {
159 if bones.is_empty() || character.weights.len() != character.positions.len() {
160 return *position;
161 }
162 let weights = character.weights[index];
163 let joints = character.joints.get(index).copied().unwrap_or([0, 0, 0, 0]);
164 let mut mixed = [0.0; 16];
165 for slot in 0..4 {
166 let weight = weights[slot];
167 if weight == 0.0 {
168 continue;
169 }
170 let joint = joints[slot] as usize;
171 let bone = bones
172 .get(joint)
173 .copied()
174 .unwrap_or_else(crate::preview::identity);
175 for value in 0..16 {
176 mixed[value] += bone[value] * weight;
177 }
178 }
179 let point = transform_point(&mixed, *position);
180 [point[0], point[1], point[2]]
181 })
182 .collect()
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use crate::preview::load_gltf_file;
189 use std::path::PathBuf;
190
191 #[test]
192 fn front_surface_wins_independent_of_triangle_submission_order() {
193 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
194 .join("tests/fixtures/bodies/test-body-male.glb");
195 let mut character = load_gltf_file(&path).unwrap();
196 character.positions = vec![
197 [-0.5, 0.4, 0.25],
198 [0.5, 0.4, 0.25],
199 [0., 1.5, 0.25],
200 [-0.5, 0.4, -0.25],
201 [0.5, 0.4, -0.25],
202 [0., 1.5, -0.95],
203 ];
204 character.joint_nodes.clear();
205 character.inverse_binds.clear();
206 character.clips.clear();
207 character.base_color = [1., 1., 1., 1.];
208 let controls = PreviewControls::default();
209 character.indices = vec![0, 1, 2];
210 let front = raster_body(&character, &controls, 96, 128).unwrap();
211 character.indices = vec![3, 4, 5];
212 let back = raster_body(&character, &controls, 96, 128).unwrap();
213 character.indices = vec![0, 1, 2, 3, 4, 5];
214 let first = raster_body(&character, &controls, 96, 128).unwrap();
215 character.indices = vec![3, 4, 5, 0, 1, 2];
216 let reversed = raster_body(&character, &controls, 96, 128).unwrap();
217 assert_eq!(first, reversed);
218 let pixel = (64 * 96 + 48) * 4;
219 assert_ne!(&front[pixel..pixel + 4], &back[pixel..pixel + 4]);
220 assert_eq!(&first[pixel..pixel + 4], &front[pixel..pixel + 4]);
221 }
222
223 #[test]
224 fn male_walk_still_has_a_visible_body() {
225 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
226 .join("tests/fixtures/bodies/test-body-male.glb");
227 let character = load_gltf_file(&path).unwrap();
228 let controls = PreviewControls::default();
229 let image = raster_body(&character, &controls, 96, 128).unwrap();
230 let body = image
231 .chunks_exact(4)
232 .filter(|pixel| pixel[0] > 40 || pixel[2] > 40)
233 .filter(|pixel| pixel[0] != 18 || pixel[1] != 22 || pixel[2] != 28)
234 .count();
235 assert!(body > 30, "body pixels {body}");
236 }
237}