oxihuman_wasm/
engine_core.rs1use anyhow::Result;
7use oxihuman_core::parser::obj::parse_obj;
8use oxihuman_core::policy::{Policy, PolicyProfile};
9use oxihuman_mesh::mesh::MeshBuffers;
10use oxihuman_mesh::normals::compute_normals;
11use oxihuman_mesh::suit::apply_suit_flag;
12use oxihuman_morph::engine::HumanEngine;
13use oxihuman_morph::params::ParamState;
14use oxihuman_morph::weight_curves::auto_weight_fn_for_target;
15
16use crate::buffer::serialize_quantized_to_bytes;
17use crate::pack::scan_zip_local_entries;
18use crate::BUFFER_FORMAT_VERSION;
19
20pub(crate) type JsonDelta = (u32, f32, f32, f32);
22
23pub(crate) type JsonTargetMap = std::collections::HashMap<String, (Vec<JsonDelta>, f32)>;
25
26#[derive(Debug, Clone)]
28pub struct ParticleSystem {
29 pub emit_rate: f32,
30 pub lifetime: f32,
31 pub particles: Vec<Particle>,
32 pub time_accum: f32,
33}
34
35#[derive(Debug, Clone)]
37pub struct Particle {
38 pub position: [f32; 3],
39 pub velocity: [f32; 3],
40 pub age: f32,
41 pub lifetime: f32,
42}
43
44pub struct WasmEngine {
46 pub(crate) engine: HumanEngine,
47 pub(crate) params: ParamState,
48 pub(crate) last_mesh: Option<MeshBuffers>,
49 pub(crate) target_names: Vec<String>,
51 pub(crate) json_targets: JsonTargetMap,
53 pub(crate) anim_frames: Vec<std::collections::HashMap<String, f32>>,
55 pub(crate) anim_current_frame: usize,
56 pub(crate) anim_fps: f32,
57 #[allow(dead_code)]
58 pub(crate) anim_playing: bool,
59 pub(crate) anim_accum: f32,
60 pub(crate) particle_sys: Option<ParticleSystem>,
62}
63
64impl WasmEngine {
65 pub fn new_from_obj_bytes(obj_bytes: &[u8]) -> Result<Self> {
67 let src = std::str::from_utf8(obj_bytes)?;
68 let base = parse_obj(src)?;
69 let policy = Policy::new(PolicyProfile::Standard);
70 Ok(WasmEngine {
71 engine: HumanEngine::new(base, policy),
72 params: ParamState::default(),
73 last_mesh: None,
74 target_names: Vec::new(),
75 json_targets: std::collections::HashMap::new(),
76 anim_frames: Vec::new(),
77 anim_current_frame: 0,
78 anim_fps: 24.0,
79 anim_playing: false,
80 anim_accum: 0.0,
81 particle_sys: None,
82 })
83 }
84
85 pub fn new_strict(obj_bytes: &[u8]) -> Result<Self> {
87 let src = std::str::from_utf8(obj_bytes)?;
88 let base = parse_obj(src)?;
89 let policy = Policy::new(PolicyProfile::Strict);
90 Ok(WasmEngine {
91 engine: HumanEngine::new(base, policy),
92 params: ParamState::default(),
93 last_mesh: None,
94 target_names: Vec::new(),
95 json_targets: std::collections::HashMap::new(),
96 anim_frames: Vec::new(),
97 anim_current_frame: 0,
98 anim_fps: 24.0,
99 anim_playing: false,
100 anim_accum: 0.0,
101 particle_sys: None,
102 })
103 }
104
105 pub fn load_target_bytes(&mut self, name: &str, target_bytes: &[u8]) -> Result<()> {
108 use oxihuman_core::parser::target::parse_target;
109 let src = std::str::from_utf8(target_bytes)?;
110 let target = parse_target(name, src)?;
111 let before = self.engine.target_count();
112 let weight_fn = auto_weight_fn_for_target(name);
113 self.engine.load_target(target, weight_fn);
114 if self.engine.target_count() > before {
116 self.target_names.push(name.to_string());
117 }
118 self.last_mesh = None; Ok(())
120 }
121
122 pub fn load_zip_pack_bytes(&mut self, zip_bytes: &[u8]) -> Result<usize> {
136 let entries = scan_zip_local_entries(zip_bytes)?;
137
138 let obj_entry = entries
140 .iter()
141 .find(|(name, _)| name == "base.obj" || name.ends_with(".obj"))
142 .ok_or_else(|| anyhow::anyhow!("ZIP pack contains no .obj entry"))?;
143
144 let src = std::str::from_utf8(&obj_entry.1)
146 .map_err(|e| anyhow::anyhow!("base.obj is not valid UTF-8: {e}"))?;
147 let base = parse_obj(src)?;
148 let policy = Policy::new(PolicyProfile::Standard);
149 self.engine = HumanEngine::new(base, policy);
150 self.params = ParamState::default();
151 self.target_names.clear();
152 self.json_targets.clear();
153 self.last_mesh = None;
154
155 let mut loaded = 0usize;
157 for (name, data) in &entries {
158 if name.ends_with(".target") {
159 let stem = name
160 .strip_suffix(".target")
161 .unwrap_or(name.as_str())
162 .rsplit('/')
163 .next()
164 .unwrap_or(name.as_str());
165 self.load_target_bytes(stem, data)?;
166 loaded += 1;
167 }
168 }
169
170 Ok(loaded)
171 }
172
173 pub fn list_loaded_targets(&self) -> String {
182 let count = self.engine.target_count();
183 if self.target_names.len() == count {
184 let items: Vec<String> = self
186 .target_names
187 .iter()
188 .map(|n| format!("\"{}\"", n.replace('\\', "\\\\").replace('"', "\\\"")))
189 .collect();
190 format!("[{}]", items.join(","))
191 } else {
192 format!("{{\"count\":{count}}}")
194 }
195 }
196
197 pub fn export_quantized_bytes(&mut self) -> Vec<u8> {
215 use oxihuman_export::mesh_quantize::quantize_mesh;
216
217 let morph_buf = self.engine.build_mesh_incremental();
218 let mut mesh = MeshBuffers::from_morph(morph_buf);
219 compute_normals(&mut mesh);
220 apply_suit_flag(&mut mesh);
221
222 self.last_mesh = Some(mesh.clone());
223
224 let q = quantize_mesh(&mesh);
225 serialize_quantized_to_bytes(&q)
226 }
227
228 pub fn set_height(&mut self, v: f32) {
232 self._update_param(|p| p.height = v);
233 }
234 pub fn set_weight(&mut self, v: f32) {
236 self._update_param(|p| p.weight = v);
237 }
238 pub fn set_muscle(&mut self, v: f32) {
240 self._update_param(|p| p.muscle = v);
241 }
242 pub fn set_age(&mut self, v: f32) {
244 self._update_param(|p| p.age = v);
245 }
246
247 pub fn set_param(&mut self, name: &str, value: f32) {
249 self._update_param(|p| {
250 p.extra.insert(name.to_string(), value);
251 });
252 }
253
254 pub(crate) fn _update_param<F: FnOnce(&mut ParamState)>(&mut self, f: F) {
255 let mut p = self.params.clone();
256 f(&mut p);
257 self.engine.set_params(p.clone());
258 self.params = p;
259 self.last_mesh = None;
260 }
261
262 pub fn reset_params(&mut self) {
264 let default = ParamState::default();
265 self.engine.set_params(default.clone());
266 self.params = default;
267 self.last_mesh = None;
268 }
269
270 pub fn target_count(&self) -> usize {
272 self.engine.target_count()
273 }
274
275 pub fn build_mesh_bytes(&mut self) -> Vec<u8> {
281 let morph_buf = self.engine.build_mesh();
282 let mut mesh = MeshBuffers::from_morph(morph_buf);
283 compute_normals(&mut mesh);
284 apply_suit_flag(&mut mesh);
285
286 let n_verts = mesh.positions.len() as u32;
287 let n_idx = mesh.indices.len() as u32;
288
289 let mut out =
290 Vec::with_capacity(12 + (n_verts as usize) * (3 + 3 + 2) * 4 + (n_idx as usize) * 4);
291
292 out.extend_from_slice(&BUFFER_FORMAT_VERSION.to_le_bytes());
294 out.extend_from_slice(&n_verts.to_le_bytes());
295 out.extend_from_slice(&n_idx.to_le_bytes());
296
297 for p in &mesh.positions {
299 for &c in p {
300 out.extend_from_slice(&c.to_le_bytes());
301 }
302 }
303 for n in &mesh.normals {
305 for &c in n {
306 out.extend_from_slice(&c.to_le_bytes());
307 }
308 }
309 for uv in &mesh.uvs {
311 for &c in uv {
312 out.extend_from_slice(&c.to_le_bytes());
313 }
314 }
315 for &i in &mesh.indices {
317 out.extend_from_slice(&i.to_le_bytes());
318 }
319
320 self.last_mesh = Some(mesh);
321 out
322 }
323
324 pub fn vertex_count(&self) -> usize {
326 self.engine.vertex_count()
327 }
328
329 pub fn reset_incremental_cache(&mut self) {
334 self.engine.clear_incremental_cache();
335 self.last_mesh = None;
336 }
337
338 pub fn has_cached_mesh(&self) -> bool {
340 self.last_mesh.is_some()
341 }
342
343 pub fn build_mesh_prepared(&mut self) -> MeshBuffers {
349 let morph_buf = self.engine.build_mesh_incremental();
350 let mut mesh = MeshBuffers::from_morph(morph_buf);
351 compute_normals(&mut mesh);
352 apply_suit_flag(&mut mesh);
353 self.last_mesh = Some(mesh.clone());
354 mesh
355 }
356
357 pub fn set_allowlist(&mut self, names: &[&str]) {
362 let allowlist: Vec<String> = names.iter().map(|s| s.to_string()).collect();
363 let policy = Policy::with_allowlist(PolicyProfile::Strict, allowlist);
364 self.engine.set_policy(policy);
365 }
366
367 pub fn reset_all_weights(&mut self) {
369 for v in self.params.extra.values_mut() {
371 *v = 0.0;
372 }
373 self.params.height = 0.5;
374 self.params.weight = 0.5;
375 self.params.muscle = 0.5;
376 self.params.age = 0.5;
377 self.engine.set_params(self.params.clone());
378 for entry in self.json_targets.values_mut() {
380 entry.1 = 0.0;
381 }
382 self.last_mesh = None;
383 }
384
385 pub fn apply_preset_by_name(&mut self, name: &str) -> bool {
388 use oxihuman_morph::presets::BodyPreset;
389 if BodyPreset::from_name(name).is_some() {
390 self.set_params_from_preset(name);
391 true
392 } else {
393 false
394 }
395 }
396
397 pub fn step_physics(&mut self, _dt: f32) {
399 }
401
402 pub fn get_cloth_state(&self) -> String {
404 r#"{"cloth_positions":[]}"#.to_string()
405 }
406
407 pub fn get_physics_proxy_json(&self) -> String {
409 r#"{"proxies":[]}"#.to_string()
410 }
411
412 pub fn set_wind(&mut self, _x: f32, _y: f32, _z: f32) {
414 }
416
417 pub fn get_vertex_count(&self) -> u32 {
419 self.engine.vertex_count() as u32
420 }
421
422 pub fn get_index_count(&self) -> u32 {
424 if let Some(ref m) = self.last_mesh {
425 return m.indices.len() as u32;
426 }
427 0
429 }
430}