#![allow(clippy::needless_range_loop)] //! Bakes the hair of Tim's original "Animated Female Base Mesh" into the //! bundled rigid equipment preset `female-hair` (design.md, t1019u): the hair //! primitive (material ponytail01, last primitive) is skinned once at the rest //! pose, expressed in the rest-pose space of the `head` joint, written as //! OBJ + MTL + PNG, imported with the normal OBJ importer and bound to `head` //! of the edited default female with zero offset. //! Usage: bake_female_hair ORIGINAL_FEMALE.gltf HAIR_TEXTURE.png EDITED_FEMALE.gltf OUT.ppe use paperdoll_renderer::{equipment, preview}; use std::io::Write; fn inverse(m: [f32; 16]) -> [f32; 16] { let mut a = [[0f64; 8]; 4]; for r in 0..4 { for c in 0..4 { a[r][c] = m[c * 4 + r] as f64; } a[r][4 + r] = 1.; } for c in 0..4 { let p = (c..4) .max_by(|&x, &y| a[x][c].abs().total_cmp(&a[y][c].abs())) .unwrap(); a.swap(c, p); let d = a[c][c]; for k in 0..8 { a[c][k] /= d; } for r in 0..4 { if r != c { let f = a[r][c]; for k in 0..8 { a[r][k] -= f * a[c][k]; } } } } let mut out = [0f32; 16]; for r in 0..4 { for c in 0..4 { out[c * 4 + r] = a[r][4 + c] as f32; } } out } fn apply(m: &[f32; 16], p: [f32; 3], w: f32) -> [f32; 3] { [0, 1, 2].map(|i| m[i] * p[0] + m[4 + i] * p[1] + m[8 + i] * p[2] + m[12 + i] * w) } fn main() -> Result<(), Box> { let a: Vec = std::env::args().skip(1).collect(); let body = preview::load_body_file(std::path::Path::new(&a[0]))?; let hair = body.surfaces.last().ok_or("no surfaces")?; let bones: Vec<[f32; 16]> = (0..body.joint_nodes.len()) .map(|j| { preview::mul( preview::rest_joint_world(&body, j).unwrap(), body.inverse_binds[j], ) }) .collect(); let head = body .joint_names .iter() .position(|n| n.to_ascii_lowercase().starts_with("head")) .ok_or("no head joint")?; let head_world = preview::animated_joint_world(&body, "", 0.0, head).ok_or("head")?; let to_head = inverse(head_world); let idx = &body.indices[hair.first..hair.first + hair.count]; let mut used: Vec = idx.to_vec(); used.sort_unstable(); used.dedup(); let map: std::collections::HashMap = used.iter().enumerate().map(|(i, v)| (*v, i + 1)).collect(); let dir = tempdir(); let mut obj = std::fs::File::create(dir.join("female-hair.obj"))?; writeln!(obj, "# Hair of \"Animated Female Base Mesh\" by assetfactory (Sketchfab Standard), baked rigid to {}", body.joint_names[head])?; writeln!(obj, "mtllib female-hair.mtl\nusemtl hair")?; for v in &used { let i = *v as usize; let mut m = [0f32; 16]; for (j, w) in body.joints[i].iter().zip(body.weights[i]) { for k in 0..16 { m[k] += bones[*j as usize][k] * w; } } let p = apply(&to_head, apply(&m, body.positions[i], 1.), 1.); let n = apply(&to_head, apply(&m, body.normals[i], 0.), 0.); let l = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt().max(1e-9); let uv = body.uvs[i]; writeln!(obj, "v {} {} {}", p[0], p[1], p[2])?; writeln!(obj, "vt {} {}", uv[0], 1. - uv[1])?; writeln!(obj, "vn {} {} {}", n[0] / l, n[1] / l, n[2] / l)?; } for t in idx.chunks_exact(3) { let f = [map[&t[0]], map[&t[1]], map[&t[2]]]; writeln!( obj, "f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}", f[0], f[1], f[2] )?; } drop(obj); std::fs::write( dir.join("female-hair.mtl"), "newmtl hair\nKd 1 1 1\nmap_Kd female-hair.png\n", )?; std::fs::copy(&a[1], dir.join("female-hair.png"))?; let edited = preview::load_body_file(std::path::Path::new(&a[2]))?; let mut preset = equipment::Preset::new("female-hair".into()); preset.applies = [false, true]; 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(); let part = equipment::import_obj(&mut preset, &dir.join("female-hair.obj"))?; let head_edited = edited .joint_names .iter() .position(|n| n == &body.joint_names[head]) .ok_or("head missing in edited body")?; preset.bind(1, &edited, part, head_edited)?; std::fs::write(&a[3], preset.encode()?)?; println!( "hair: {} vertices, {} triangles, bound to {}", used.len(), idx.len() / 3, body.joint_names[head] ); Ok(()) } fn tempdir() -> std::path::PathBuf { let d = std::env::temp_dir().join(format!("bake-hair-{}", std::process::id())); std::fs::create_dir_all(&d).unwrap(); d }