BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / examples / bake_female_hair.rs

bake_female_hair.rs

bake_female_hair.rs 5.3 KiB Download Raw
1#![allow(clippy::needless_range_loop)]
2//! Bakes the hair of Tim's original "Animated Female Base Mesh" into the
3//! bundled rigid equipment preset `female-hair` (design.md, t1019u): the hair
4//! primitive (material ponytail01, last primitive) is skinned once at the rest
5//! pose, expressed in the rest-pose space of the `head` joint, written as
6//! OBJ + MTL + PNG, imported with the normal OBJ importer and bound to `head`
7//! of the edited default female with zero offset.
8//! Usage: bake_female_hair ORIGINAL_FEMALE.gltf HAIR_TEXTURE.png EDITED_FEMALE.gltf OUT.ppe
9use paperdoll_renderer::{equipment, preview};
10use std::io::Write;
11
12fn inverse(m: [f32; 16]) -> [f32; 16] {
13 let mut a = [[0f64; 8]; 4];
14 for r in 0..4 {
15 for c in 0..4 {
16 a[r][c] = m[c * 4 + r] as f64;
17 }
18 a[r][4 + r] = 1.;
19 }
20 for c in 0..4 {
21 let p = (c..4)
22 .max_by(|&x, &y| a[x][c].abs().total_cmp(&a[y][c].abs()))
23 .unwrap();
24 a.swap(c, p);
25 let d = a[c][c];
26 for k in 0..8 {
27 a[c][k] /= d;
28 }
29 for r in 0..4 {
30 if r != c {
31 let f = a[r][c];
32 for k in 0..8 {
33 a[r][k] -= f * a[c][k];
34 }
35 }
36 }
37 }
38 let mut out = [0f32; 16];
39 for r in 0..4 {
40 for c in 0..4 {
41 out[c * 4 + r] = a[r][4 + c] as f32;
42 }
43 }
44 out
45}
46fn apply(m: &[f32; 16], p: [f32; 3], w: f32) -> [f32; 3] {
47 [0, 1, 2].map(|i| m[i] * p[0] + m[4 + i] * p[1] + m[8 + i] * p[2] + m[12 + i] * w)
48}
49
50fn main() -> Result<(), Box<dyn std::error::Error>> {
51 let a: Vec<String> = std::env::args().skip(1).collect();
52 let body = preview::load_body_file(std::path::Path::new(&a[0]))?;
53 let hair = body.surfaces.last().ok_or("no surfaces")?;
54 let bones: Vec<[f32; 16]> = (0..body.joint_nodes.len())
55 .map(|j| {
56 preview::mul(
57 preview::rest_joint_world(&body, j).unwrap(),
58 body.inverse_binds[j],
59 )
60 })
61 .collect();
62 let head = body
63 .joint_names
64 .iter()
65 .position(|n| n.to_ascii_lowercase().starts_with("head"))
66 .ok_or("no head joint")?;
67 let head_world = preview::animated_joint_world(&body, "", 0.0, head).ok_or("head")?;
68 let to_head = inverse(head_world);
69 let idx = &body.indices[hair.first..hair.first + hair.count];
70 let mut used: Vec<u32> = idx.to_vec();
71 used.sort_unstable();
72 used.dedup();
73 let map: std::collections::HashMap<u32, usize> =
74 used.iter().enumerate().map(|(i, v)| (*v, i + 1)).collect();
75 let dir = tempdir();
76 let mut obj = std::fs::File::create(dir.join("female-hair.obj"))?;
77 writeln!(obj, "# Hair of \"Animated Female Base Mesh\" by assetfactory (Sketchfab Standard), baked rigid to {}", body.joint_names[head])?;
78 writeln!(obj, "mtllib female-hair.mtl\nusemtl hair")?;
79 for v in &used {
80 let i = *v as usize;
81 let mut m = [0f32; 16];
82 for (j, w) in body.joints[i].iter().zip(body.weights[i]) {
83 for k in 0..16 {
84 m[k] += bones[*j as usize][k] * w;
85 }
86 }
87 let p = apply(&to_head, apply(&m, body.positions[i], 1.), 1.);
88 let n = apply(&to_head, apply(&m, body.normals[i], 0.), 0.);
89 let l = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt().max(1e-9);
90 let uv = body.uvs[i];
91 writeln!(obj, "v {} {} {}", p[0], p[1], p[2])?;
92 writeln!(obj, "vt {} {}", uv[0], 1. - uv[1])?;
93 writeln!(obj, "vn {} {} {}", n[0] / l, n[1] / l, n[2] / l)?;
94 }
95 for t in idx.chunks_exact(3) {
96 let f = [map[&t[0]], map[&t[1]], map[&t[2]]];
97 writeln!(
98 obj,
99 "f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}",
100 f[0], f[1], f[2]
101 )?;
102 }
103 drop(obj);
104 std::fs::write(
105 dir.join("female-hair.mtl"),
106 "newmtl hair\nKd 1 1 1\nmap_Kd female-hair.png\n",
107 )?;
108 std::fs::copy(&a[1], dir.join("female-hair.png"))?;
109 let edited = preview::load_body_file(std::path::Path::new(&a[2]))?;
110 let mut preset = equipment::Preset::new("female-hair".into());
111 preset.applies = [false, true];
112 preset.provenance = "Hair of \"Animated Female Base Mesh\" by assetfactory, https://sketchfab.com/3d-models/animated-female-base-mesh-52fc9c15cf3e49d2a5a9cee6444964ec, Sketchfab Standard licence. Baked rigid to the head joint at the rest pose (was skinned to several bones).".into();
113 let part = equipment::import_obj(&mut preset, &dir.join("female-hair.obj"))?;
114 let head_edited = edited
115 .joint_names
116 .iter()
117 .position(|n| n == &body.joint_names[head])
118 .ok_or("head missing in edited body")?;
119 preset.bind(1, &edited, part, head_edited)?;
120 // Tim t1023u defaults: position relative to the bone, rotation, gloss.
121 preset.attachments[part].translation[1] = [0.0, -0.370, 0.0];
122 preset.attachments[part].rotation_deg[1] = [0.0, 0.0, 0.0];
123 preset.attachments[part].specular = 0.62;
124 std::fs::write(&a[3], preset.encode()?)?;
125 println!(
126 "hair: {} vertices, {} triangles, bound to {}",
127 used.len(),
128 idx.len() / 3,
129 body.joint_names[head]
130 );
131 Ok(())
132}
133fn tempdir() -> std::path::PathBuf {
134 let d = std::env::temp_dir().join(format!("bake-hair-{}", std::process::id()));
135 std::fs::create_dir_all(&d).unwrap();
136 d
137}