Skip to main content

siglus_scene_vm/
mesh3d.rs

1use std::collections::HashMap;
2use std::fs;
3use std::io::{Cursor as IoCursor, Read, Write};
4use std::path::{Path, PathBuf};
5
6use anyhow::{bail, Context, Result};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
9pub enum MeshLightingType {
10    #[default]
11    None = 0,
12    Lambert = 1,
13    BlinnPhong = 2,
14    PerPixelBlinnPhong = 3,
15    PerPixelHalfLambert = 4,
16    Toon = 5,
17    FixedFunction = 6,
18    PerPixelFixedFunction = 7,
19    Bump = 8,
20    Parallax = 9,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
24pub enum MeshShadingType {
25    #[default]
26    None = 0,
27    DepthBuffer = 1,
28}
29
30pub const MESH_SHADER_OPTION_NONE: u32 = 0;
31pub const MESH_SHADER_OPTION_RIM_LIGHT: u32 = 1 << 0;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
34pub enum MeshEffectProfile {
35    #[default]
36    None = 0,
37    Mesh = 1,
38    SkinnedMesh = 2,
39    ShadowMap = 3,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
43pub struct MeshRuntimeMaterialKey {
44    pub use_mesh_tex: bool,
45    pub use_shadow_tex: bool,
46    pub use_toon_tex: bool,
47    pub use_normal_tex: bool,
48    pub use_mul_vertex_color: bool,
49    pub use_mrbd: bool,
50    pub use_rgb: bool,
51    pub lighting_type: MeshLightingType,
52    pub shading_type: MeshShadingType,
53    pub shader_option: u32,
54    pub skinned: bool,
55    pub alpha_test_enable: bool,
56    pub cull_disable: bool,
57    pub shadow_map_enable: bool,
58}
59
60#[derive(Debug, Clone, Default)]
61pub struct MeshPrimitiveRuntimeDesc {
62    pub effect_profile: MeshEffectProfile,
63    pub effect_key: String,
64    pub technique_name: String,
65    pub shadow_effect_key: String,
66    pub shadow_technique_name: String,
67    pub use_mesh_texture_slot: bool,
68    pub use_normal_texture_slot: bool,
69    pub use_toon_texture_slot: bool,
70    pub use_shadow_texture_slot: bool,
71    pub material_key: MeshRuntimeMaterialKey,
72    pub vertex_stride_bytes: u32,
73    pub vertex_count: u32,
74    pub bone_palette_len: u32,
75}
76
77#[derive(Debug, Clone, Default)]
78pub struct MeshTriVertex {
79    pub pos: [f32; 3],
80    pub uv: [f32; 2],
81    pub normal: [f32; 3],
82    pub tangent: [f32; 3],
83    pub binormal: [f32; 3],
84    pub color: [f32; 4],
85    pub bone_indices: [u16; 4],
86    pub bone_weights: [f32; 4],
87}
88
89#[derive(Debug, Clone, Default)]
90pub struct MeshMaterial {
91    pub diffuse: [f32; 4],
92    pub ambient: [f32; 4],
93    pub specular: [f32; 4],
94    pub emissive: [f32; 4],
95    pub power: f32,
96    pub lighting_type: MeshLightingType,
97    pub shading_type: MeshShadingType,
98    pub shader_option: u32,
99    pub rim_light_color: [f32; 4],
100    pub rim_light_power: f32,
101    pub parallax_max_height: f32,
102    pub alpha_test_enable: bool,
103    pub alpha_ref: f32,
104    pub cull_disable: bool,
105    pub shadow_map_enable: bool,
106    pub use_mesh_tex: bool,
107    pub use_mrbd: bool,
108    pub mrbd: [f32; 4],
109    pub use_rgb: bool,
110    pub rgb_rate: [f32; 4],
111    pub add_rgb: [f32; 4],
112    pub use_mul_vertex_color: bool,
113    pub mul_vertex_color_rate: f32,
114    pub depth_buffer_shadow_bias: f32,
115    pub directional_light_ids: Vec<i32>,
116    pub point_light_ids: Vec<i32>,
117    pub spot_light_ids: Vec<i32>,
118    pub normal_texture_path: Option<PathBuf>,
119    pub toon_texture_path: Option<PathBuf>,
120    pub effect_filename: Option<String>,
121}
122
123#[derive(Debug, Clone, Default)]
124pub struct MeshGpuPrimitiveBatch {
125    pub vertices: Vec<MeshTriVertex>,
126    pub frame_cols: [[f32; 4]; 4],
127    pub bone_cols: Vec<[[f32; 4]; 4]>,
128    pub skinned: bool,
129    pub texture_path: Option<PathBuf>,
130    pub material: MeshMaterial,
131    pub runtime_desc: MeshPrimitiveRuntimeDesc,
132}
133
134#[derive(Debug, Clone)]
135pub struct MeshAnimationState {
136    pub clip_name: Option<String>,
137    pub clip_index: Option<usize>,
138    pub blend_clip_name: Option<String>,
139    pub blend_clip_index: Option<usize>,
140    pub blend_weight: f32,
141    /// Object-local animation controller time, advanced by runtime tick.
142    pub time_sec: f32,
143    pub rate: f32,
144    pub time_offset_sec: f32,
145    /// Frozen controller sample time used while paused.
146    pub hold_time_sec: f32,
147    pub paused: bool,
148    pub looped: bool,
149    /// tona3-like controller state.
150    pub anim_track_no: u32,
151    pub anim_shift_time_sec: f32,
152    pub is_anim: bool,
153    pub prev_clip_name: Option<String>,
154    pub prev_clip_index: Option<usize>,
155    pub prev_time_sec: f32,
156    pub prev_time_offset_sec: f32,
157    pub prev_rate: f32,
158    pub transition_elapsed_sec: f32,
159}
160
161pub const MESH_ANIM_CONTROLLER_STEP_SEC: f32 = 60.0 / 4800.0;
162
163impl Default for MeshAnimationState {
164    fn default() -> Self {
165        Self {
166            clip_name: None,
167            clip_index: None,
168            blend_clip_name: None,
169            blend_clip_index: None,
170            blend_weight: 0.0,
171            time_sec: 0.0,
172            rate: 1.0,
173            time_offset_sec: 0.0,
174            hold_time_sec: 0.0,
175            paused: false,
176            looped: true,
177            anim_track_no: 0,
178            anim_shift_time_sec: 0.0,
179            is_anim: true,
180            prev_clip_name: None,
181            prev_clip_index: None,
182            prev_time_sec: 0.0,
183            prev_time_offset_sec: 0.0,
184            prev_rate: 1.0,
185            transition_elapsed_sec: 0.0,
186        }
187    }
188}
189
190impl MeshAnimationState {
191    pub fn sanitized(&self) -> Self {
192        let mut out = self.clone();
193        out.blend_weight = out.blend_weight.clamp(0.0, 1.0);
194        out.time_sec = out.time_sec.max(0.0);
195        out.rate = out.rate.max(0.0);
196        out.time_offset_sec = out.time_offset_sec.max(0.0);
197        out.hold_time_sec = out.hold_time_sec.max(0.0);
198        out.anim_shift_time_sec = out.anim_shift_time_sec.max(0.0);
199        out.prev_time_sec = out.prev_time_sec.max(0.0);
200        out.prev_time_offset_sec = out.prev_time_offset_sec.max(0.0);
201        out.prev_rate = out.prev_rate.max(0.0);
202        out.transition_elapsed_sec = out.transition_elapsed_sec.max(0.0);
203        if out.anim_shift_time_sec <= 0.0 {
204            out.prev_clip_name = None;
205            out.prev_clip_index = None;
206            out.prev_time_sec = 0.0;
207            out.prev_time_offset_sec = 0.0;
208            out.prev_rate = 1.0;
209            out.transition_elapsed_sec = 0.0;
210        }
211        out
212    }
213
214    pub fn current_sample_base_sec(&self) -> f32 {
215        if self.paused || !self.is_anim {
216            self.hold_time_sec.max(0.0)
217        } else {
218            self.time_sec.max(0.0) * self.rate.max(0.0)
219        }
220    }
221
222    pub fn current_sample_time_sec(&self) -> f32 {
223        self.current_sample_base_sec() + self.time_offset_sec.max(0.0)
224    }
225
226    pub fn previous_track_sample_time_sec(&self) -> f32 {
227        (self.prev_time_sec.max(0.0) * self.prev_rate.max(0.0)) + self.prev_time_offset_sec.max(0.0)
228    }
229
230    pub fn previous_track_weight(&self) -> f32 {
231        if (self.prev_clip_name.is_none() && self.prev_clip_index.is_none())
232            || self.anim_shift_time_sec <= 0.0
233        {
234            0.0
235        } else {
236            (1.0 - (self.transition_elapsed_sec / self.anim_shift_time_sec)).clamp(0.0, 1.0)
237        }
238    }
239
240    pub fn current_track_weight(&self) -> f32 {
241        (1.0 - self.previous_track_weight()).clamp(0.0, 1.0)
242    }
243
244    pub fn previous_track_speed(&self) -> f32 {
245        if (self.prev_clip_name.is_none() && self.prev_clip_index.is_none())
246            || self.anim_shift_time_sec <= 0.0
247        {
248            0.0
249        } else {
250            (1.0 - (self.transition_elapsed_sec / self.anim_shift_time_sec)).clamp(0.0, 1.0)
251        }
252    }
253
254    pub fn current_track_speed(&self) -> f32 {
255        if (self.prev_clip_name.is_none() && self.prev_clip_index.is_none())
256            || self.anim_shift_time_sec <= 0.0
257        {
258            1.0
259        } else {
260            (self.transition_elapsed_sec / self.anim_shift_time_sec).clamp(0.0, 1.0)
261        }
262    }
263
264    pub fn set_anim_shift_time_sec(&mut self, shift_sec: f32) {
265        self.anim_shift_time_sec = shift_sec.max(0.0);
266        *self = self.sanitized();
267    }
268
269    pub fn change_animation_clip(
270        &mut self,
271        next_clip_name: Option<String>,
272        next_clip_index: Option<usize>,
273    ) {
274        if self.clip_name == next_clip_name && self.clip_index == next_clip_index {
275            return;
276        }
277        self.prev_clip_name = self.clip_name.clone();
278        self.prev_clip_index = self.clip_index;
279        self.prev_time_sec = self.time_sec.max(0.0);
280        self.prev_time_offset_sec = self.time_offset_sec.max(0.0);
281        self.prev_rate = self.rate.max(0.0);
282        self.clip_name = next_clip_name;
283        self.clip_index = next_clip_index;
284        self.time_sec = 0.0;
285        self.hold_time_sec = 0.0;
286        self.transition_elapsed_sec = 0.0;
287        self.anim_track_no = if self.anim_track_no == 0 { 1 } else { 0 };
288        if self.anim_shift_time_sec <= 0.0 {
289            self.prev_clip_name = None;
290            self.prev_clip_index = None;
291            self.prev_time_sec = 0.0;
292            self.prev_time_offset_sec = 0.0;
293            self.prev_rate = 1.0;
294        }
295        *self = self.sanitized();
296    }
297
298    pub fn advance_controller_frames(&mut self, delta_frames: i32) {
299        let delta_sec = (delta_frames.max(0) as f32) * MESH_ANIM_CONTROLLER_STEP_SEC;
300        if !self.paused && self.is_anim {
301            let has_prev = self.prev_clip_name.is_some() || self.prev_clip_index.is_some();
302            if has_prev && self.anim_shift_time_sec > 0.0 {
303                let start = self.transition_elapsed_sec.max(0.0);
304                let end = (start + delta_sec).min(self.anim_shift_time_sec);
305                let active_delta = (end - start).max(0.0);
306                if active_delta > 0.0 {
307                    let shift = self.anim_shift_time_sec.max(0.000_001);
308                    let cur_advance = ((end * end) - (start * start)) / (2.0 * shift);
309                    let prev_advance = active_delta - cur_advance;
310                    self.time_sec = (self.time_sec.max(0.0) + cur_advance.max(0.0)).max(0.0);
311                    self.prev_time_sec =
312                        (self.prev_time_sec.max(0.0) + prev_advance.max(0.0)).max(0.0);
313                    self.transition_elapsed_sec = end.max(0.0);
314                }
315                if delta_sec > active_delta {
316                    self.time_sec = (self.time_sec.max(0.0) + (delta_sec - active_delta)).max(0.0);
317                }
318                if self.transition_elapsed_sec >= self.anim_shift_time_sec {
319                    self.prev_clip_name = None;
320                    self.prev_clip_index = None;
321                    self.prev_time_sec = 0.0;
322                    self.prev_time_offset_sec = 0.0;
323                    self.prev_rate = 1.0;
324                    self.transition_elapsed_sec = 0.0;
325                }
326            } else {
327                self.time_sec = (self.time_sec.max(0.0) + delta_sec).max(0.0);
328                if has_prev {
329                    self.prev_clip_name = None;
330                    self.prev_clip_index = None;
331                    self.prev_time_sec = 0.0;
332                    self.prev_time_offset_sec = 0.0;
333                    self.prev_rate = 1.0;
334                    self.transition_elapsed_sec = 0.0;
335                }
336            }
337        }
338        *self = self.sanitized();
339    }
340}
341
342#[derive(Debug, Clone, Default)]
343pub struct MeshAsset {
344    pub source_path: PathBuf,
345    pub texture_path: Option<PathBuf>,
346    pub vertices: Vec<MeshTriVertex>,
347    pub bone_count: usize,
348    pub ticks_per_second: f32,
349    primitives: Vec<MeshPrimitive>,
350    frames: Vec<FrameNode>,
351    animations: Vec<AnimationClip>,
352}
353
354impl MeshAsset {
355    pub fn bounds_size(&self) -> [f32; 3] {
356        let vertices = if self.vertices.is_empty() {
357            self.sample_vertices(0.0)
358        } else {
359            self.vertices.clone()
360        };
361        if vertices.is_empty() {
362            return [0.0, 0.0, 0.0];
363        }
364
365        let mut min = [f32::INFINITY; 3];
366        let mut max = [f32::NEG_INFINITY; 3];
367        for v in vertices {
368            for axis in 0..3 {
369                min[axis] = min[axis].min(v.pos[axis]);
370                max[axis] = max[axis].max(v.pos[axis]);
371            }
372        }
373        [
374            (max[0] - min[0]).max(0.0),
375            (max[1] - min[1]).max(0.0),
376            (max[2] - min[2]).max(0.0),
377        ]
378    }
379
380    pub fn is_skinned(&self) -> bool {
381        self.bone_count != 0
382            && self.primitives.iter().any(|p| {
383                !p.bones.is_empty()
384                    && p.vertices
385                        .iter()
386                        .any(|v| v.bone_weights.iter().copied().sum::<f32>() > 0.0)
387            })
388    }
389
390    pub fn duration_seconds(&self) -> f32 {
391        self.animations
392            .iter()
393            .map(|a| self.seconds_from_ticks(a.max_time))
394            .fold(0.0, f32::max)
395    }
396
397    fn seconds_from_ticks(&self, ticks: i64) -> f32 {
398        ticks as f32 / self.ticks_per_second.max(1.0)
399    }
400
401    fn ticks_from_seconds(&self, sec: f32) -> i64 {
402        (sec.max(0.0) * self.ticks_per_second.max(1.0)) as i64
403    }
404
405    pub fn animation_clip_names(&self) -> Vec<String> {
406        self.animations.iter().map(|a| a.name.clone()).collect()
407    }
408
409    fn resolve_clip_index(
410        &self,
411        clip_name: Option<&str>,
412        clip_index: Option<usize>,
413    ) -> Option<usize> {
414        if let Some(name) = clip_name {
415            if let Some((idx, _)) = self
416                .animations
417                .iter()
418                .enumerate()
419                .find(|(_, c)| c.name.eq_ignore_ascii_case(name))
420            {
421                return Some(idx);
422            }
423        }
424        if let Some(idx) = clip_index {
425            if idx < self.animations.len() {
426                return Some(idx);
427            }
428        }
429        if self.animations.is_empty() {
430            None
431        } else {
432            Some(0)
433        }
434    }
435
436    pub fn sample_vertices_with_clip(
437        &self,
438        time_sec: f32,
439        clip_name: Option<&str>,
440        clip_index: Option<usize>,
441    ) -> Vec<MeshTriVertex> {
442        if self.primitives.is_empty() {
443            return self.vertices.clone();
444        }
445        let pose = PoseState::new_with_clip(
446            self,
447            time_sec,
448            self.resolve_clip_index(clip_name, clip_index),
449        );
450        let mut out = Vec::new();
451        for prim in &self.primitives {
452            let frame_world = pose
453                .combined
454                .get(prim.frame_index)
455                .copied()
456                .unwrap_or(Mat4::identity());
457            for src in &prim.vertices {
458                let (pos, normal) = if prim.bones.is_empty() {
459                    (
460                        frame_world.transform_point(src.pos),
461                        frame_world.transform_vector(src.normal),
462                    )
463                } else {
464                    skin_vertex(src, prim, &pose)
465                };
466                out.push(MeshTriVertex {
467                    pos,
468                    uv: src.uv,
469                    normal: normalize3(normal),
470                    tangent: src.tangent,
471                    binormal: src.binormal,
472                    color: src.color,
473                    bone_indices: src.bone_indices,
474                    bone_weights: src.bone_weights,
475                });
476            }
477        }
478        out
479    }
480
481    pub fn sample_vertices(&self, time_sec: f32) -> Vec<MeshTriVertex> {
482        self.sample_vertices_with_clip(time_sec, None, None)
483    }
484
485    pub fn sample_gpu_primitives_with_clip(
486        &self,
487        time_sec: f32,
488        clip_name: Option<&str>,
489        clip_index: Option<usize>,
490    ) -> Vec<MeshGpuPrimitiveBatch> {
491        if self.primitives.is_empty() {
492            return vec![MeshGpuPrimitiveBatch {
493                vertices: self.vertices.clone(),
494                frame_cols: Mat4::identity().m,
495                bone_cols: Vec::new(),
496                skinned: false,
497                texture_path: self.texture_path.clone(),
498                material: default_mesh_material(),
499                runtime_desc: build_mesh_primitive_runtime_desc_from_material(
500                    &default_mesh_material(),
501                    self.texture_path.is_some(),
502                    false,
503                    self.vertices.len() as u32,
504                    0,
505                ),
506            }];
507        }
508        let pose = PoseState::new_with_clip(
509            self,
510            time_sec,
511            self.resolve_clip_index(clip_name, clip_index),
512        );
513        let mut out = Vec::with_capacity(self.primitives.len());
514        for prim in &self.primitives {
515            let frame_world = pose
516                .combined
517                .get(prim.frame_index)
518                .copied()
519                .unwrap_or(Mat4::identity());
520            let mut bone_cols = Vec::with_capacity(prim.bones.len());
521            for bone in &prim.bones {
522                bone_cols.push(skin_matrix_for_bone(bone, &pose).m);
523            }
524            out.push(MeshGpuPrimitiveBatch {
525                vertices: prim.vertices.clone(),
526                frame_cols: frame_world.m,
527                bone_cols,
528                skinned: !prim.bones.is_empty(),
529                texture_path: prim.texture_path.clone(),
530                material: prim.material.clone(),
531                runtime_desc: prim.runtime_desc.clone(),
532            });
533        }
534        out
535    }
536
537    pub fn sample_gpu_primitives(&self, time_sec: f32) -> Vec<MeshGpuPrimitiveBatch> {
538        self.sample_gpu_primitives_with_clip(time_sec, None, None)
539    }
540
541    pub fn sample_gpu_primitives_with_state(
542        &self,
543        state: &MeshAnimationState,
544    ) -> Vec<MeshGpuPrimitiveBatch> {
545        if self.primitives.is_empty() {
546            return vec![MeshGpuPrimitiveBatch {
547                vertices: self.vertices.clone(),
548                frame_cols: Mat4::identity().m,
549                bone_cols: Vec::new(),
550                skinned: false,
551                texture_path: self.texture_path.clone(),
552                material: default_mesh_material(),
553                runtime_desc: build_mesh_primitive_runtime_desc_from_material(
554                    &default_mesh_material(),
555                    self.texture_path.is_some(),
556                    false,
557                    self.vertices.len() as u32,
558                    0,
559                ),
560            }];
561        }
562        let pose = PoseState::new_with_state(self, state);
563        let mut out = Vec::with_capacity(self.primitives.len());
564        for prim in &self.primitives {
565            let frame_world = pose
566                .combined
567                .get(prim.frame_index)
568                .copied()
569                .unwrap_or(Mat4::identity());
570            let mut bone_cols = Vec::with_capacity(prim.bones.len());
571            for bone in &prim.bones {
572                bone_cols.push(skin_matrix_for_bone(bone, &pose).m);
573            }
574            out.push(MeshGpuPrimitiveBatch {
575                vertices: prim.vertices.clone(),
576                frame_cols: frame_world.m,
577                bone_cols,
578                skinned: !prim.bones.is_empty(),
579                texture_path: prim.texture_path.clone(),
580                material: prim.material.clone(),
581                runtime_desc: prim.runtime_desc.clone(),
582            });
583        }
584        out
585    }
586}
587
588#[derive(Debug, Clone)]
589struct MeshPrimitive {
590    frame_index: usize,
591    texture_path: Option<PathBuf>,
592    material: MeshMaterial,
593    runtime_desc: MeshPrimitiveRuntimeDesc,
594    vertices: Vec<MeshTriVertex>,
595    bones: Vec<BoneBinding>,
596}
597
598#[derive(Debug, Clone)]
599struct BoneBinding {
600    frame_name: String,
601    frame_index: usize,
602    offset_matrix: Mat4,
603}
604
605#[derive(Debug, Clone)]
606struct FrameNode {
607    name: String,
608    base_local: Mat4,
609    children: Vec<usize>,
610}
611
612#[derive(Debug, Clone, Default)]
613struct AnimationClip {
614    name: String,
615    tracks: HashMap<String, AnimationTrack>,
616    max_time: i64,
617    open_closed: bool,
618}
619
620#[derive(Debug, Clone, Default)]
621struct AnimationTrack {
622    rotation_keys: Vec<QuatKey>,
623    scale_keys: Vec<Vec3Key>,
624    position_keys: Vec<Vec3Key>,
625    matrix_keys: Vec<MatKey>,
626}
627
628#[derive(Debug, Clone, Copy)]
629struct Vec3Key {
630    time: i64,
631    value: [f32; 3],
632}
633
634#[derive(Debug, Clone, Copy)]
635struct QuatKey {
636    time: i64,
637    value: Quat,
638}
639
640#[derive(Debug, Clone, Copy)]
641struct MatKey {
642    time: i64,
643    value: Mat4,
644}
645
646#[derive(Debug, Clone)]
647struct PoseState {
648    combined: Vec<Mat4>,
649    frame_lookup: HashMap<String, usize>,
650}
651
652impl PoseState {
653    fn new(asset: &MeshAsset, time_sec: f32) -> Self {
654        Self::new_with_clip(
655            asset,
656            time_sec,
657            if asset.animations.is_empty() {
658                None
659            } else {
660                Some(0)
661            },
662        )
663    }
664
665    fn new_with_clip(asset: &MeshAsset, time_sec: f32, clip_index: Option<usize>) -> Self {
666        let mut state = MeshAnimationState::default();
667        state.time_sec = time_sec;
668        state.clip_index = clip_index;
669        Self::new_with_state(asset, &state)
670    }
671
672    fn new_with_state(asset: &MeshAsset, state: &MeshAnimationState) -> Self {
673        let frame_lookup: HashMap<String, usize> = asset
674            .frames
675            .iter()
676            .enumerate()
677            .map(|(i, f)| (f.name.clone(), i))
678            .collect();
679        let clip = asset
680            .resolve_clip_index(state.clip_name.as_deref(), state.clip_index)
681            .and_then(|idx| asset.animations.get(idx));
682        let blend_clip = asset
683            .resolve_clip_index(state.blend_clip_name.as_deref(), state.blend_clip_index)
684            .and_then(|idx| asset.animations.get(idx));
685        let prev_clip = asset
686            .resolve_clip_index(state.prev_clip_name.as_deref(), state.prev_clip_index)
687            .and_then(|idx| asset.animations.get(idx));
688        let primary_time = animation_time_for_state(state);
689        let prev_time = state.previous_track_sample_time_sec();
690        let prev_weight = state.previous_track_weight();
691        let cur_weight = state.current_track_weight();
692        let blend_weight = state.blend_weight.clamp(0.0, 1.0);
693        let mut local = Vec::with_capacity(asset.frames.len());
694        for frame in &asset.frames {
695            let current = if let Some(clip) = clip {
696                if let Some(track) = clip.tracks.get(&frame.name) {
697                    sample_track(
698                        track,
699                        frame.base_local,
700                        primary_time,
701                        clip.max_time,
702                        state.looped && clip.open_closed,
703                        asset.ticks_per_second,
704                    )
705                } else {
706                    frame.base_local
707                }
708            } else {
709                frame.base_local
710            };
711            let mut primary = current;
712            if prev_weight > 0.0 {
713                if let Some(prev_clip) = prev_clip {
714                    let previous = if let Some(prev_track) = prev_clip.tracks.get(&frame.name) {
715                        sample_track(
716                            prev_track,
717                            frame.base_local,
718                            prev_time,
719                            prev_clip.max_time,
720                            state.looped && prev_clip.open_closed,
721                            asset.ticks_per_second,
722                        )
723                    } else {
724                        frame.base_local
725                    };
726                    let normalized_prev = if (cur_weight + prev_weight) > 0.0 {
727                        prev_weight / (cur_weight + prev_weight)
728                    } else {
729                        0.0
730                    };
731                    primary = blend_mats(current, previous, normalized_prev);
732                }
733            }
734            let mat = if blend_weight > 0.0 {
735                if let Some(clip_b) = blend_clip {
736                    let secondary = if let Some(track_b) = clip_b.tracks.get(&frame.name) {
737                        sample_track(
738                            track_b,
739                            frame.base_local,
740                            primary_time,
741                            clip_b.max_time,
742                            state.looped && clip_b.open_closed,
743                            asset.ticks_per_second,
744                        )
745                    } else {
746                        frame.base_local
747                    };
748                    blend_mats(primary, secondary, blend_weight)
749                } else {
750                    primary
751                }
752            } else {
753                primary
754            };
755            local.push(mat);
756        }
757        let mut combined = vec![Mat4::identity(); asset.frames.len()];
758        if !asset.frames.is_empty() {
759            let mut has_parent = vec![false; asset.frames.len()];
760            for frame in &asset.frames {
761                for &child in &frame.children {
762                    if child < has_parent.len() {
763                        has_parent[child] = true;
764                    }
765                }
766            }
767            for frame_idx in 0..asset.frames.len() {
768                if !has_parent[frame_idx] {
769                    update_combined_recursive(
770                        &asset.frames,
771                        &local,
772                        &mut combined,
773                        frame_idx,
774                        None,
775                    );
776                }
777            }
778        }
779        Self {
780            combined,
781            frame_lookup,
782        }
783    }
784}
785
786fn update_combined_recursive(
787    frames: &[FrameNode],
788    local: &[Mat4],
789    combined: &mut [Mat4],
790    frame_idx: usize,
791    parent: Option<Mat4>,
792) {
793    let l = local.get(frame_idx).copied().unwrap_or(Mat4::identity());
794    let c = if let Some(p) = parent { l.mul(&p) } else { l };
795    combined[frame_idx] = c;
796    if let Some(frame) = frames.get(frame_idx) {
797        for &child in &frame.children {
798            update_combined_recursive(frames, local, combined, child, Some(c));
799        }
800    }
801}
802
803fn animation_time_for_state(state: &MeshAnimationState) -> f32 {
804    state.current_sample_time_sec()
805}
806
807fn blend_mats(a: Mat4, b: Mat4, w: f32) -> Mat4 {
808    let w = w.clamp(0.0, 1.0);
809    if w <= 0.0 {
810        return a;
811    }
812    if w >= 1.0 {
813        return b;
814    }
815    let (sa, ra, pa) = a.decompose_srt();
816    let (sb, rb, pb) = b.decompose_srt();
817    Mat4::from_scale_rotation_translation(
818        lerp3(sa, sb, w),
819        Quat::nlerp(ra, rb, w).normalize(),
820        lerp3(pa, pb, w),
821    )
822}
823
824fn sample_track(
825    track: &AnimationTrack,
826    base_local: Mat4,
827    time_sec: f32,
828    clip_max_time: i64,
829    looped: bool,
830    ticks_per_second: f32,
831) -> Mat4 {
832    let mut t = (time_sec.max(0.0) * ticks_per_second.max(1.0)) as i64;
833    if clip_max_time > 0 {
834        let max_time = clip_max_time.max(1);
835        if looped {
836            t %= max_time;
837        } else {
838            t = t.clamp(0, max_time);
839        }
840    }
841    if !track.matrix_keys.is_empty() {
842        return sample_mat_keys(&track.matrix_keys, t);
843    }
844    let (base_scale, base_rot, base_pos) = base_local.decompose_srt();
845    let scale = sample_vec3_keys(&track.scale_keys, t).unwrap_or(base_scale);
846    let rot = sample_quat_keys(&track.rotation_keys, t).unwrap_or(base_rot);
847    let pos = sample_vec3_keys(&track.position_keys, t).unwrap_or(base_pos);
848    Mat4::from_scale_rotation_translation(scale, rot, pos)
849}
850
851fn sample_vec3_keys(keys: &[Vec3Key], t: i64) -> Option<[f32; 3]> {
852    if keys.is_empty() {
853        return None;
854    }
855    if keys.len() == 1 {
856        return Some(keys[0].value);
857    }
858    let (a, b, k) = find_key_pair_vec3(keys, t);
859    Some(lerp3(a.value, b.value, k))
860}
861
862fn sample_quat_keys(keys: &[QuatKey], t: i64) -> Option<Quat> {
863    if keys.is_empty() {
864        return None;
865    }
866    if keys.len() == 1 {
867        return Some(keys[0].value.normalize());
868    }
869    let (a, b, k) = find_key_pair_quat(keys, t);
870    Some(Quat::nlerp(a.value, b.value, k).normalize())
871}
872
873fn sample_mat_keys(keys: &[MatKey], t: i64) -> Mat4 {
874    if keys.is_empty() {
875        return Mat4::identity();
876    }
877    if keys.len() == 1 {
878        return keys[0].value;
879    }
880    let (a, b, k) = find_key_pair_mat(keys, t);
881    Mat4::lerp(a.value, b.value, k)
882}
883
884fn find_key_pair_vec3(keys: &[Vec3Key], t: i64) -> (Vec3Key, Vec3Key, f32) {
885    for w in keys.windows(2) {
886        let a = w[0];
887        let b = w[1];
888        if t >= a.time && t <= b.time {
889            let span = (b.time - a.time).max(1) as f32;
890            return (a, b, (t - a.time) as f32 / span);
891        }
892    }
893    let first = keys[0];
894    let last = keys[keys.len() - 1];
895    if t < first.time {
896        (first, first, 0.0)
897    } else {
898        (last, last, 0.0)
899    }
900}
901
902fn find_key_pair_quat(keys: &[QuatKey], t: i64) -> (QuatKey, QuatKey, f32) {
903    for w in keys.windows(2) {
904        let a = w[0];
905        let b = w[1];
906        if t >= a.time && t <= b.time {
907            let span = (b.time - a.time).max(1) as f32;
908            return (a, b, (t - a.time) as f32 / span);
909        }
910    }
911    let first = keys[0];
912    let last = keys[keys.len() - 1];
913    if t < first.time {
914        (first, first, 0.0)
915    } else {
916        (last, last, 0.0)
917    }
918}
919
920fn find_key_pair_mat(keys: &[MatKey], t: i64) -> (MatKey, MatKey, f32) {
921    for w in keys.windows(2) {
922        let a = w[0];
923        let b = w[1];
924        if t >= a.time && t <= b.time {
925            let span = (b.time - a.time).max(1) as f32;
926            return (a, b, (t - a.time) as f32 / span);
927        }
928    }
929    let first = keys[0];
930    let last = keys[keys.len() - 1];
931    if t < first.time {
932        (first, first, 0.0)
933    } else {
934        (last, last, 0.0)
935    }
936}
937
938fn skin_matrix_for_bone(bone: &BoneBinding, pose: &PoseState) -> Mat4 {
939    let combined = pose
940        .combined
941        .get(bone.frame_index)
942        .copied()
943        .unwrap_or(Mat4::identity());
944    bone.offset_matrix.mul(&combined)
945}
946
947fn skin_vertex(
948    src: &MeshTriVertex,
949    prim: &MeshPrimitive,
950    pose: &PoseState,
951) -> ([f32; 3], [f32; 3]) {
952    let mut pos = [0.0f32; 3];
953    let mut normal = [0.0f32; 3];
954    let mut accum = 0.0f32;
955    for lane in 0..4 {
956        let weight = src.bone_weights[lane];
957        if weight <= 0.0 {
958            continue;
959        }
960        let bone_idx = src.bone_indices[lane] as usize;
961        let Some(bone) = prim.bones.get(bone_idx) else {
962            continue;
963        };
964        let skin_mat = skin_matrix_for_bone(bone, pose);
965        let p = skin_mat.transform_point(src.pos);
966        let n = skin_mat.transform_vector(src.normal);
967        for i in 0..3 {
968            pos[i] += p[i] * weight;
969            normal[i] += n[i] * weight;
970        }
971        accum += weight;
972    }
973    if accum <= 1e-6 {
974        let frame_world = pose
975            .combined
976            .get(prim.frame_index)
977            .copied()
978            .unwrap_or(Mat4::identity());
979        (
980            frame_world.transform_point(src.pos),
981            frame_world.transform_vector(src.normal),
982        )
983    } else {
984        (pos, normal)
985    }
986}
987
988#[derive(Debug, Clone, Copy, Default)]
989struct Quat {
990    x: f32,
991    y: f32,
992    z: f32,
993    w: f32,
994}
995
996impl Quat {
997    fn normalize(self) -> Self {
998        let len = (self.x * self.x + self.y * self.y + self.z * self.z + self.w * self.w).sqrt();
999        if len <= 1e-8 {
1000            return Self {
1001                x: 0.0,
1002                y: 0.0,
1003                z: 0.0,
1004                w: 1.0,
1005            };
1006        }
1007        let inv = 1.0 / len;
1008        Self {
1009            x: self.x * inv,
1010            y: self.y * inv,
1011            z: self.z * inv,
1012            w: self.w * inv,
1013        }
1014    }
1015
1016    fn nlerp(a: Self, mut b: Self, t: f32) -> Self {
1017        let mut dot = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
1018        if dot < 0.0 {
1019            dot = -dot;
1020            b = Self {
1021                x: -b.x,
1022                y: -b.y,
1023                z: -b.z,
1024                w: -b.w,
1025            };
1026        }
1027        let k = t.clamp(0.0, 1.0);
1028        Self {
1029            x: a.x + (b.x - a.x) * k,
1030            y: a.y + (b.y - a.y) * k,
1031            z: a.z + (b.z - a.z) * k,
1032            w: a.w + (b.w - a.w) * k,
1033        }
1034        .normalize()
1035    }
1036}
1037
1038#[derive(Debug, Clone, Copy)]
1039struct Mat4 {
1040    m: [[f32; 4]; 4],
1041}
1042
1043impl Mat4 {
1044    fn identity() -> Self {
1045        Self {
1046            m: [
1047                [1.0, 0.0, 0.0, 0.0],
1048                [0.0, 1.0, 0.0, 0.0],
1049                [0.0, 0.0, 1.0, 0.0],
1050                [0.0, 0.0, 0.0, 1.0],
1051            ],
1052        }
1053    }
1054
1055    fn from_x_values(vals: [f32; 16]) -> Self {
1056        Self {
1057            m: [
1058                [vals[0], vals[1], vals[2], vals[3]],
1059                [vals[4], vals[5], vals[6], vals[7]],
1060                [vals[8], vals[9], vals[10], vals[11]],
1061                [vals[12], vals[13], vals[14], vals[15]],
1062            ],
1063        }
1064    }
1065
1066    fn mul(&self, rhs: &Self) -> Self {
1067        let mut out = [[0.0f32; 4]; 4];
1068        for r in 0..4 {
1069            for c in 0..4 {
1070                out[r][c] = self.m[r][0] * rhs.m[0][c]
1071                    + self.m[r][1] * rhs.m[1][c]
1072                    + self.m[r][2] * rhs.m[2][c]
1073                    + self.m[r][3] * rhs.m[3][c];
1074            }
1075        }
1076        Self { m: out }
1077    }
1078
1079    fn transform_point(&self, p: [f32; 3]) -> [f32; 3] {
1080        [
1081            p[0] * self.m[0][0] + p[1] * self.m[1][0] + p[2] * self.m[2][0] + self.m[3][0],
1082            p[0] * self.m[0][1] + p[1] * self.m[1][1] + p[2] * self.m[2][1] + self.m[3][1],
1083            p[0] * self.m[0][2] + p[1] * self.m[1][2] + p[2] * self.m[2][2] + self.m[3][2],
1084        ]
1085    }
1086
1087    fn transform_vector(&self, p: [f32; 3]) -> [f32; 3] {
1088        [
1089            p[0] * self.m[0][0] + p[1] * self.m[1][0] + p[2] * self.m[2][0],
1090            p[0] * self.m[0][1] + p[1] * self.m[1][1] + p[2] * self.m[2][1],
1091            p[0] * self.m[0][2] + p[1] * self.m[1][2] + p[2] * self.m[2][2],
1092        ]
1093    }
1094
1095    fn lerp(a: Self, b: Self, t: f32) -> Self {
1096        let k = t.clamp(0.0, 1.0);
1097        let mut out = [[0.0f32; 4]; 4];
1098        for r in 0..4 {
1099            for c in 0..4 {
1100                out[r][c] = a.m[r][c] + (b.m[r][c] - a.m[r][c]) * k;
1101            }
1102        }
1103        Self { m: out }
1104    }
1105
1106    fn from_scale_rotation_translation(scale: [f32; 3], rot: Quat, pos: [f32; 3]) -> Self {
1107        let q = rot.normalize();
1108        let xx = q.x * q.x;
1109        let yy = q.y * q.y;
1110        let zz = q.z * q.z;
1111        let xy = q.x * q.y;
1112        let xz = q.x * q.z;
1113        let yz = q.y * q.z;
1114        let wx = q.w * q.x;
1115        let wy = q.w * q.y;
1116        let wz = q.w * q.z;
1117        let mut m = Self::identity();
1118        m.m[0][0] = (1.0 - 2.0 * (yy + zz)) * scale[0];
1119        m.m[0][1] = (2.0 * (xy + wz)) * scale[0];
1120        m.m[0][2] = (2.0 * (xz - wy)) * scale[0];
1121        m.m[1][0] = (2.0 * (xy - wz)) * scale[1];
1122        m.m[1][1] = (1.0 - 2.0 * (xx + zz)) * scale[1];
1123        m.m[1][2] = (2.0 * (yz + wx)) * scale[1];
1124        m.m[2][0] = (2.0 * (xz + wy)) * scale[2];
1125        m.m[2][1] = (2.0 * (yz - wx)) * scale[2];
1126        m.m[2][2] = (1.0 - 2.0 * (xx + yy)) * scale[2];
1127        m.m[3][0] = pos[0];
1128        m.m[3][1] = pos[1];
1129        m.m[3][2] = pos[2];
1130        m
1131    }
1132
1133    fn decompose_srt(&self) -> ([f32; 3], Quat, [f32; 3]) {
1134        let pos = [self.m[3][0], self.m[3][1], self.m[3][2]];
1135        let sx = (self.m[0][0] * self.m[0][0]
1136            + self.m[0][1] * self.m[0][1]
1137            + self.m[0][2] * self.m[0][2])
1138            .sqrt()
1139            .max(1e-8);
1140        let sy = (self.m[1][0] * self.m[1][0]
1141            + self.m[1][1] * self.m[1][1]
1142            + self.m[1][2] * self.m[1][2])
1143            .sqrt()
1144            .max(1e-8);
1145        let sz = (self.m[2][0] * self.m[2][0]
1146            + self.m[2][1] * self.m[2][1]
1147            + self.m[2][2] * self.m[2][2])
1148            .sqrt()
1149            .max(1e-8);
1150        let scale = [sx, sy, sz];
1151        let r00 = self.m[0][0] / sx;
1152        let r01 = self.m[0][1] / sx;
1153        let r02 = self.m[0][2] / sx;
1154        let r10 = self.m[1][0] / sy;
1155        let r11 = self.m[1][1] / sy;
1156        let r12 = self.m[1][2] / sy;
1157        let r20 = self.m[2][0] / sz;
1158        let r21 = self.m[2][1] / sz;
1159        let r22 = self.m[2][2] / sz;
1160        let trace = r00 + r11 + r22;
1161        let rot = if trace > 0.0 {
1162            let s = (trace + 1.0).sqrt() * 2.0;
1163            Quat {
1164                w: 0.25 * s,
1165                x: (r21 - r12) / s,
1166                y: (r02 - r20) / s,
1167                z: (r10 - r01) / s,
1168            }
1169        } else if r00 > r11 && r00 > r22 {
1170            let s = (1.0 + r00 - r11 - r22).sqrt() * 2.0;
1171            Quat {
1172                w: (r21 - r12) / s,
1173                x: 0.25 * s,
1174                y: (r01 + r10) / s,
1175                z: (r02 + r20) / s,
1176            }
1177        } else if r11 > r22 {
1178            let s = (1.0 + r11 - r00 - r22).sqrt() * 2.0;
1179            Quat {
1180                w: (r02 - r20) / s,
1181                x: (r01 + r10) / s,
1182                y: 0.25 * s,
1183                z: (r12 + r21) / s,
1184            }
1185        } else {
1186            let s = (1.0 + r22 - r00 - r11).sqrt() * 2.0;
1187            Quat {
1188                w: (r10 - r01) / s,
1189                x: (r02 + r20) / s,
1190                y: (r12 + r21) / s,
1191                z: 0.25 * s,
1192            }
1193        };
1194        (scale, rot.normalize(), pos)
1195    }
1196}
1197
1198fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
1199    [
1200        a[1] * b[2] - a[2] * b[1],
1201        a[2] * b[0] - a[0] * b[2],
1202        a[0] * b[1] - a[1] * b[0],
1203    ]
1204}
1205
1206fn normalize3(v: [f32; 3]) -> [f32; 3] {
1207    let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
1208    if len <= 1e-8 {
1209        [0.0, 0.0, 1.0]
1210    } else {
1211        [v[0] / len, v[1] / len, v[2] / len]
1212    }
1213}
1214
1215fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
1216    let k = t.clamp(0.0, 1.0);
1217    [
1218        a[0] + (b[0] - a[0]) * k,
1219        a[1] + (b[1] - a[1]) * k,
1220        a[2] + (b[2] - a[2]) * k,
1221    ]
1222}
1223
1224#[derive(Debug, Clone)]
1225enum Tok {
1226    Ident(String),
1227    Number(f32),
1228    Str(String),
1229    Sym(char),
1230}
1231
1232struct Cursor {
1233    toks: Vec<Tok>,
1234    i: usize,
1235}
1236
1237impl Cursor {
1238    fn new(toks: Vec<Tok>) -> Self {
1239        Self { toks, i: 0 }
1240    }
1241
1242    fn peek(&self) -> Option<&Tok> {
1243        self.toks.get(self.i)
1244    }
1245
1246    fn next(&mut self) -> Option<Tok> {
1247        let out = self.toks.get(self.i).cloned();
1248        if out.is_some() {
1249            self.i += 1;
1250        }
1251        out
1252    }
1253
1254    fn consume_ident(&mut self, name: &str) -> bool {
1255        match self.peek() {
1256            Some(Tok::Ident(s)) if s == name => {
1257                self.i += 1;
1258                true
1259            }
1260            _ => false,
1261        }
1262    }
1263
1264    fn consume_sym(&mut self, ch: char) -> bool {
1265        match self.peek() {
1266            Some(Tok::Sym(c)) if *c == ch => {
1267                self.i += 1;
1268                true
1269            }
1270            _ => false,
1271        }
1272    }
1273
1274    fn next_number(&mut self) -> Result<f32> {
1275        match self.next() {
1276            Some(Tok::Number(v)) => Ok(v),
1277            other => bail!("expected number, got {:?}", other),
1278        }
1279    }
1280
1281    fn next_i64(&mut self) -> Result<i64> {
1282        Ok(self.next_number()? as i64)
1283    }
1284
1285    fn next_usize(&mut self) -> Result<usize> {
1286        Ok(self.next_number()? as usize)
1287    }
1288
1289    fn next_string(&mut self) -> Result<String> {
1290        match self.next() {
1291            Some(Tok::Str(s)) => Ok(s),
1292            Some(Tok::Ident(s)) => Ok(s),
1293            other => bail!("expected string, got {:?}", other),
1294        }
1295    }
1296
1297    fn optional_name_before_block(&mut self) -> Option<String> {
1298        match self.peek() {
1299            Some(Tok::Ident(s)) if matches!(self.toks.get(self.i + 1), Some(Tok::Sym('{'))) => {
1300                let out = s.clone();
1301                self.i += 1;
1302                Some(out)
1303            }
1304            Some(Tok::Str(s)) if matches!(self.toks.get(self.i + 1), Some(Tok::Sym('{'))) => {
1305                let out = s.clone();
1306                self.i += 1;
1307                Some(out)
1308            }
1309            _ => None,
1310        }
1311    }
1312
1313    fn expect_block_start(&mut self) -> Result<()> {
1314        let _ = self.optional_name_before_block();
1315        if self.consume_sym('{') {
1316            Ok(())
1317        } else {
1318            bail!("expected '{{' at token {}", self.i)
1319        }
1320    }
1321
1322    fn skip_block(&mut self) {
1323        let _ = self.optional_name_before_block();
1324        if !self.consume_sym('{') {
1325            let _ = self.next();
1326            return;
1327        }
1328        let mut depth = 1usize;
1329        while let Some(tok) = self.next() {
1330            match tok {
1331                Tok::Sym('{') => depth += 1,
1332                Tok::Sym('}') => {
1333                    depth -= 1;
1334                    if depth == 0 {
1335                        break;
1336                    }
1337                }
1338                _ => {}
1339            }
1340        }
1341    }
1342}
1343
1344fn find_existing_casefold(path: &Path) -> Option<PathBuf> {
1345    if path.exists() {
1346        return Some(path.to_path_buf());
1347    }
1348    let parent = path.parent()?;
1349    let name = path.file_name()?.to_string_lossy().to_string();
1350    let norm = name.replace('\\', "/");
1351    let entries = fs::read_dir(parent).ok()?;
1352    for ent in entries.flatten() {
1353        let fname = ent.file_name().to_string_lossy().to_string();
1354        if fname.eq_ignore_ascii_case(&norm) {
1355            return Some(ent.path());
1356        }
1357    }
1358    None
1359}
1360
1361fn resolve_relative_casefold(base: &Path, rel: &str) -> Option<PathBuf> {
1362    let mut cur = base.to_path_buf();
1363    for part in rel.replace('\\', "/").split('/') {
1364        if part.is_empty() || part == "." {
1365            continue;
1366        }
1367        if part == ".." {
1368            cur = cur.parent()?.to_path_buf();
1369            continue;
1370        }
1371        let next = cur.join(part);
1372        cur = find_existing_casefold(&next).unwrap_or(next);
1373    }
1374    if cur.exists() {
1375        Some(cur)
1376    } else {
1377        find_existing_casefold(&cur)
1378    }
1379}
1380
1381pub fn resolve_mesh_path(project_dir: &Path, append_dir: &str, file_name: &str) -> Result<PathBuf> {
1382    if file_name.trim().is_empty() {
1383        bail!("empty mesh file name")
1384    }
1385    let raw = Path::new(file_name);
1386    if raw.is_absolute() {
1387        let x_raw = if raw.extension().is_some() {
1388            raw.to_path_buf()
1389        } else {
1390            raw.with_extension("x")
1391        };
1392        if let Some(found) = find_existing_casefold(&x_raw) {
1393            return Ok(found);
1394        }
1395        let sgmesh_raw = raw.with_extension("sgmesh");
1396        if let Some(found) = find_existing_casefold(&sgmesh_raw) {
1397            return Ok(found);
1398        }
1399    }
1400    let norm = file_name.replace('\\', "/");
1401    let p = Path::new(&norm);
1402    let x_name = if p.extension().is_some() {
1403        norm.clone()
1404    } else {
1405        format!("{norm}.x")
1406    };
1407    let sgmesh_name = if p
1408        .extension()
1409        .and_then(|s| s.to_str())
1410        .map(|s| s.eq_ignore_ascii_case("sgmesh"))
1411        .unwrap_or(false)
1412    {
1413        norm.clone()
1414    } else {
1415        p.with_extension("sgmesh")
1416            .to_string_lossy()
1417            .replace('\\', "/")
1418    };
1419
1420    for append in crate::resource::ordered_append_dirs(project_dir, append_dir) {
1421        let base = if append.is_empty() {
1422            project_dir.join("x")
1423        } else {
1424            project_dir.join(&append).join("x")
1425        };
1426        if let Some(found) = resolve_relative_casefold(&base, &x_name) {
1427            return Ok(found);
1428        }
1429    }
1430
1431    for append in crate::resource::ordered_append_dirs(project_dir, append_dir) {
1432        let base = if append.is_empty() {
1433            project_dir.join("x")
1434        } else {
1435            project_dir.join(&append).join("x")
1436        };
1437        if let Some(found) = resolve_relative_casefold(&base, &sgmesh_name) {
1438            return Ok(found);
1439        }
1440    }
1441
1442    bail!("mesh asset not found through x resource path: {file_name}")
1443}
1444
1445pub fn load_mesh_asset(project_dir: &Path, append_dir: &str, file_name: &str) -> Result<MeshAsset> {
1446    let path = resolve_mesh_path(project_dir, append_dir, file_name)?;
1447    load_mesh_asset_from_path(&path)
1448}
1449
1450fn load_mesh_asset_from_path(path: &Path) -> Result<MeshAsset> {
1451    let bytes = fs::read(path).with_context(|| format!("read mesh {:?}", path))?;
1452    let ext = path
1453        .extension()
1454        .and_then(|s| s.to_str())
1455        .unwrap_or("")
1456        .to_ascii_lowercase();
1457    let mut asset = if ext == "sgmesh" {
1458        read_internal_mesh_asset(path)?
1459    } else if ext == "obj" {
1460        parse_obj(&decode_text(&bytes), path)?
1461    } else if ext == "x" {
1462        import_x_scene_bytes(&bytes, path)?.into_mesh_asset(path)?
1463    } else {
1464        let text = decode_text(&bytes);
1465        if text.contains("Mesh") && text.contains("xof") {
1466            import_x_scene_bytes(&bytes, path)?.into_mesh_asset(path)?
1467        } else {
1468            parse_obj(&text, path)?
1469        }
1470    };
1471    finalize_mesh_asset(&mut asset);
1472    Ok(asset)
1473}
1474
1475fn finalize_mesh_asset(asset: &mut MeshAsset) {
1476    if asset.texture_path.is_none() {
1477        asset.texture_path = asset.primitives.iter().find_map(|p| p.texture_path.clone());
1478    }
1479    for prim in &mut asset.primitives {
1480        if prim.runtime_desc.effect_key.is_empty() || prim.runtime_desc.technique_name.is_empty() {
1481            prim.runtime_desc = build_mesh_primitive_runtime_desc(prim);
1482        } else {
1483            prim.runtime_desc.vertex_stride_bytes = std::mem::size_of::<MeshTriVertex>() as u32;
1484            prim.runtime_desc.vertex_count = prim.vertices.len() as u32;
1485            prim.runtime_desc.bone_palette_len = prim.bones.len() as u32;
1486        }
1487    }
1488    if asset.vertices.is_empty() {
1489        asset.vertices = asset.sample_vertices(0.0);
1490    }
1491    asset.bone_count = asset
1492        .primitives
1493        .iter()
1494        .map(|p| p.bones.len())
1495        .max()
1496        .unwrap_or(0);
1497    if asset.ticks_per_second <= 0.0 {
1498        asset.ticks_per_second = 1.0;
1499    }
1500}
1501
1502pub fn internal_mesh_asset_path_for_source(path: &Path) -> PathBuf {
1503    path.with_extension("sgmesh")
1504}
1505
1506pub fn compile_mesh_asset_file(input: &Path, output: &Path) -> Result<()> {
1507    let mut asset = load_mesh_asset_from_path(input)?;
1508    asset.source_path = input.to_path_buf();
1509    write_internal_mesh_asset(output, &asset)
1510}
1511
1512fn decode_text(bytes: &[u8]) -> String {
1513    if bytes.len() >= 2 && bytes[1] == 0 {
1514        let mut u16s = Vec::with_capacity(bytes.len() / 2);
1515        let mut i = 0usize;
1516        while i + 1 < bytes.len() {
1517            u16s.push(u16::from_le_bytes([bytes[i], bytes[i + 1]]));
1518            i += 2;
1519        }
1520        String::from_utf16_lossy(&u16s)
1521    } else {
1522        String::from_utf8_lossy(bytes).into_owned()
1523    }
1524}
1525
1526fn parse_obj(text: &str, path: &Path) -> Result<MeshAsset> {
1527    #[derive(Debug, Clone)]
1528    struct ObjGroup {
1529        material_name: Option<String>,
1530        vertices: Vec<MeshTriVertex>,
1531    }
1532
1533    let mut positions = Vec::<[f32; 3]>::new();
1534    let mut texcoords = Vec::<[f32; 2]>::new();
1535    let mut normals = Vec::<[f32; 3]>::new();
1536    let mut mtllibs = Vec::<PathBuf>::new();
1537    let mut groups = vec![ObjGroup {
1538        material_name: None,
1539        vertices: Vec::new(),
1540    }];
1541    let mut current_group = 0usize;
1542
1543    for line in text.lines() {
1544        let line = line.trim();
1545        if line.is_empty() || line.starts_with('#') {
1546            continue;
1547        }
1548        let mut parts = line.split_whitespace();
1549        let Some(tag) = parts.next() else {
1550            continue;
1551        };
1552        match tag {
1553            "v" => {
1554                let vals: Vec<f32> = parts.filter_map(|s| s.parse::<f32>().ok()).collect();
1555                if vals.len() >= 3 {
1556                    positions.push([vals[0], vals[1], vals[2]]);
1557                }
1558            }
1559            "vt" => {
1560                let vals: Vec<f32> = parts.filter_map(|s| s.parse::<f32>().ok()).collect();
1561                if vals.len() >= 2 {
1562                    texcoords.push([vals[0], 1.0 - vals[1]]);
1563                }
1564            }
1565            "vn" => {
1566                let vals: Vec<f32> = parts.filter_map(|s| s.parse::<f32>().ok()).collect();
1567                if vals.len() >= 3 {
1568                    normals.push(normalize3([vals[0], vals[1], vals[2]]));
1569                }
1570            }
1571            "mtllib" => {
1572                for name in parts {
1573                    let cand = if let Some(parent) = path.parent() {
1574                        parent.join(name)
1575                    } else {
1576                        PathBuf::from(name)
1577                    };
1578                    mtllibs.push(cand);
1579                }
1580            }
1581            "usemtl" => {
1582                let mat = parts.next().map(|s| s.to_string());
1583                if let Some(idx) = groups.iter().position(|g| g.material_name == mat) {
1584                    current_group = idx;
1585                } else {
1586                    current_group = groups.len();
1587                    groups.push(ObjGroup {
1588                        material_name: mat,
1589                        vertices: Vec::new(),
1590                    });
1591                }
1592            }
1593            "f" => {
1594                let items: Vec<String> = parts.map(|s| s.to_string()).collect();
1595                if items.len() < 3 {
1596                    continue;
1597                }
1598                let mut face_verts = Vec::<MeshTriVertex>::new();
1599                for item in items {
1600                    let comps: Vec<&str> = item.split('/').collect();
1601                    let vi = comps
1602                        .get(0)
1603                        .and_then(|s| s.parse::<isize>().ok())
1604                        .unwrap_or(0);
1605                    let ti = comps
1606                        .get(1)
1607                        .and_then(|s| {
1608                            if s.is_empty() {
1609                                None
1610                            } else {
1611                                s.parse::<isize>().ok()
1612                            }
1613                        })
1614                        .unwrap_or(0);
1615                    let ni = comps
1616                        .get(2)
1617                        .and_then(|s| {
1618                            if s.is_empty() {
1619                                None
1620                            } else {
1621                                s.parse::<isize>().ok()
1622                            }
1623                        })
1624                        .unwrap_or(0);
1625                    let pos = obj_index(&positions, vi)
1626                        .copied()
1627                        .unwrap_or([0.0, 0.0, 0.0]);
1628                    let uv = obj_index(&texcoords, ti).copied().unwrap_or([0.0, 0.0]);
1629                    let normal = obj_index(&normals, ni).copied().unwrap_or([0.0, 0.0, 0.0]);
1630                    face_verts.push(MeshTriVertex {
1631                        pos,
1632                        uv,
1633                        normal,
1634                        tangent: [0.0, 0.0, 0.0],
1635                        binormal: [0.0, 0.0, 0.0],
1636                        color: [1.0, 1.0, 1.0, 1.0],
1637                        bone_indices: [0; 4],
1638                        bone_weights: [0.0; 4],
1639                    });
1640                }
1641                for i in 1..face_verts.len() - 1 {
1642                    let a = face_verts[0].clone();
1643                    let b = face_verts[i].clone();
1644                    let c = face_verts[i + 1].clone();
1645                    let n = triangle_normal(a.pos, b.pos, c.pos);
1646                    groups[current_group].vertices.push(MeshTriVertex {
1647                        normal: if has_normal(a.normal) { a.normal } else { n },
1648                        ..a
1649                    });
1650                    groups[current_group].vertices.push(MeshTriVertex {
1651                        normal: if has_normal(b.normal) { b.normal } else { n },
1652                        ..b
1653                    });
1654                    groups[current_group].vertices.push(MeshTriVertex {
1655                        normal: if has_normal(c.normal) { c.normal } else { n },
1656                        ..c
1657                    });
1658                }
1659            }
1660            _ => {}
1661        }
1662    }
1663
1664    let material_map = parse_obj_material_libs(path, &mtllibs);
1665    let mut primitives = Vec::new();
1666    let mut combined = Vec::new();
1667    let mut fallback_tex = None;
1668    for group in groups {
1669        if group.vertices.is_empty() {
1670            continue;
1671        }
1672        let (material, texture_path) = if let Some(name) = group.material_name.as_deref() {
1673            material_map
1674                .get(name)
1675                .cloned()
1676                .unwrap_or((default_mesh_material(), None))
1677        } else {
1678            (default_mesh_material(), None)
1679        };
1680        if fallback_tex.is_none() {
1681            fallback_tex = texture_path.clone();
1682        }
1683        let mut vertices = group.vertices;
1684        apply_tangent_space(&mut vertices);
1685        combined.extend(vertices.iter().cloned());
1686        primitives.push(MeshPrimitive {
1687            frame_index: 0,
1688            texture_path,
1689            material,
1690            runtime_desc: MeshPrimitiveRuntimeDesc::default(),
1691            vertices,
1692            bones: Vec::new(),
1693        });
1694    }
1695    if primitives.is_empty() {
1696        bail!("no faces in {:?}", path);
1697    }
1698    Ok(MeshAsset {
1699        source_path: path.to_path_buf(),
1700        texture_path: fallback_tex,
1701        vertices: combined,
1702        bone_count: 0,
1703        ticks_per_second: 4800.0,
1704        primitives,
1705        frames: vec![FrameNode {
1706            name: "__root__".to_string(),
1707            base_local: Mat4::identity(),
1708            children: Vec::new(),
1709        }],
1710        animations: Vec::new(),
1711    })
1712}
1713
1714fn parse_obj_material_libs(
1715    mesh_path: &Path,
1716    libs: &[PathBuf],
1717) -> HashMap<String, (MeshMaterial, Option<PathBuf>)> {
1718    let mut out = HashMap::new();
1719    for lib in libs {
1720        if let Ok(text) = fs::read_to_string(lib) {
1721            let base = lib
1722                .parent()
1723                .unwrap_or_else(|| mesh_path.parent().unwrap_or(Path::new(".")));
1724            let mut current_name: Option<String> = None;
1725            let mut current_material = default_mesh_material();
1726            let mut current_texture: Option<PathBuf> = None;
1727            for line in text.lines() {
1728                let line = line.trim();
1729                if line.is_empty() || line.starts_with('#') {
1730                    continue;
1731                }
1732                let mut parts = line.split_whitespace();
1733                let Some(tag) = parts.next() else {
1734                    continue;
1735                };
1736                match tag {
1737                    "newmtl" => {
1738                        if let Some(name) = current_name.take() {
1739                            out.insert(name, (current_material.clone(), current_texture.clone()));
1740                        }
1741                        current_name = parts.next().map(|s| s.to_string());
1742                        current_material = default_mesh_material();
1743                        current_texture = None;
1744                    }
1745                    "Ka" => {
1746                        let vals: Vec<f32> = parts.filter_map(|s| s.parse::<f32>().ok()).collect();
1747                        if vals.len() >= 3 {
1748                            current_material.ambient = [vals[0], vals[1], vals[2], 1.0];
1749                        }
1750                    }
1751                    "Kd" => {
1752                        let vals: Vec<f32> = parts.filter_map(|s| s.parse::<f32>().ok()).collect();
1753                        if vals.len() >= 3 {
1754                            current_material.diffuse = [vals[0], vals[1], vals[2], 1.0];
1755                        }
1756                    }
1757                    "Ks" => {
1758                        let vals: Vec<f32> = parts.filter_map(|s| s.parse::<f32>().ok()).collect();
1759                        if vals.len() >= 3 {
1760                            current_material.specular = [vals[0], vals[1], vals[2], 1.0];
1761                        }
1762                    }
1763                    "Ke" => {
1764                        let vals: Vec<f32> = parts.filter_map(|s| s.parse::<f32>().ok()).collect();
1765                        if vals.len() >= 3 {
1766                            current_material.emissive = [vals[0], vals[1], vals[2], 1.0];
1767                        }
1768                    }
1769                    "Ns" => {
1770                        if let Some(v) = parts.next().and_then(|s| s.parse::<f32>().ok()) {
1771                            current_material.power = v.max(1.0);
1772                        }
1773                    }
1774                    "illum" => {
1775                        if let Some(v) = parts.next().and_then(|s| s.parse::<i32>().ok()) {
1776                            current_material.lighting_type = match v {
1777                                0 => MeshLightingType::None,
1778                                1 => MeshLightingType::Lambert,
1779                                2..=10 => MeshLightingType::BlinnPhong,
1780                                _ => current_material.lighting_type,
1781                            };
1782                        }
1783                    }
1784                    "d" | "Tr" => {
1785                        if let Some(v) = parts.next().and_then(|s| s.parse::<f32>().ok()) {
1786                            current_material.diffuse[3] = if tag == "Tr" { 1.0 - v } else { v };
1787                        }
1788                    }
1789                    "map_Kd" => {
1790                        if let Some(name) = parts.next() {
1791                            let tex = base.join(name);
1792                            current_texture = if tex.exists() {
1793                                Some(tex)
1794                            } else {
1795                                resolve_texture_path(mesh_path, name)
1796                            };
1797                        }
1798                    }
1799                    "map_Bump" | "bump" | "norm" => {
1800                        if let Some(name) = parts.next() {
1801                            let tex = base.join(name);
1802                            current_material.normal_texture_path = if tex.exists() {
1803                                Some(tex)
1804                            } else {
1805                                resolve_texture_path(mesh_path, name)
1806                            };
1807                            if current_material.normal_texture_path.is_some() {
1808                                current_material.lighting_type = MeshLightingType::Bump;
1809                            }
1810                        }
1811                    }
1812                    _ => {}
1813                }
1814            }
1815            if let Some(name) = current_name.take() {
1816                out.insert(name, (current_material, current_texture));
1817            }
1818        }
1819    }
1820    out
1821}
1822
1823fn default_mesh_material() -> MeshMaterial {
1824    MeshMaterial {
1825        diffuse: [1.0, 1.0, 1.0, 1.0],
1826        ambient: [1.0, 1.0, 1.0, 1.0],
1827        specular: [0.0, 0.0, 0.0, 1.0],
1828        emissive: [0.0, 0.0, 0.0, 1.0],
1829        power: 16.0,
1830        lighting_type: MeshLightingType::None,
1831        shading_type: MeshShadingType::None,
1832        shader_option: MESH_SHADER_OPTION_NONE,
1833        rim_light_color: [1.0, 1.0, 1.0, 1.0],
1834        rim_light_power: 1.0,
1835        parallax_max_height: 0.016,
1836        alpha_test_enable: false,
1837        alpha_ref: 0.001,
1838        cull_disable: false,
1839        shadow_map_enable: true,
1840        use_mesh_tex: true,
1841        use_mrbd: false,
1842        mrbd: [0.0, 0.0, 0.0, 0.0],
1843        use_rgb: false,
1844        rgb_rate: [0.0, 0.0, 0.0, 0.0],
1845        add_rgb: [0.0, 0.0, 0.0, 0.0],
1846        use_mul_vertex_color: false,
1847        mul_vertex_color_rate: 1.0,
1848        depth_buffer_shadow_bias: 0.03,
1849        directional_light_ids: vec![10],
1850        point_light_ids: vec![20],
1851        spot_light_ids: vec![30],
1852        normal_texture_path: None,
1853        toon_texture_path: None,
1854        effect_filename: None,
1855    }
1856}
1857
1858pub fn mesh_runtime_material_key(
1859    material: &MeshMaterial,
1860    has_texture: bool,
1861    skinned: bool,
1862) -> MeshRuntimeMaterialKey {
1863    let use_shadow_tex = material.shading_type == MeshShadingType::DepthBuffer;
1864    let use_toon_tex =
1865        material.lighting_type == MeshLightingType::Toon && material.toon_texture_path.is_some();
1866    let use_normal_tex = matches!(
1867        material.lighting_type,
1868        MeshLightingType::Bump | MeshLightingType::Parallax
1869    ) && material.normal_texture_path.is_some();
1870    MeshRuntimeMaterialKey {
1871        use_mesh_tex: has_texture && material.use_mesh_tex,
1872        use_shadow_tex,
1873        use_toon_tex,
1874        use_normal_tex,
1875        use_mul_vertex_color: material.use_mul_vertex_color,
1876        use_mrbd: material.use_mrbd,
1877        use_rgb: material.use_rgb,
1878        lighting_type: material.lighting_type,
1879        shading_type: material.shading_type,
1880        shader_option: material.shader_option,
1881        skinned,
1882        alpha_test_enable: material.alpha_test_enable,
1883        cull_disable: material.cull_disable,
1884        shadow_map_enable: material.shadow_map_enable,
1885    }
1886}
1887
1888pub fn mesh_effect_variant_bits(material: &MeshMaterial, has_texture: bool, skinned: bool) -> u64 {
1889    mesh_effect_variant_bits_from_runtime_key(mesh_runtime_material_key(
1890        material,
1891        has_texture,
1892        skinned,
1893    ))
1894}
1895
1896pub fn mesh_effect_variant_bits_from_runtime_desc(desc: &MeshPrimitiveRuntimeDesc) -> u64 {
1897    mesh_effect_variant_bits_from_runtime_key(desc.material_key)
1898}
1899
1900pub fn mesh_effect_variant_bits_from_runtime_key(key: MeshRuntimeMaterialKey) -> u64 {
1901    let mut bits = 0u64;
1902    if key.use_mesh_tex {
1903        bits |= 1 << 0;
1904    }
1905    if key.use_shadow_tex {
1906        bits |= 1 << 1;
1907    }
1908    if key.use_toon_tex {
1909        bits |= 1 << 2;
1910    }
1911    if key.use_normal_tex {
1912        bits |= 1 << 3;
1913    }
1914    if key.use_mul_vertex_color {
1915        bits |= 1 << 4;
1916    }
1917    if key.use_mrbd {
1918        bits |= 1 << 5;
1919    }
1920    if key.use_rgb {
1921        bits |= 1 << 6;
1922    }
1923    if key.shader_option & MESH_SHADER_OPTION_RIM_LIGHT != 0 {
1924        bits |= 1 << 7;
1925    }
1926    bits |= ((key.lighting_type as u64) & 0xF) << 8;
1927    bits |= ((key.shading_type as u64) & 0xF) << 12;
1928    if key.skinned {
1929        bits |= 1 << 16;
1930    }
1931    if key.alpha_test_enable {
1932        bits |= 1 << 17;
1933    }
1934    if key.cull_disable {
1935        bits |= 1 << 18;
1936    }
1937    if key.shadow_map_enable {
1938        bits |= 1 << 19;
1939    }
1940    bits
1941}
1942
1943pub fn mesh_effect_key_from_variant(bits: u64) -> String {
1944    let key = MeshRuntimeMaterialKey {
1945        use_mesh_tex: (bits & (1 << 0)) != 0,
1946        use_shadow_tex: (bits & (1 << 1)) != 0,
1947        use_toon_tex: (bits & (1 << 2)) != 0,
1948        use_normal_tex: (bits & (1 << 3)) != 0,
1949        use_mul_vertex_color: (bits & (1 << 4)) != 0,
1950        use_mrbd: (bits & (1 << 5)) != 0,
1951        use_rgb: (bits & (1 << 6)) != 0,
1952        lighting_type: match ((bits >> 8) & 0xF) as u32 {
1953            1 => MeshLightingType::Lambert,
1954            2 => MeshLightingType::BlinnPhong,
1955            3 => MeshLightingType::PerPixelBlinnPhong,
1956            4 => MeshLightingType::PerPixelHalfLambert,
1957            5 => MeshLightingType::Toon,
1958            6 => MeshLightingType::FixedFunction,
1959            7 => MeshLightingType::PerPixelFixedFunction,
1960            8 => MeshLightingType::Bump,
1961            9 => MeshLightingType::Parallax,
1962            _ => MeshLightingType::None,
1963        },
1964        shading_type: if ((bits >> 12) & 0xF) as u32 == 1 {
1965            MeshShadingType::DepthBuffer
1966        } else {
1967            MeshShadingType::None
1968        },
1969        shader_option: if (bits & (1 << 7)) != 0 {
1970            MESH_SHADER_OPTION_RIM_LIGHT
1971        } else {
1972            MESH_SHADER_OPTION_NONE
1973        },
1974        skinned: (bits & (1 << 16)) != 0,
1975        alpha_test_enable: (bits & (1 << 17)) != 0,
1976        cull_disable: (bits & (1 << 18)) != 0,
1977        shadow_map_enable: (bits & (1 << 19)) != 0,
1978    };
1979    mesh_effect_filename_from_runtime_key(
1980        if key.skinned {
1981            MeshEffectProfile::SkinnedMesh
1982        } else {
1983            MeshEffectProfile::Mesh
1984        },
1985        key,
1986    )
1987}
1988
1989pub fn mesh_effect_filename_from_runtime_key(
1990    effect_profile: MeshEffectProfile,
1991    key: MeshRuntimeMaterialKey,
1992) -> String {
1993    match effect_profile {
1994        MeshEffectProfile::ShadowMap => {
1995            let mut name = String::from("shadow_map");
1996            if key.skinned {
1997                name.push_str("_skn_p_bw_bi_n.fx");
1998            } else {
1999                name.push_str("_frm_p_n.fx");
2000            }
2001            name
2002        }
2003        MeshEffectProfile::Mesh | MeshEffectProfile::SkinnedMesh | MeshEffectProfile::None => {
2004            let mut name =
2005                if key.skinned || matches!(effect_profile, MeshEffectProfile::SkinnedMesh) {
2006                    String::from("skinned")
2007                } else {
2008                    String::from("mesh")
2009                };
2010            if key.use_mesh_tex {
2011                name.push_str("_mt");
2012            }
2013            if key.use_shadow_tex {
2014                name.push_str("_st");
2015            }
2016            if key.use_toon_tex {
2017                name.push_str("_tt");
2018            }
2019            if key.use_normal_tex {
2020                name.push_str("_nt");
2021            }
2022            if key.use_mul_vertex_color {
2023                name.push_str("_vc");
2024            }
2025            if key.use_mrbd {
2026                name.push_str("_mrbd");
2027            }
2028            if key.use_rgb {
2029                name.push_str("_rgb");
2030            }
2031            name.push_str(match key.lighting_type {
2032                MeshLightingType::Lambert => "_lmbt",
2033                MeshLightingType::BlinnPhong => "_blph",
2034                MeshLightingType::PerPixelBlinnPhong => "_ppbp",
2035                MeshLightingType::PerPixelHalfLambert => "_pphl",
2036                MeshLightingType::Toon => "_toon",
2037                MeshLightingType::FixedFunction => "_ffp",
2038                MeshLightingType::PerPixelFixedFunction => "_ppfp",
2039                MeshLightingType::Bump => "_bump",
2040                MeshLightingType::Parallax => "_para",
2041                MeshLightingType::None => "_nolt",
2042            });
2043            name.push_str(match key.shading_type {
2044                MeshShadingType::DepthBuffer => "_dpbs",
2045                MeshShadingType::None => "_nost",
2046            });
2047            if key.shader_option & MESH_SHADER_OPTION_RIM_LIGHT != 0 {
2048                name.push_str("_rmlt");
2049            }
2050            name.push_str(".fx");
2051            name
2052        }
2053    }
2054}
2055
2056fn build_mesh_primitive_runtime_desc_from_material(
2057    material: &MeshMaterial,
2058    has_texture: bool,
2059    skinned: bool,
2060    vertex_count: u32,
2061    bone_palette_len: u32,
2062) -> MeshPrimitiveRuntimeDesc {
2063    let material_key = mesh_runtime_material_key(material, has_texture, skinned);
2064    let effect_profile = if skinned {
2065        MeshEffectProfile::SkinnedMesh
2066    } else {
2067        MeshEffectProfile::Mesh
2068    };
2069    MeshPrimitiveRuntimeDesc {
2070        effect_profile,
2071        effect_key: mesh_effect_filename_from_runtime_key(effect_profile, material_key),
2072        technique_name: String::from("tech"),
2073        shadow_effect_key: mesh_effect_filename_from_runtime_key(
2074            MeshEffectProfile::ShadowMap,
2075            MeshRuntimeMaterialKey {
2076                skinned,
2077                shadow_map_enable: true,
2078                ..MeshRuntimeMaterialKey::default()
2079            },
2080        ),
2081        shadow_technique_name: String::from("tech"),
2082        use_mesh_texture_slot: material_key.use_mesh_tex,
2083        use_normal_texture_slot: material_key.use_normal_tex,
2084        use_toon_texture_slot: material_key.use_toon_tex,
2085        use_shadow_texture_slot: material_key.use_shadow_tex,
2086        material_key,
2087        vertex_stride_bytes: std::mem::size_of::<MeshTriVertex>() as u32,
2088        vertex_count,
2089        bone_palette_len,
2090    }
2091}
2092
2093fn build_mesh_primitive_runtime_desc(prim: &MeshPrimitive) -> MeshPrimitiveRuntimeDesc {
2094    build_mesh_primitive_runtime_desc_from_material(
2095        &prim.material,
2096        prim.texture_path.is_some(),
2097        !prim.bones.is_empty(),
2098        prim.vertices.len() as u32,
2099        prim.bones.len() as u32,
2100    )
2101}
2102
2103fn apply_effect_filename(material: &mut MeshMaterial, effect_name: &str) {
2104    let lower = effect_name.to_ascii_lowercase();
2105    material.effect_filename = Some(effect_name.to_string());
2106    if lower.contains("_lmbt") {
2107        material.lighting_type = MeshLightingType::Lambert;
2108    } else if lower.contains("_blph") {
2109        material.lighting_type = MeshLightingType::BlinnPhong;
2110    } else if lower.contains("_ppbp") {
2111        material.lighting_type = MeshLightingType::PerPixelBlinnPhong;
2112    } else if lower.contains("_pphl") {
2113        material.lighting_type = MeshLightingType::PerPixelHalfLambert;
2114    } else if lower.contains("_toon") {
2115        material.lighting_type = MeshLightingType::Toon;
2116    } else if lower.contains("_ffp") {
2117        material.lighting_type = MeshLightingType::FixedFunction;
2118    } else if lower.contains("_ppfp") {
2119        material.lighting_type = MeshLightingType::PerPixelFixedFunction;
2120    } else if lower.contains("_bump") {
2121        material.lighting_type = MeshLightingType::Bump;
2122    } else if lower.contains("_para") {
2123        material.lighting_type = MeshLightingType::Parallax;
2124    } else if lower.contains("_nolt") {
2125        material.lighting_type = MeshLightingType::None;
2126    }
2127    if lower.contains("_dpbs") {
2128        material.shading_type = MeshShadingType::DepthBuffer;
2129    } else if lower.contains("_nost") {
2130        material.shading_type = MeshShadingType::None;
2131    }
2132    if lower.contains("_rmlt") {
2133        material.shader_option |= MESH_SHADER_OPTION_RIM_LIGHT;
2134    }
2135}
2136
2137fn parse_effect_bool(value_str: Option<&str>, values: &[f32]) -> Option<bool> {
2138    if let Some(v) = values.first() {
2139        return Some(*v != 0.0);
2140    }
2141    value_str.map(|s| {
2142        !matches!(
2143            s.to_ascii_lowercase().as_str(),
2144            "0" | "false" | "off" | "none"
2145        )
2146    })
2147}
2148
2149fn parse_effect_int_list(values: &[f32]) -> Vec<i32> {
2150    values.iter().map(|v| *v as i32).collect()
2151}
2152
2153fn assign_effect_param(
2154    material: &mut MeshMaterial,
2155    mesh_path: &Path,
2156    key: &str,
2157    value_str: Option<&str>,
2158    values: &[f32],
2159) {
2160    let key = key.to_ascii_lowercase();
2161    match key.as_str() {
2162        "lighting_type" | "mp__lighting_type" => {
2163            if let Some(v) = values.first() {
2164                material.lighting_type = match *v as i32 {
2165                    1 => MeshLightingType::Lambert,
2166                    2 => MeshLightingType::BlinnPhong,
2167                    3 => MeshLightingType::PerPixelBlinnPhong,
2168                    4 => MeshLightingType::PerPixelHalfLambert,
2169                    5 => MeshLightingType::Toon,
2170                    6 => MeshLightingType::FixedFunction,
2171                    7 => MeshLightingType::PerPixelFixedFunction,
2172                    8 => MeshLightingType::Bump,
2173                    9 => MeshLightingType::Parallax,
2174                    _ => MeshLightingType::None,
2175                };
2176            }
2177        }
2178        "shading_type" | "mp__shading_type" => {
2179            if let Some(v) = values.first() {
2180                material.shading_type = if (*v as i32) == 1 {
2181                    MeshShadingType::DepthBuffer
2182                } else {
2183                    MeshShadingType::None
2184                };
2185            }
2186        }
2187        "shader_option" => {
2188            if let Some(v) = values.first() {
2189                material.shader_option = *v as u32;
2190            }
2191        }
2192        "use_mesh_tex" | "g_usemeshtex" => {
2193            if let Some(v) = parse_effect_bool(value_str, values) {
2194                material.use_mesh_tex = v;
2195            }
2196        }
2197        "use_mrbd" | "g_usemrbd" => {
2198            if let Some(v) = parse_effect_bool(value_str, values) {
2199                material.use_mrbd = v;
2200            }
2201        }
2202        "mrbd" | "g_mrbd" => {
2203            if !values.is_empty() {
2204                material.mrbd = [
2205                    values.first().copied().unwrap_or(0.0),
2206                    values.get(1).copied().unwrap_or(0.0),
2207                    values.get(2).copied().unwrap_or(0.0),
2208                    values.get(3).copied().unwrap_or(0.0),
2209                ];
2210                material.use_mrbd = values.iter().any(|v| v.abs() > 1e-6);
2211            }
2212        }
2213        "use_rgb" | "g_usergb" => {
2214            if let Some(v) = parse_effect_bool(value_str, values) {
2215                material.use_rgb = v;
2216            }
2217        }
2218        "rgb_rate" | "g_rgbrate" => {
2219            if !values.is_empty() {
2220                material.rgb_rate = [
2221                    values.first().copied().unwrap_or(0.0),
2222                    values.get(1).copied().unwrap_or(0.0),
2223                    values.get(2).copied().unwrap_or(0.0),
2224                    values.get(3).copied().unwrap_or(0.0),
2225                ];
2226                material.use_rgb = true;
2227            }
2228        }
2229        "add_rgb" | "g_addrgb" => {
2230            if !values.is_empty() {
2231                material.add_rgb = [
2232                    values.first().copied().unwrap_or(0.0),
2233                    values.get(1).copied().unwrap_or(0.0),
2234                    values.get(2).copied().unwrap_or(0.0),
2235                    values.get(3).copied().unwrap_or(0.0),
2236                ];
2237                material.use_rgb = true;
2238            }
2239        }
2240        "use_mul_vertex_color" | "g_usemulvertexcolor" => {
2241            if let Some(v) = parse_effect_bool(value_str, values) {
2242                material.use_mul_vertex_color = v;
2243            }
2244        }
2245        "mul_vertex_color_rate" | "g_mulvertexcolorrate" | "g_vertexcolorrate" => {
2246            if let Some(v) = values.first() {
2247                material.mul_vertex_color_rate = (*v).clamp(0.0, 8.0);
2248                material.use_mul_vertex_color =
2249                    material.use_mul_vertex_color || (*v - 1.0).abs() > 1e-6;
2250            }
2251        }
2252        "depth_buffer_shadow_bias" | "g_depthbuffershadowbias" | "g_dbsbias" => {
2253            if let Some(v) = values.first() {
2254                material.depth_buffer_shadow_bias = *v;
2255            }
2256        }
2257        "directional_light_id_list" | "directionallightidlist" | "g_directionallightidlist" => {
2258            let ids = parse_effect_int_list(values);
2259            if !ids.is_empty() {
2260                material.directional_light_ids = ids;
2261            }
2262        }
2263        "point_light_id_list" | "pointlightidlist" | "g_pointlightidlist" => {
2264            let ids = parse_effect_int_list(values);
2265            if !ids.is_empty() {
2266                material.point_light_ids = ids;
2267            }
2268        }
2269        "spot_light_id_list" | "spotlightidlist" | "g_spotlightidlist" => {
2270            let ids = parse_effect_int_list(values);
2271            if !ids.is_empty() {
2272                material.spot_light_ids = ids;
2273            }
2274        }
2275        "g_rimlightcolor" | "rim_light_color" => {
2276            if values.len() >= 3 {
2277                material.rim_light_color = [
2278                    values[0],
2279                    values[1],
2280                    values[2],
2281                    values.get(3).copied().unwrap_or(1.0),
2282                ];
2283            }
2284        }
2285        "g_rimlightpower" | "rim_light_power" => {
2286            if let Some(v) = values.first() {
2287                material.rim_light_power = (*v).max(0.0);
2288                if material.rim_light_power > 0.0 {
2289                    material.shader_option |= MESH_SHADER_OPTION_RIM_LIGHT;
2290                }
2291            }
2292        }
2293        "g_parallaxmaxheight" | "parallax_max_height" => {
2294            if let Some(v) = values.first() {
2295                material.parallax_max_height = (*v).max(0.0);
2296                if material.parallax_max_height > 0.0
2297                    && material.lighting_type == MeshLightingType::None
2298                {
2299                    material.lighting_type = MeshLightingType::Parallax;
2300                }
2301            }
2302        }
2303        "g_materialdiffuse" | "material_diffuse" => {
2304            if values.len() >= 3 {
2305                material.diffuse = [
2306                    values[0],
2307                    values[1],
2308                    values[2],
2309                    values.get(3).copied().unwrap_or(material.diffuse[3]),
2310                ];
2311            }
2312        }
2313        "g_materialambient" | "material_ambient" => {
2314            if values.len() >= 3 {
2315                material.ambient = [
2316                    values[0],
2317                    values[1],
2318                    values[2],
2319                    values.get(3).copied().unwrap_or(material.ambient[3]),
2320                ];
2321            }
2322        }
2323        "g_materialspecular" | "material_specular" => {
2324            if values.len() >= 3 {
2325                material.specular = [
2326                    values[0],
2327                    values[1],
2328                    values[2],
2329                    values.get(3).copied().unwrap_or(material.specular[3]),
2330                ];
2331            }
2332        }
2333        "g_materialemissive" | "material_emissive" => {
2334            if values.len() >= 3 {
2335                material.emissive = [
2336                    values[0],
2337                    values[1],
2338                    values[2],
2339                    values.get(3).copied().unwrap_or(material.emissive[3]),
2340                ];
2341            }
2342        }
2343        "g_materialspecularpower" | "material_specular_power" | "material_power" => {
2344            if let Some(v) = values.first() {
2345                material.power = (*v).max(1.0);
2346            }
2347        }
2348        "prerendertype" | "pre_render_type" | "g_prerendertype" | "shadow_map_enable"
2349        | "shadowmapenable" => {
2350            if let Some(v) = values.first() {
2351                material.shadow_map_enable = ((*v as i32) & 1) != 0;
2352            }
2353        }
2354        "alphatestenable" | "alpha_test_enable" | "g_alphatestenable" => {
2355            if let Some(v) = parse_effect_bool(value_str, values) {
2356                material.alpha_test_enable = v;
2357            }
2358        }
2359        "alpharef" | "alpha_ref" | "g_alpharef" => {
2360            if let Some(v) = values.first() {
2361                material.alpha_ref = (*v).clamp(0.0, 1.0);
2362                if material.alpha_ref > 0.0 {
2363                    material.alpha_test_enable = true;
2364                }
2365            }
2366        }
2367        "cullmode" | "cull_mode" | "twosided" | "double_sided" => {
2368            if let Some(v) = values.first() {
2369                let vi = *v as i32;
2370                material.cull_disable = vi == 0 || vi == 1;
2371            } else if let Some(s) = value_str {
2372                let s = s.to_ascii_lowercase();
2373                material.cull_disable = matches!(
2374                    s.as_str(),
2375                    "none" | "off" | "0" | "1" | "two" | "twosided" | "double" | "double_sided"
2376                );
2377            }
2378        }
2379        "normaltexture" | "normal_tex" | "g_normaltexture" | "g_normaltex" | "normalmap"
2380        | "normal_map" => {
2381            if let Some(s) = value_str {
2382                material.normal_texture_path = resolve_texture_path(mesh_path, s);
2383                if material.normal_texture_path.is_some()
2384                    && material.lighting_type == MeshLightingType::None
2385                {
2386                    material.lighting_type = MeshLightingType::Bump;
2387                }
2388            }
2389        }
2390        "toontexture" | "toon_tex" | "g_toontexture" | "g_toontex" | "toonmap" | "toon_map" => {
2391            if let Some(s) = value_str {
2392                material.toon_texture_path = resolve_texture_path(mesh_path, s);
2393                if material.toon_texture_path.is_some()
2394                    && material.lighting_type == MeshLightingType::None
2395                {
2396                    material.lighting_type = MeshLightingType::Toon;
2397                }
2398            }
2399        }
2400        _ => {}
2401    }
2402}
2403
2404fn parse_x_effect_instance(
2405    cur: &mut Cursor,
2406    mesh_path: &Path,
2407    material: &mut MeshMaterial,
2408) -> Result<()> {
2409    cur.expect_block_start()?;
2410    if let Ok(effect_name) = cur.next_string() {
2411        apply_effect_filename(material, &effect_name);
2412    }
2413    while let Some(tok) = cur.peek().cloned() {
2414        match tok {
2415            Tok::Sym('}') => {
2416                cur.next();
2417                break;
2418            }
2419            Tok::Ident(_) => {
2420                let block_name = cur.next_string()?;
2421                if !cur.consume_sym('{') {
2422                    continue;
2423                }
2424                let mut strings = Vec::<String>::new();
2425                let mut values = Vec::<f32>::new();
2426                let mut depth = 1usize;
2427                while let Some(tok2) = cur.next() {
2428                    match tok2 {
2429                        Tok::Sym('{') => depth += 1,
2430                        Tok::Sym('}') => {
2431                            depth -= 1;
2432                            if depth == 0 {
2433                                break;
2434                            }
2435                        }
2436                        Tok::Str(s) | Tok::Ident(s) => strings.push(s),
2437                        Tok::Number(v) => values.push(v),
2438                        Tok::Sym(_) => {}
2439                    }
2440                }
2441                let value_str = strings
2442                    .iter()
2443                    .find(|s| s.contains('.') || s.contains('/') || s.contains('\\'))
2444                    .map(|s| s.as_str())
2445                    .or_else(|| strings.get(1).map(|s| s.as_str()));
2446                assign_effect_param(material, mesh_path, &block_name, value_str, &values);
2447                if let Some(first) = strings.first() {
2448                    assign_effect_param(
2449                        material,
2450                        mesh_path,
2451                        first,
2452                        strings.get(1).map(|s| s.as_str()),
2453                        &values,
2454                    );
2455                }
2456            }
2457            _ => {
2458                cur.next();
2459            }
2460        }
2461    }
2462    Ok(())
2463}
2464
2465fn triangle_tangent_binormal(
2466    a: &MeshTriVertex,
2467    b: &MeshTriVertex,
2468    c: &MeshTriVertex,
2469) -> ([f32; 3], [f32; 3]) {
2470    let e1 = [
2471        b.pos[0] - a.pos[0],
2472        b.pos[1] - a.pos[1],
2473        b.pos[2] - a.pos[2],
2474    ];
2475    let e2 = [
2476        c.pos[0] - a.pos[0],
2477        c.pos[1] - a.pos[1],
2478        c.pos[2] - a.pos[2],
2479    ];
2480    let duv1 = [b.uv[0] - a.uv[0], b.uv[1] - a.uv[1]];
2481    let duv2 = [c.uv[0] - a.uv[0], c.uv[1] - a.uv[1]];
2482    let denom = duv1[0] * duv2[1] - duv1[1] * duv2[0];
2483    if denom.abs() <= 1e-8 {
2484        let n = if has_normal(a.normal) {
2485            a.normal
2486        } else {
2487            triangle_normal(a.pos, b.pos, c.pos)
2488        };
2489        let up = if n[1].abs() < 0.999 {
2490            [0.0, 1.0, 0.0]
2491        } else {
2492            [1.0, 0.0, 0.0]
2493        };
2494        let t = normalize3(cross3(up, n));
2495        let b = normalize3(cross3(n, t));
2496        return (t, b);
2497    }
2498    let inv = 1.0 / denom;
2499    let tangent = normalize3([
2500        inv * (duv2[1] * e1[0] - duv1[1] * e2[0]),
2501        inv * (duv2[1] * e1[1] - duv1[1] * e2[1]),
2502        inv * (duv2[1] * e1[2] - duv1[1] * e2[2]),
2503    ]);
2504    let binormal = normalize3([
2505        inv * (-duv2[0] * e1[0] + duv1[0] * e2[0]),
2506        inv * (-duv2[0] * e1[1] + duv1[0] * e2[1]),
2507        inv * (-duv2[0] * e1[2] + duv1[0] * e2[2]),
2508    ]);
2509    (tangent, binormal)
2510}
2511
2512fn apply_tangent_space(vertices: &mut [MeshTriVertex]) {
2513    for tri in vertices.chunks_mut(3) {
2514        if tri.len() != 3 {
2515            continue;
2516        }
2517        let (t, b) = triangle_tangent_binormal(&tri[0], &tri[1], &tri[2]);
2518        for v in tri {
2519            v.tangent = t;
2520            v.binormal = b;
2521        }
2522    }
2523}
2524
2525fn has_normal(n: [f32; 3]) -> bool {
2526    n[0].abs() > 1e-6 || n[1].abs() > 1e-6 || n[2].abs() > 1e-6
2527}
2528
2529fn triangle_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
2530    let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
2531    let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
2532    normalize3([
2533        ab[1] * ac[2] - ab[2] * ac[1],
2534        ab[2] * ac[0] - ab[0] * ac[2],
2535        ab[0] * ac[1] - ab[1] * ac[0],
2536    ])
2537}
2538
2539fn obj_index<T>(items: &[T], raw: isize) -> Option<&T> {
2540    if raw == 0 {
2541        return None;
2542    }
2543    if raw > 0 {
2544        items.get((raw - 1) as usize)
2545    } else {
2546        let idx = items.len() as isize + raw;
2547        if idx < 0 {
2548            None
2549        } else {
2550            items.get(idx as usize)
2551        }
2552    }
2553}
2554
2555#[derive(Debug, Clone)]
2556struct ParsedXMaterial {
2557    name: Option<String>,
2558    material: MeshMaterial,
2559    texture_path: Option<PathBuf>,
2560}
2561
2562#[derive(Debug, Clone)]
2563struct XFace {
2564    indices: Vec<usize>,
2565    material_index: usize,
2566}
2567
2568#[derive(Debug, Clone)]
2569struct ImportedXBoneBinding {
2570    frame_name: String,
2571    offset_matrix: Mat4,
2572}
2573
2574#[derive(Debug, Clone)]
2575struct ImportedXPrimitive {
2576    frame_node: usize,
2577    texture_path: Option<PathBuf>,
2578    material: MeshMaterial,
2579    vertices: Vec<MeshTriVertex>,
2580    bones: Vec<ImportedXBoneBinding>,
2581}
2582
2583#[derive(Debug, Clone)]
2584struct ImportedXFrame {
2585    name: String,
2586    parent: Option<usize>,
2587    base_local: Mat4,
2588}
2589
2590#[derive(Debug, Clone, Default)]
2591struct ImportedXScene {
2592    frames: Vec<ImportedXFrame>,
2593    primitives: Vec<ImportedXPrimitive>,
2594    animations: Vec<AnimationClip>,
2595    ticks_per_second: f32,
2596}
2597
2598
2599fn import_x_scene_bytes_shion(bytes: &[u8], path: &Path) -> Result<ImportedXScene> {
2600    let file = shion_xfile::parse_x(bytes)
2601        .with_context(|| format!("parse DirectX .x file with Shion parser: {:?}", path))?;
2602    let scene = shion_xfile::Scene::from_xfile(&file)
2603        .with_context(|| format!("lift DirectX .x semantic scene: {:?}", path))?;
2604    import_x_scene_from_shion_scene(&scene, path)
2605}
2606
2607fn import_x_scene_from_shion_scene(
2608    scene: &shion_xfile::Scene,
2609    path: &Path,
2610) -> Result<ImportedXScene> {
2611    let loose_materials = shion_loose_material_lookup(scene);
2612    let mut out = ImportedXScene {
2613        frames: vec![ImportedXFrame {
2614            name: "__root__".to_string(),
2615            parent: None,
2616            base_local: Mat4::identity(),
2617        }],
2618        primitives: Vec::new(),
2619        animations: shion_animation_clips(scene),
2620        ticks_per_second: scene.anim_ticks_per_second.unwrap_or(4800).max(1) as f32,
2621    };
2622
2623    for frame in &scene.frames {
2624        import_shion_frame(frame, path, &loose_materials, &mut out, 0)?;
2625    }
2626    for mesh in &scene.loose_meshes {
2627        out.primitives.extend(shion_mesh_primitives(
2628            mesh,
2629            path,
2630            0,
2631            &loose_materials,
2632        )?);
2633    }
2634    Ok(out)
2635}
2636
2637fn shion_loose_material_lookup(
2638    scene: &shion_xfile::Scene,
2639) -> HashMap<String, shion_xfile::Material> {
2640    let mut out = HashMap::new();
2641    for material in &scene.loose_materials {
2642        if let Some(name) = &material.name {
2643            out.insert(name.clone(), material.clone());
2644        }
2645    }
2646    out
2647}
2648
2649fn import_shion_frame(
2650    frame: &shion_xfile::Frame,
2651    path: &Path,
2652    loose_materials: &HashMap<String, shion_xfile::Material>,
2653    scene: &mut ImportedXScene,
2654    parent_frame: usize,
2655) -> Result<usize> {
2656    let frame_idx = scene.frames.len();
2657    scene.frames.push(ImportedXFrame {
2658        name: frame
2659            .name
2660            .clone()
2661            .unwrap_or_else(|| format!("frame_{}", frame_idx)),
2662        parent: Some(parent_frame),
2663        base_local: frame
2664            .transform
2665            .map(Mat4::from_x_values)
2666            .unwrap_or_else(Mat4::identity),
2667    });
2668
2669    for mesh in &frame.meshes {
2670        scene.primitives.extend(shion_mesh_primitives(
2671            mesh,
2672            path,
2673            frame_idx,
2674            loose_materials,
2675        )?);
2676    }
2677    for child in &frame.child_frames {
2678        import_shion_frame(child, path, loose_materials, scene, frame_idx)?;
2679    }
2680    Ok(frame_idx)
2681}
2682
2683fn shion_mesh_primitives(
2684    mesh: &shion_xfile::Mesh,
2685    path: &Path,
2686    frame_index: usize,
2687    loose_materials: &HashMap<String, shion_xfile::Material>,
2688) -> Result<Vec<ImportedXPrimitive>> {
2689    let material_slots = shion_material_slots(mesh, path, loose_materials);
2690    let face_material_indices = mesh
2691        .material_list
2692        .as_ref()
2693        .map(|list| list.face_indexes.as_slice())
2694        .unwrap_or(&[]);
2695    let texcoords = mesh.texcoords.as_deref().unwrap_or(&[]);
2696    let vertex_colors = shion_vertex_color_lookup(mesh);
2697    let (influences, bones) = shion_skin_bindings(mesh);
2698    let mut groups: HashMap<usize, Vec<MeshTriVertex>> = HashMap::new();
2699
2700    for (face_idx, face) in mesh.faces.iter().enumerate() {
2701        if face.len() < 3 {
2702            continue;
2703        }
2704        let material_index = face_material_indices
2705            .get(face_idx)
2706            .copied()
2707            .unwrap_or(0) as usize;
2708        for tri in 1..face.len() - 1 {
2709            let face_corners = [0usize, tri, tri + 1];
2710            let vertex_indices = [
2711                face[face_corners[0]] as usize,
2712                face[face_corners[1]] as usize,
2713                face[face_corners[2]] as usize,
2714            ];
2715            let tri_n = triangle_normal(
2716                mesh.vertices
2717                    .get(vertex_indices[0])
2718                    .copied()
2719                    .unwrap_or([0.0, 0.0, 0.0]),
2720                mesh.vertices
2721                    .get(vertex_indices[1])
2722                    .copied()
2723                    .unwrap_or([0.0, 0.0, 0.0]),
2724                mesh.vertices
2725                    .get(vertex_indices[2])
2726                    .copied()
2727                    .unwrap_or([0.0, 0.0, 0.0]),
2728            );
2729            let group = groups.entry(material_index).or_default();
2730            for (&vertex_index, &corner_index) in vertex_indices.iter().zip(face_corners.iter()) {
2731                let pos = mesh
2732                    .vertices
2733                    .get(vertex_index)
2734                    .copied()
2735                    .unwrap_or([0.0, 0.0, 0.0]);
2736                let uv = texcoords.get(vertex_index).copied().unwrap_or([0.0, 0.0]);
2737                let normal = shion_face_corner_normal(mesh, face_idx, corner_index)
2738                    .or_else(|| shion_vertex_normal(mesh, vertex_index))
2739                    .unwrap_or(tri_n);
2740                let color = vertex_colors
2741                    .get(&vertex_index)
2742                    .copied()
2743                    .unwrap_or([1.0, 1.0, 1.0, 1.0]);
2744                let (bone_indices, bone_weights) = packed_influences(influences.get(&vertex_index));
2745                group.push(MeshTriVertex {
2746                    pos,
2747                    uv,
2748                    normal,
2749                    tangent: [0.0, 0.0, 0.0],
2750                    binormal: [0.0, 0.0, 0.0],
2751                    color,
2752                    bone_indices,
2753                    bone_weights,
2754                });
2755            }
2756        }
2757    }
2758
2759    let mut out = Vec::new();
2760    for (material_index, vertices) in groups {
2761        if vertices.is_empty() {
2762            continue;
2763        }
2764        let slot = material_slots
2765            .get(material_index)
2766            .cloned()
2767            .unwrap_or_else(|| ParsedXMaterial {
2768                name: None,
2769                material: default_mesh_material(),
2770                texture_path: None,
2771            });
2772        let mut vertices = vertices;
2773        apply_tangent_space(&mut vertices);
2774        out.push(ImportedXPrimitive {
2775            frame_node: frame_index,
2776            texture_path: slot.texture_path.clone(),
2777            material: slot.material,
2778            vertices,
2779            bones: bones.clone(),
2780        });
2781    }
2782    Ok(out)
2783}
2784
2785fn shion_material_slots(
2786    mesh: &shion_xfile::Mesh,
2787    path: &Path,
2788    loose_materials: &HashMap<String, shion_xfile::Material>,
2789) -> Vec<ParsedXMaterial> {
2790    let mut slots = Vec::new();
2791    if let Some(list) = &mesh.material_list {
2792        for material in &list.materials {
2793            slots.push(shion_material_to_parsed(material, path));
2794        }
2795        for reference in &list.material_references {
2796            if let Some(name) = &reference.name {
2797                if let Some(material) = loose_materials.get(name) {
2798                    slots.push(shion_material_to_parsed(material, path));
2799                    continue;
2800                }
2801            }
2802            slots.push(ParsedXMaterial {
2803                name: reference.name.clone(),
2804                material: default_mesh_material(),
2805                texture_path: None,
2806            });
2807        }
2808        while slots.len() < list.material_count as usize {
2809            slots.push(ParsedXMaterial {
2810                name: None,
2811                material: default_mesh_material(),
2812                texture_path: None,
2813            });
2814        }
2815    }
2816    if slots.is_empty() {
2817        slots.push(ParsedXMaterial {
2818            name: None,
2819            material: default_mesh_material(),
2820            texture_path: None,
2821        });
2822    }
2823
2824    for effect in &mesh.effect_instances {
2825        for slot in &mut slots {
2826            shion_apply_effect_instance(&mut slot.material, path, effect);
2827        }
2828    }
2829    slots
2830}
2831
2832fn shion_material_to_parsed(
2833    material: &shion_xfile::Material,
2834    path: &Path,
2835) -> ParsedXMaterial {
2836    let mut out = default_mesh_material();
2837    out.diffuse = material.face_color;
2838    out.ambient = material.face_color;
2839    out.specular = [
2840        material.specular_color[0],
2841        material.specular_color[1],
2842        material.specular_color[2],
2843        1.0,
2844    ];
2845    out.emissive = [
2846        material.emissive_color[0],
2847        material.emissive_color[1],
2848        material.emissive_color[2],
2849        1.0,
2850    ];
2851    out.power = material.power.max(1.0);
2852
2853    let mut texture_path = material
2854        .texture_filenames
2855        .iter()
2856        .find_map(|tex| resolve_texture_path(path, tex));
2857    for effect in &material.effect_instances {
2858        shion_apply_effect_instance(&mut out, path, effect);
2859    }
2860    if texture_path.is_none() {
2861        texture_path = material.effect_instances.iter().find_map(|effect| {
2862            effect
2863                .strings
2864                .iter()
2865                .find_map(|param| resolve_texture_path(path, &param.value))
2866                .or_else(|| {
2867                    effect
2868                        .legacy_strings
2869                        .iter()
2870                        .find_map(|param| resolve_texture_path(path, &param.value))
2871                })
2872        });
2873    }
2874
2875    ParsedXMaterial {
2876        name: material.name.clone(),
2877        material: out,
2878        texture_path,
2879    }
2880}
2881
2882fn shion_apply_effect_instance(
2883    material: &mut MeshMaterial,
2884    path: &Path,
2885    effect: &shion_xfile::EffectInstance,
2886) {
2887    if !effect.effect_filename.trim().is_empty() {
2888        apply_effect_filename(material, &effect.effect_filename);
2889    }
2890    for param in &effect.strings {
2891        assign_effect_param(material, path, &param.param_name, Some(&param.value), &[]);
2892    }
2893    for param in &effect.dwords {
2894        assign_effect_param(
2895            material,
2896            path,
2897            &param.param_name,
2898            None,
2899            &[param.value as f32],
2900        );
2901    }
2902    for param in &effect.floats {
2903        assign_effect_param(material, path, &param.param_name, None, &param.values);
2904    }
2905    for param in &effect.legacy_strings {
2906        assign_effect_param(material, path, "effect_string", Some(&param.value), &[]);
2907    }
2908    for param in &effect.legacy_dwords {
2909        assign_effect_param(material, path, "effect_dword", None, &[param.value as f32]);
2910    }
2911    for param in &effect.legacy_floats {
2912        assign_effect_param(material, path, "effect_floats", None, &param.values);
2913    }
2914}
2915
2916fn shion_vertex_color_lookup(mesh: &shion_xfile::Mesh) -> HashMap<usize, [f32; 4]> {
2917    let mut out = HashMap::new();
2918    if let Some(colors) = &mesh.vertex_colors {
2919        for color in colors {
2920            out.insert(color.index as usize, color.rgba);
2921        }
2922    }
2923    out
2924}
2925
2926fn shion_face_corner_normal(
2927    mesh: &shion_xfile::Mesh,
2928    face_index: usize,
2929    corner_index: usize,
2930) -> Option<[f32; 3]> {
2931    let normals = mesh.normals.as_ref()?;
2932    let normal_index = (*normals.face_normals.get(face_index)?.get(corner_index)?) as usize;
2933    normals.normals.get(normal_index).copied()
2934}
2935
2936fn shion_vertex_normal(mesh: &shion_xfile::Mesh, vertex_index: usize) -> Option<[f32; 3]> {
2937    let normals = mesh.normals.as_ref()?;
2938    normals.normals.get(vertex_index).copied()
2939}
2940
2941fn shion_skin_bindings(
2942    mesh: &shion_xfile::Mesh,
2943) -> (
2944    HashMap<usize, Vec<(usize, f32)>>,
2945    Vec<ImportedXBoneBinding>,
2946) {
2947    let mut influences: HashMap<usize, Vec<(usize, f32)>> = HashMap::new();
2948    let mut bones = Vec::new();
2949    for skin in &mesh.skin_weights {
2950        let bone_index = bones.len();
2951        bones.push(ImportedXBoneBinding {
2952            frame_name: skin.transform_node_name.clone(),
2953            offset_matrix: Mat4::from_x_values(skin.matrix_offset),
2954        });
2955        for (&vertex_index, &weight) in skin.vertex_indices.iter().zip(skin.weights.iter()) {
2956            influences
2957                .entry(vertex_index as usize)
2958                .or_default()
2959                .push((bone_index, weight));
2960        }
2961    }
2962    (influences, bones)
2963}
2964
2965fn shion_animation_clips(scene: &shion_xfile::Scene) -> Vec<AnimationClip> {
2966    let mut clips = Vec::new();
2967    for (set_index, set) in scene.animation_sets.iter().enumerate() {
2968        let mut clip = AnimationClip {
2969            name: set
2970                .name
2971                .clone()
2972                .unwrap_or_else(|| format!("anim_{}", set_index)),
2973            open_closed: true,
2974            ..Default::default()
2975        };
2976        for animation in &set.animations {
2977            if let Some(options) = &animation.options {
2978                clip.open_closed = options.open_closed != 0;
2979            }
2980            let Some(target_name) = animation.target_name.as_deref() else {
2981                continue;
2982            };
2983            let track = clip.tracks.entry(target_name.to_string()).or_default();
2984            for block in &animation.keys {
2985                for key in &block.keys {
2986                    let time = key.time as i64;
2987                    clip.max_time = clip.max_time.max(time);
2988                    match block.key_type {
2989                        0 => {
2990                            if key.values.len() >= 4 {
2991                                track.rotation_keys.push(QuatKey {
2992                                    time,
2993                                    value: Quat {
2994                                        x: key.values[1],
2995                                        y: key.values[2],
2996                                        z: key.values[3],
2997                                        w: key.values[0],
2998                                    }
2999                                    .normalize(),
3000                                });
3001                            }
3002                        }
3003                        1 => {
3004                            track.scale_keys.push(Vec3Key {
3005                                time,
3006                                value: [
3007                                    key.values.get(0).copied().unwrap_or(1.0),
3008                                    key.values.get(1).copied().unwrap_or(1.0),
3009                                    key.values.get(2).copied().unwrap_or(1.0),
3010                                ],
3011                            });
3012                        }
3013                        2 => {
3014                            track.position_keys.push(Vec3Key {
3015                                time,
3016                                value: [
3017                                    key.values.get(0).copied().unwrap_or(0.0),
3018                                    key.values.get(1).copied().unwrap_or(0.0),
3019                                    key.values.get(2).copied().unwrap_or(0.0),
3020                                ],
3021                            });
3022                        }
3023                        4 => {
3024                            let mut m = [0.0f32; 16];
3025                            for (dst, src) in m.iter_mut().zip(key.values.iter().copied()) {
3026                                *dst = src;
3027                            }
3028                            track.matrix_keys.push(MatKey {
3029                                time,
3030                                value: Mat4::from_x_values(m),
3031                            });
3032                        }
3033                        _ => {}
3034                    }
3035                }
3036            }
3037        }
3038        if !clip.tracks.is_empty() {
3039            clips.push(clip);
3040        }
3041    }
3042    clips
3043}
3044
3045impl ImportedXScene {
3046    fn into_mesh_asset(self, path: &Path) -> Result<MeshAsset> {
3047        let mut children_map: Vec<Vec<usize>> = vec![Vec::new(); self.frames.len().max(1)];
3048        for (idx, frame) in self.frames.iter().enumerate() {
3049            if let Some(parent) = frame.parent {
3050                if parent < children_map.len() {
3051                    children_map[parent].push(idx);
3052                }
3053            }
3054        }
3055        let frames = self
3056            .frames
3057            .iter()
3058            .enumerate()
3059            .map(|(idx, frame)| FrameNode {
3060                name: frame.name.clone(),
3061                base_local: frame.base_local,
3062                children: children_map.get(idx).cloned().unwrap_or_default(),
3063            })
3064            .collect::<Vec<_>>();
3065        let frame_lookup: HashMap<String, usize> = frames
3066            .iter()
3067            .enumerate()
3068            .map(|(i, f)| (f.name.clone(), i))
3069            .collect();
3070        let primitives = self
3071            .primitives
3072            .into_iter()
3073            .map(|prim| MeshPrimitive {
3074                frame_index: prim.frame_node,
3075                texture_path: prim.texture_path,
3076                material: prim.material,
3077                runtime_desc: MeshPrimitiveRuntimeDesc::default(),
3078                vertices: prim.vertices,
3079                bones: prim
3080                    .bones
3081                    .into_iter()
3082                    .map(|bone| BoneBinding {
3083                        frame_index: frame_lookup.get(&bone.frame_name).copied().unwrap_or(0),
3084                        frame_name: bone.frame_name,
3085                        offset_matrix: bone.offset_matrix,
3086                    })
3087                    .collect(),
3088            })
3089            .collect::<Vec<_>>();
3090        let texture_path = primitives.iter().find_map(|p| p.texture_path.clone());
3091        let mut asset = MeshAsset {
3092            source_path: path.to_path_buf(),
3093            texture_path,
3094            vertices: Vec::new(),
3095            bone_count: primitives.iter().map(|p| p.bones.len()).max().unwrap_or(0),
3096            ticks_per_second: self.ticks_per_second.max(1.0),
3097            primitives,
3098            frames,
3099            animations: self.animations,
3100        };
3101        asset.vertices = asset.sample_vertices(0.0);
3102        if asset.vertices.is_empty() {
3103            bail!("no usable Mesh block in {:?}", path)
3104        }
3105        Ok(asset)
3106    }
3107}
3108
3109fn import_x_scene_tokens(toks: Vec<Tok>, path: &Path) -> Result<ImportedXScene> {
3110    let mut cur = Cursor::new(toks);
3111    let mut scene = ImportedXScene {
3112        frames: vec![ImportedXFrame {
3113            name: "__root__".to_string(),
3114            parent: None,
3115            base_local: Mat4::identity(),
3116        }],
3117        primitives: Vec::new(),
3118        animations: Vec::new(),
3119        ticks_per_second: 4800.0,
3120    };
3121    while let Some(tok) = cur.peek().cloned() {
3122        match tok {
3123            Tok::Ident(name) if name == "Frame" => {
3124                cur.next();
3125                import_x_frame_block(&mut cur, path, &mut scene, 0)?;
3126            }
3127            Tok::Ident(name) if name == "Mesh" => {
3128                cur.next();
3129                let mesh_prims = parse_x_mesh_block(&mut cur, path, 0)?;
3130                scene
3131                    .primitives
3132                    .extend(mesh_prims.into_iter().map(imported_primitive_from_mesh));
3133            }
3134            Tok::Ident(name) if name == "AnimationSet" => {
3135                cur.next();
3136                parse_x_animation_set(&mut cur, &mut scene.animations)?;
3137            }
3138            Tok::Ident(name) if name == "AnimTicksPerSecond" => {
3139                cur.next();
3140                scene.ticks_per_second = parse_x_anim_ticks_per_second(&mut cur)
3141                    .unwrap_or(scene.ticks_per_second)
3142                    .max(1.0);
3143            }
3144            Tok::Ident(_) => {
3145                cur.next();
3146                cur.skip_block();
3147            }
3148            _ => {
3149                cur.next();
3150            }
3151        }
3152    }
3153    Ok(scene)
3154}
3155
3156fn parse_x_tokens(toks: Vec<Tok>, path: &Path) -> Result<MeshAsset> {
3157    import_x_scene_tokens(toks, path)?.into_mesh_asset(path)
3158}
3159
3160fn import_x_scene_text(text: &str, path: &Path) -> Result<ImportedXScene> {
3161    import_x_scene_tokens(tokenize_x(text), path)
3162}
3163
3164fn import_x_scene_binary(bytes: &[u8], path: &Path, float_bits: u32) -> Result<ImportedXScene> {
3165    import_x_scene_tokens(tokenize_x_binary(bytes, float_bits)?, path)
3166}
3167
3168fn import_x_scene_bytes(bytes: &[u8], path: &Path) -> Result<ImportedXScene> {
3169    import_x_scene_bytes_shion(bytes, path)
3170}
3171
3172fn parse_x_text(text: &str, path: &Path) -> Result<MeshAsset> {
3173    import_x_scene_text(text, path)?.into_mesh_asset(path)
3174}
3175
3176fn parse_x_binary(bytes: &[u8], path: &Path, float_bits: u32) -> Result<MeshAsset> {
3177    import_x_scene_binary(bytes, path, float_bits)?.into_mesh_asset(path)
3178}
3179
3180fn parse_x_bytes(bytes: &[u8], path: &Path) -> Result<MeshAsset> {
3181    import_x_scene_bytes(bytes, path)?.into_mesh_asset(path)
3182}
3183
3184fn imported_primitive_from_mesh(prim: MeshPrimitive) -> ImportedXPrimitive {
3185    ImportedXPrimitive {
3186        frame_node: prim.frame_index,
3187        texture_path: prim.texture_path,
3188        material: prim.material,
3189        vertices: prim.vertices,
3190        bones: prim
3191            .bones
3192            .into_iter()
3193            .map(|bone| ImportedXBoneBinding {
3194                frame_name: bone.frame_name,
3195                offset_matrix: bone.offset_matrix,
3196            })
3197            .collect(),
3198    }
3199}
3200
3201fn import_x_frame_block(
3202    cur: &mut Cursor,
3203    path: &Path,
3204    scene: &mut ImportedXScene,
3205    parent_frame: usize,
3206) -> Result<usize> {
3207    let name = cur
3208        .optional_name_before_block()
3209        .unwrap_or_else(|| format!("frame_{}", scene.frames.len()));
3210    cur.expect_block_start()?;
3211    let frame_idx = scene.frames.len();
3212    scene.frames.push(ImportedXFrame {
3213        name,
3214        parent: Some(parent_frame),
3215        base_local: Mat4::identity(),
3216    });
3217    while let Some(tok) = cur.peek().cloned() {
3218        match tok {
3219            Tok::Sym('}') => {
3220                cur.next();
3221                break;
3222            }
3223            Tok::Ident(name) if name == "FrameTransformMatrix" => {
3224                cur.next();
3225                scene.frames[frame_idx].base_local = parse_x_matrix_block(cur)?;
3226            }
3227            Tok::Ident(name) if name == "Frame" => {
3228                cur.next();
3229                let _ = import_x_frame_block(cur, path, scene, frame_idx)?;
3230            }
3231            Tok::Ident(name) if name == "Mesh" => {
3232                cur.next();
3233                let mesh_prims = parse_x_mesh_block(cur, path, frame_idx)?;
3234                scene
3235                    .primitives
3236                    .extend(mesh_prims.into_iter().map(imported_primitive_from_mesh));
3237            }
3238            Tok::Ident(name) if name == "AnimationSet" => {
3239                cur.next();
3240                cur.skip_block();
3241            }
3242            Tok::Ident(_) => {
3243                cur.next();
3244                cur.skip_block();
3245            }
3246            _ => {
3247                cur.next();
3248            }
3249        }
3250    }
3251    Ok(frame_idx)
3252}
3253
3254fn parse_x_mesh_block(
3255    cur: &mut Cursor,
3256    path: &Path,
3257    frame_index: usize,
3258) -> Result<Vec<MeshPrimitive>> {
3259    cur.expect_block_start()?;
3260    let vertex_count = cur.next_usize()?;
3261    let mut positions = Vec::with_capacity(vertex_count);
3262    for _ in 0..vertex_count {
3263        positions.push([cur.next_number()?, cur.next_number()?, cur.next_number()?]);
3264    }
3265    let face_count = cur.next_usize()?;
3266    let mut faces = Vec::<XFace>::with_capacity(face_count);
3267    for _ in 0..face_count {
3268        let n = cur.next_usize()?;
3269        let mut face = Vec::with_capacity(n);
3270        for _ in 0..n {
3271            face.push(cur.next_usize()?);
3272        }
3273        faces.push(XFace {
3274            indices: face,
3275            material_index: 0,
3276        });
3277    }
3278
3279    let mut texcoords = Vec::<[f32; 2]>::new();
3280    let mut normals = Vec::<[f32; 3]>::new();
3281    let mut vertex_colors = HashMap::<usize, [f32; 4]>::new();
3282    let mut influences: HashMap<usize, Vec<(usize, f32)>> = HashMap::new();
3283    let mut bones = Vec::<BoneBinding>::new();
3284    let mut bone_map: HashMap<String, usize> = HashMap::new();
3285    let mut texture_path: Option<PathBuf> = None;
3286    let mut materials = vec![ParsedXMaterial {
3287        name: None,
3288        material: default_mesh_material(),
3289        texture_path: None,
3290    }];
3291
3292    while let Some(tok) = cur.peek().cloned() {
3293        match tok {
3294            Tok::Sym('}') => {
3295                cur.next();
3296                break;
3297            }
3298            Tok::Ident(name) if name == "MeshTextureCoords" => {
3299                cur.next();
3300                texcoords = parse_x_texcoords(cur)?;
3301            }
3302            Tok::Ident(name) if name == "MeshNormals" => {
3303                cur.next();
3304                normals = parse_x_normals(cur)?;
3305            }
3306            Tok::Ident(name) if name == "MeshVertexColors" => {
3307                cur.next();
3308                vertex_colors = parse_x_vertex_colors(cur)?;
3309            }
3310            Tok::Ident(name) if name == "SkinWeights" => {
3311                cur.next();
3312                parse_x_skin_weights(cur, &mut influences, &mut bones, &mut bone_map)?;
3313            }
3314            Tok::Ident(name) if name == "TextureFilename" => {
3315                cur.next();
3316                texture_path = parse_x_texture_filename(cur, path);
3317            }
3318            Tok::Ident(name) if name == "MeshMaterialList" => {
3319                cur.next();
3320                let (face_mtls, parsed_mtls) = parse_x_material_list(cur, path)?;
3321                for (face, midx) in faces.iter_mut().zip(face_mtls.into_iter()) {
3322                    face.material_index = midx;
3323                }
3324                if !parsed_mtls.is_empty() {
3325                    materials = parsed_mtls;
3326                }
3327            }
3328            Tok::Ident(_) => {
3329                cur.next();
3330                cur.skip_block();
3331            }
3332            _ => {
3333                cur.next();
3334            }
3335        }
3336    }
3337
3338    let mut groups: HashMap<usize, Vec<MeshTriVertex>> = HashMap::new();
3339    for face in faces {
3340        if face.indices.len() < 3 {
3341            continue;
3342        }
3343        for tri in 1..face.indices.len() - 1 {
3344            let idxs = [face.indices[0], face.indices[tri], face.indices[tri + 1]];
3345            let tri_n = triangle_normal(
3346                positions.get(idxs[0]).copied().unwrap_or([0.0, 0.0, 0.0]),
3347                positions.get(idxs[1]).copied().unwrap_or([0.0, 0.0, 0.0]),
3348                positions.get(idxs[2]).copied().unwrap_or([0.0, 0.0, 0.0]),
3349            );
3350            let group = groups.entry(face.material_index).or_default();
3351            for &idx in &idxs {
3352                let pos = positions.get(idx).copied().unwrap_or([0.0, 0.0, 0.0]);
3353                let uv = texcoords.get(idx).copied().unwrap_or([0.0, 0.0]);
3354                let normal = normals.get(idx).copied().unwrap_or(tri_n);
3355                let (bone_indices, bone_weights) = packed_influences(influences.get(&idx));
3356                let color = vertex_colors
3357                    .get(&idx)
3358                    .copied()
3359                    .unwrap_or([1.0, 1.0, 1.0, 1.0]);
3360                group.push(MeshTriVertex {
3361                    pos,
3362                    uv,
3363                    normal,
3364                    tangent: [0.0, 0.0, 0.0],
3365                    binormal: [0.0, 0.0, 0.0],
3366                    color,
3367                    bone_indices,
3368                    bone_weights,
3369                });
3370            }
3371        }
3372    }
3373
3374    let mut out = Vec::new();
3375    for (midx, vertices) in groups {
3376        if vertices.is_empty() {
3377            continue;
3378        }
3379        let parsed = materials.get(midx).cloned().unwrap_or(ParsedXMaterial {
3380            name: None,
3381            material: default_mesh_material(),
3382            texture_path: None,
3383        });
3384        let mut vertices = vertices;
3385        apply_tangent_space(&mut vertices);
3386        out.push(MeshPrimitive {
3387            frame_index,
3388            texture_path: parsed.texture_path.clone().or_else(|| texture_path.clone()),
3389            material: parsed.material,
3390            runtime_desc: MeshPrimitiveRuntimeDesc::default(),
3391            vertices,
3392            bones: bones.clone(),
3393        });
3394    }
3395    Ok(out)
3396}
3397
3398fn parse_x_material_list(
3399    cur: &mut Cursor,
3400    mesh_path: &Path,
3401) -> Result<(Vec<usize>, Vec<ParsedXMaterial>)> {
3402    cur.expect_block_start()?;
3403    let material_count = cur.next_usize()?;
3404    let face_count = cur.next_usize()?;
3405    let mut face_mtls = Vec::with_capacity(face_count);
3406    for _ in 0..face_count {
3407        face_mtls.push(cur.next_usize()?);
3408    }
3409    let mut materials = Vec::new();
3410    let mut named_materials = HashMap::<String, ParsedXMaterial>::new();
3411    while let Some(tok) = cur.peek().cloned() {
3412        match tok {
3413            Tok::Sym('}') => {
3414                cur.next();
3415                break;
3416            }
3417            Tok::Ident(name) if name == "Material" => {
3418                cur.next();
3419                let parsed = parse_x_material(cur, mesh_path)?;
3420                if let Some(name) = parsed.name.clone() {
3421                    named_materials.insert(name, parsed.clone());
3422                }
3423                materials.push(parsed);
3424            }
3425            Tok::Sym('{') => {
3426                cur.next();
3427                let ref_name = match cur.peek() {
3428                    Some(Tok::Ident(s)) => Some(s.clone()),
3429                    Some(Tok::Str(s)) => Some(s.clone()),
3430                    _ => None,
3431                };
3432                if ref_name.is_some() {
3433                    let _ = cur.next();
3434                }
3435                while !cur.consume_sym('}') {
3436                    if cur.next().is_none() {
3437                        break;
3438                    }
3439                }
3440                let parsed = ref_name
3441                    .and_then(|name| named_materials.get(&name).cloned())
3442                    .unwrap_or(ParsedXMaterial {
3443                        name: None,
3444                        material: default_mesh_material(),
3445                        texture_path: None,
3446                    });
3447                materials.push(parsed);
3448            }
3449            _ => {
3450                cur.next();
3451            }
3452        }
3453    }
3454    while materials.len() < material_count {
3455        materials.push(ParsedXMaterial {
3456            name: None,
3457            material: default_mesh_material(),
3458            texture_path: None,
3459        });
3460    }
3461    Ok((face_mtls, materials))
3462}
3463
3464fn parse_x_material(cur: &mut Cursor, mesh_path: &Path) -> Result<ParsedXMaterial> {
3465    let name = cur.optional_name_before_block();
3466    cur.expect_block_start()?;
3467    let diffuse = [
3468        cur.next_number()?,
3469        cur.next_number()?,
3470        cur.next_number()?,
3471        cur.next_number()?,
3472    ];
3473    let power = cur.next_number()?.max(1.0);
3474    let specular = [
3475        cur.next_number()?,
3476        cur.next_number()?,
3477        cur.next_number()?,
3478        1.0,
3479    ];
3480    let emissive = [
3481        cur.next_number()?,
3482        cur.next_number()?,
3483        cur.next_number()?,
3484        1.0,
3485    ];
3486    let mut material = default_mesh_material();
3487    material.diffuse = diffuse;
3488    material.ambient = [diffuse[0], diffuse[1], diffuse[2], diffuse[3]];
3489    material.specular = specular;
3490    material.emissive = emissive;
3491    material.power = power;
3492    let mut texture_path = None;
3493    while let Some(tok) = cur.peek().cloned() {
3494        match tok {
3495            Tok::Sym('}') => {
3496                cur.next();
3497                break;
3498            }
3499            Tok::Ident(name) if name == "TextureFilename" => {
3500                cur.next();
3501                texture_path = parse_x_texture_filename(cur, mesh_path);
3502            }
3503            Tok::Ident(name) if name == "EffectInstance" => {
3504                cur.next();
3505                parse_x_effect_instance(cur, mesh_path, &mut material)?;
3506            }
3507            Tok::Ident(_) => {
3508                cur.next();
3509                cur.skip_block();
3510            }
3511            _ => {
3512                cur.next();
3513            }
3514        }
3515    }
3516    Ok(ParsedXMaterial {
3517        name,
3518        material,
3519        texture_path,
3520    })
3521}
3522
3523fn parse_x_matrix_block(cur: &mut Cursor) -> Result<Mat4> {
3524    cur.expect_block_start()?;
3525    let mut vals = [0.0f32; 16];
3526    for v in &mut vals {
3527        *v = cur.next_number()?;
3528    }
3529    let _ = cur.consume_sym('}');
3530    Ok(Mat4::from_x_values(vals))
3531}
3532
3533fn parse_x_texcoords(cur: &mut Cursor) -> Result<Vec<[f32; 2]>> {
3534    cur.expect_block_start()?;
3535    let count = cur.next_usize()?;
3536    let mut out = Vec::with_capacity(count);
3537    for _ in 0..count {
3538        out.push([cur.next_number()?, cur.next_number()?]);
3539    }
3540    let _ = cur.consume_sym('}');
3541    Ok(out)
3542}
3543
3544fn parse_x_normals(cur: &mut Cursor) -> Result<Vec<[f32; 3]>> {
3545    cur.expect_block_start()?;
3546    let count = cur.next_usize()?;
3547    let mut out = Vec::with_capacity(count);
3548    for _ in 0..count {
3549        out.push(normalize3([
3550            cur.next_number()?,
3551            cur.next_number()?,
3552            cur.next_number()?,
3553        ]));
3554    }
3555    let face_count = cur.next_usize()?;
3556    for _ in 0..face_count {
3557        let n = cur.next_usize()?;
3558        for _ in 0..n {
3559            let _ = cur.next_usize()?;
3560        }
3561    }
3562    let _ = cur.consume_sym('}');
3563    Ok(out)
3564}
3565
3566fn parse_x_vertex_colors(cur: &mut Cursor) -> Result<HashMap<usize, [f32; 4]>> {
3567    cur.expect_block_start()?;
3568    let count = cur.next_usize()?;
3569    let mut out = HashMap::with_capacity(count);
3570    for _ in 0..count {
3571        let vidx = cur.next_usize()?;
3572        let rgba = [
3573            cur.next_number()?,
3574            cur.next_number()?,
3575            cur.next_number()?,
3576            cur.next_number()?,
3577        ];
3578        out.insert(vidx, rgba);
3579    }
3580    let _ = cur.consume_sym('}');
3581    Ok(out)
3582}
3583
3584fn parse_x_skin_weights(
3585    cur: &mut Cursor,
3586    influences: &mut HashMap<usize, Vec<(usize, f32)>>,
3587    bones: &mut Vec<BoneBinding>,
3588    bone_map: &mut HashMap<String, usize>,
3589) -> Result<()> {
3590    cur.expect_block_start()?;
3591    let bone_name = cur.next_string()?;
3592    let bone_idx = if let Some(idx) = bone_map.get(&bone_name).copied() {
3593        idx
3594    } else {
3595        let idx = bones.len();
3596        bones.push(BoneBinding {
3597            frame_name: bone_name.clone(),
3598            frame_index: 0,
3599            offset_matrix: Mat4::identity(),
3600        });
3601        bone_map.insert(bone_name.clone(), idx);
3602        idx
3603    };
3604    let count = cur.next_usize()?;
3605    let mut verts = Vec::with_capacity(count);
3606    for _ in 0..count {
3607        verts.push(cur.next_usize()?);
3608    }
3609    let mut weights = Vec::with_capacity(count);
3610    for _ in 0..count {
3611        weights.push(cur.next_number()?);
3612    }
3613    let mut vals = [0.0f32; 16];
3614    for v in &mut vals {
3615        *v = cur.next_number()?;
3616    }
3617    bones[bone_idx].offset_matrix = Mat4::from_x_values(vals);
3618    let _ = cur.consume_sym('}');
3619    for (vidx, weight) in verts.into_iter().zip(weights.into_iter()) {
3620        influences.entry(vidx).or_default().push((bone_idx, weight));
3621    }
3622    Ok(())
3623}
3624
3625fn parse_x_texture_filename(cur: &mut Cursor, mesh_path: &Path) -> Option<PathBuf> {
3626    if cur.expect_block_start().is_err() {
3627        return None;
3628    }
3629    let name = cur.next_string().ok()?;
3630    let _ = cur.consume_sym('}');
3631    resolve_texture_path(mesh_path, &name)
3632}
3633
3634fn parse_x_anim_ticks_per_second(cur: &mut Cursor) -> Result<f32> {
3635    cur.expect_block_start()?;
3636    let ticks = cur.next_number()?.max(1.0);
3637    let _ = cur.consume_sym('}');
3638    Ok(ticks)
3639}
3640
3641fn parse_x_animation_options(cur: &mut Cursor, clip: &mut AnimationClip) -> Result<()> {
3642    cur.expect_block_start()?;
3643    clip.open_closed = cur.next_i64().unwrap_or(1) != 0;
3644    let _ = cur.next_i64();
3645    let _ = cur.consume_sym('}');
3646    Ok(())
3647}
3648
3649fn parse_x_animation_set(cur: &mut Cursor, out: &mut Vec<AnimationClip>) -> Result<()> {
3650    let name = cur
3651        .optional_name_before_block()
3652        .unwrap_or_else(|| format!("anim_{}", out.len()));
3653    cur.expect_block_start()?;
3654    let mut clip = AnimationClip {
3655        name,
3656        open_closed: true,
3657        ..Default::default()
3658    };
3659    while let Some(tok) = cur.peek().cloned() {
3660        match tok {
3661            Tok::Sym('}') => {
3662                cur.next();
3663                break;
3664            }
3665            Tok::Ident(id) if id == "Animation" => {
3666                cur.next();
3667                parse_x_animation(cur, &mut clip)?;
3668            }
3669            Tok::Ident(id) if id == "AnimationOptions" => {
3670                cur.next();
3671                parse_x_animation_options(cur, &mut clip)?;
3672            }
3673            Tok::Ident(_) => {
3674                cur.next();
3675                cur.skip_block();
3676            }
3677            _ => {
3678                cur.next();
3679            }
3680        }
3681    }
3682    if !clip.tracks.is_empty() {
3683        out.push(clip);
3684    }
3685    Ok(())
3686}
3687
3688fn parse_x_animation(cur: &mut Cursor, clip: &mut AnimationClip) -> Result<()> {
3689    let _anim_name = cur.optional_name_before_block();
3690    cur.expect_block_start()?;
3691    let mut target_name: Option<String> = None;
3692    while let Some(tok) = cur.peek().cloned() {
3693        match tok {
3694            Tok::Sym('}') => {
3695                cur.next();
3696                break;
3697            }
3698            Tok::Sym('{') => {
3699                cur.next();
3700                if target_name.is_none() {
3701                    target_name = Some(cur.next_string()?);
3702                }
3703                while !cur.consume_sym('}') {
3704                    let _ = cur.next();
3705                }
3706            }
3707            Tok::Ident(id) if id == "AnimationKey" => {
3708                cur.next();
3709                if let Some(target) = target_name.clone() {
3710                    parse_x_animation_key(cur, clip, &target)?;
3711                } else {
3712                    cur.skip_block();
3713                }
3714            }
3715            Tok::Ident(_) => {
3716                cur.next();
3717                cur.skip_block();
3718            }
3719            _ => {
3720                cur.next();
3721            }
3722        }
3723    }
3724    Ok(())
3725}
3726
3727fn parse_x_animation_key(
3728    cur: &mut Cursor,
3729    clip: &mut AnimationClip,
3730    target_name: &str,
3731) -> Result<()> {
3732    cur.expect_block_start()?;
3733    let key_type = cur.next_i64()?;
3734    let count = cur.next_usize()?;
3735    let track = clip.tracks.entry(target_name.to_string()).or_default();
3736    for _ in 0..count {
3737        let time = cur.next_i64()?;
3738        let nvals = cur.next_usize()?;
3739        clip.max_time = clip.max_time.max(time);
3740        match key_type {
3741            0 => {
3742                let vals = read_n_numbers(cur, nvals)?;
3743                let q = if vals.len() >= 4 {
3744                    // .x rotation keys are written as w, x, y, z.
3745                    Quat {
3746                        x: vals[1],
3747                        y: vals[2],
3748                        z: vals[3],
3749                        w: vals[0],
3750                    }
3751                    .normalize()
3752                } else {
3753                    Quat {
3754                        x: 0.0,
3755                        y: 0.0,
3756                        z: 0.0,
3757                        w: 1.0,
3758                    }
3759                };
3760                track.rotation_keys.push(QuatKey { time, value: q });
3761            }
3762            1 => {
3763                let vals = read_n_numbers(cur, nvals)?;
3764                let v = [
3765                    vals.get(0).copied().unwrap_or(1.0),
3766                    vals.get(1).copied().unwrap_or(1.0),
3767                    vals.get(2).copied().unwrap_or(1.0),
3768                ];
3769                track.scale_keys.push(Vec3Key { time, value: v });
3770            }
3771            2 => {
3772                let vals = read_n_numbers(cur, nvals)?;
3773                let v = [
3774                    vals.get(0).copied().unwrap_or(0.0),
3775                    vals.get(1).copied().unwrap_or(0.0),
3776                    vals.get(2).copied().unwrap_or(0.0),
3777                ];
3778                track.position_keys.push(Vec3Key { time, value: v });
3779            }
3780            4 => {
3781                let vals = read_n_numbers(cur, nvals)?;
3782                let mut m = [0.0f32; 16];
3783                for (dst, src) in m.iter_mut().zip(vals.into_iter()) {
3784                    *dst = src;
3785                }
3786                track.matrix_keys.push(MatKey {
3787                    time,
3788                    value: Mat4::from_x_values(m),
3789                });
3790            }
3791            _ => {
3792                let _ = read_n_numbers(cur, nvals)?;
3793            }
3794        }
3795    }
3796    let _ = cur.consume_sym('}');
3797    Ok(())
3798}
3799
3800fn read_n_numbers(cur: &mut Cursor, n: usize) -> Result<Vec<f32>> {
3801    let mut out = Vec::with_capacity(n);
3802    for _ in 0..n {
3803        out.push(cur.next_number()?);
3804    }
3805    Ok(out)
3806}
3807
3808fn packed_influences(src: Option<&Vec<(usize, f32)>>) -> ([u16; 4], [f32; 4]) {
3809    let mut bone_indices = [0u16; 4];
3810    let mut bone_weights = [0.0f32; 4];
3811    let Some(src) = src else {
3812        return (bone_indices, bone_weights);
3813    };
3814    let mut vals = src.clone();
3815    vals.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
3816    let mut sum = 0.0f32;
3817    for (dst_idx, (bone, weight)) in vals.into_iter().take(4).enumerate() {
3818        bone_indices[dst_idx] = bone as u16;
3819        bone_weights[dst_idx] = weight.max(0.0);
3820        sum += bone_weights[dst_idx];
3821    }
3822    if sum > 0.0 {
3823        for w in &mut bone_weights {
3824            *w /= sum;
3825        }
3826    }
3827    (bone_indices, bone_weights)
3828}
3829
3830fn resolve_texture_path(mesh_path: &Path, tex_name: &str) -> Option<PathBuf> {
3831    let p = Path::new(tex_name);
3832    if p.is_absolute() {
3833        if let Some(found) = find_existing_casefold(p) {
3834            return Some(found);
3835        }
3836    }
3837    let mut candidates = Vec::new();
3838    if let Some(parent) = mesh_path.parent() {
3839        if let Some(found) = resolve_relative_casefold(parent, tex_name) {
3840            return Some(found);
3841        }
3842        candidates.push(parent.join(tex_name));
3843    }
3844    candidates.push(PathBuf::from(tex_name));
3845    if p.extension().is_none() {
3846        for ext in ["png", "bmp", "jpg", "jpeg", "g00"] {
3847            if let Some(parent) = mesh_path.parent() {
3848                candidates.push(parent.join(format!("{}.{}", tex_name, ext)));
3849            }
3850        }
3851    }
3852    candidates
3853        .into_iter()
3854        .find_map(|p| find_existing_casefold(&p))
3855}
3856
3857fn tokenize_x(text: &str) -> Vec<Tok> {
3858    let mut out = Vec::new();
3859    let chars: Vec<char> = text.chars().collect();
3860    let mut i = 0usize;
3861    while i < chars.len() {
3862        let ch = chars[i];
3863        if ch == '/' && i + 1 < chars.len() && chars[i + 1] == '/' {
3864            while i < chars.len() && chars[i] != '\n' {
3865                i += 1;
3866            }
3867            continue;
3868        }
3869        if ch.is_whitespace() || ch == ';' || ch == ',' {
3870            i += 1;
3871            continue;
3872        }
3873        if ch == '{' || ch == '}' {
3874            out.push(Tok::Sym(ch));
3875            i += 1;
3876            continue;
3877        }
3878        if ch == '"' {
3879            i += 1;
3880            let start = i;
3881            while i < chars.len() && chars[i] != '"' {
3882                i += 1;
3883            }
3884            out.push(Tok::Str(chars[start..i].iter().collect()));
3885            if i < chars.len() {
3886                i += 1;
3887            }
3888            continue;
3889        }
3890        if ch.is_ascii_digit() || ch == '-' || ch == '+' || ch == '.' {
3891            let start = i;
3892            i += 1;
3893            while i < chars.len() {
3894                let c = chars[i];
3895                if c.is_ascii_digit() || matches!(c, '.' | '-' | '+' | 'e' | 'E') {
3896                    i += 1;
3897                } else {
3898                    break;
3899                }
3900            }
3901            let s: String = chars[start..i].iter().collect();
3902            if let Ok(v) = s.parse::<f32>() {
3903                out.push(Tok::Number(v));
3904            }
3905            continue;
3906        }
3907        if ch.is_alphanumeric() || ch == '_' || ch == '.' {
3908            let start = i;
3909            i += 1;
3910            while i < chars.len()
3911                && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == '.')
3912            {
3913                i += 1;
3914            }
3915            out.push(Tok::Ident(chars[start..i].iter().collect()));
3916            continue;
3917        }
3918        i += 1;
3919    }
3920    out
3921}
3922
3923fn tokenize_x_binary(bytes: &[u8], float_bits: u32) -> Result<Vec<Tok>> {
3924    let mut out = Vec::new();
3925    let mut i = 0usize;
3926    while i + 2 <= bytes.len() {
3927        let token = u16::from_le_bytes([bytes[i], bytes[i + 1]]);
3928        i += 2;
3929        match token {
3930            1 => {
3931                let count = read_u32_le(bytes, &mut i)? as usize;
3932                let raw = read_bytes(bytes, &mut i, count)?;
3933                out.push(Tok::Ident(trim_x_string(raw)));
3934            }
3935            2 => {
3936                let count = read_u32_le(bytes, &mut i)? as usize;
3937                let raw = read_bytes(bytes, &mut i, count)?;
3938                out.push(Tok::Str(trim_x_string(raw)));
3939            }
3940            3 => {
3941                let v = read_u32_le(bytes, &mut i)? as i32;
3942                out.push(Tok::Number(v as f32));
3943            }
3944            5 => {
3945                let _ = read_bytes(bytes, &mut i, 16)?;
3946            }
3947            6 => {
3948                let count = read_u32_le(bytes, &mut i)? as usize;
3949                for _ in 0..count {
3950                    let v = read_u32_le(bytes, &mut i)? as i32;
3951                    out.push(Tok::Number(v as f32));
3952                }
3953            }
3954            7 => {
3955                let count = read_u32_le(bytes, &mut i)? as usize;
3956                if float_bits >= 64 {
3957                    for _ in 0..count {
3958                        let v = read_f64_le(bytes, &mut i)?;
3959                        out.push(Tok::Number(v as f32));
3960                    }
3961                } else {
3962                    for _ in 0..count {
3963                        let v = read_f32_le(bytes, &mut i)?;
3964                        out.push(Tok::Number(v));
3965                    }
3966                }
3967            }
3968            10 => out.push(Tok::Sym('{')),
3969            11 => out.push(Tok::Sym('}')),
3970            12 => out.push(Tok::Sym('(')),
3971            13 => out.push(Tok::Sym(')')),
3972            14 => out.push(Tok::Sym('[')),
3973            15 => out.push(Tok::Sym(']')),
3974            16 => out.push(Tok::Sym('<')),
3975            17 => out.push(Tok::Sym('>')),
3976            18 => out.push(Tok::Sym('.')),
3977            19 => out.push(Tok::Sym(',')),
3978            20 => out.push(Tok::Sym(';')),
3979            31 => out.push(Tok::Ident("template".to_string())),
3980            40 => out.push(Tok::Ident("WORD".to_string())),
3981            41 => out.push(Tok::Ident("DWORD".to_string())),
3982            42 => out.push(Tok::Ident("FLOAT".to_string())),
3983            43 => out.push(Tok::Ident("DOUBLE".to_string())),
3984            44 => out.push(Tok::Ident("CHAR".to_string())),
3985            45 => out.push(Tok::Ident("UCHAR".to_string())),
3986            46 => out.push(Tok::Ident("SWORD".to_string())),
3987            47 => out.push(Tok::Ident("SDWORD".to_string())),
3988            48 => out.push(Tok::Ident("VOID".to_string())),
3989            49 => out.push(Tok::Ident("LPSTR".to_string())),
3990            50 => out.push(Tok::Ident("UNICODE".to_string())),
3991            51 => out.push(Tok::Ident("CSTRING".to_string())),
3992            52 => out.push(Tok::Ident("ARRAY".to_string())),
3993            _ => {
3994                bail!(
3995                    "unsupported .x binary token {} at offset {}",
3996                    token,
3997                    i.saturating_sub(2)
3998                );
3999            }
4000        }
4001    }
4002    Ok(out)
4003}
4004
4005fn read_bytes<'a>(bytes: &'a [u8], i: &mut usize, n: usize) -> Result<&'a [u8]> {
4006    if bytes.len().saturating_sub(*i) < n {
4007        bail!("unexpected eof in .x binary stream")
4008    }
4009    let out = &bytes[*i..*i + n];
4010    *i += n;
4011    Ok(out)
4012}
4013
4014fn read_u32_le(bytes: &[u8], i: &mut usize) -> Result<u32> {
4015    let raw = read_bytes(bytes, i, 4)?;
4016    Ok(u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]))
4017}
4018
4019fn read_f32_le(bytes: &[u8], i: &mut usize) -> Result<f32> {
4020    let raw = read_bytes(bytes, i, 4)?;
4021    Ok(f32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]))
4022}
4023
4024fn read_f64_le(bytes: &[u8], i: &mut usize) -> Result<f64> {
4025    let raw = read_bytes(bytes, i, 8)?;
4026    Ok(f64::from_le_bytes([
4027        raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7],
4028    ]))
4029}
4030
4031fn trim_x_string(raw: &[u8]) -> String {
4032    let s = String::from_utf8_lossy(raw).to_string();
4033    s.trim_end_matches('\0').to_string()
4034}
4035
4036pub const SIGLUS_INTERNAL_MESH_MAGIC: &[u8; 8] = b"SGMESH10";
4037pub const SIGLUS_INTERNAL_MESH_VERSION: u32 = 3;
4038
4039pub fn write_internal_mesh_asset(path: &Path, asset: &MeshAsset) -> Result<()> {
4040    let mut writer = AssetWriter::default();
4041    writer.write_bytes(SIGLUS_INTERNAL_MESH_MAGIC)?;
4042    writer.write_u32(SIGLUS_INTERNAL_MESH_VERSION)?;
4043    let base_dir = asset.source_path.parent().unwrap_or_else(|| Path::new(""));
4044    writer.write_string(&normalize_runtime_path_string(&asset.source_path))?;
4045    writer.write_opt_path(asset.texture_path.as_deref(), base_dir)?;
4046    writer.write_u32(asset.bone_count as u32)?;
4047    writer.write_f32(asset.ticks_per_second)?;
4048
4049    writer.write_u32(asset.frames.len() as u32)?;
4050    for frame in &asset.frames {
4051        writer.write_string(&frame.name)?;
4052        writer.write_mat4(frame.base_local)?;
4053        writer.write_u32(frame.children.len() as u32)?;
4054        for &child in &frame.children {
4055            writer.write_u32(child as u32)?;
4056        }
4057    }
4058
4059    writer.write_u32(asset.primitives.len() as u32)?;
4060    for prim in &asset.primitives {
4061        writer.write_u32(prim.frame_index as u32)?;
4062        writer.write_opt_path(prim.texture_path.as_deref(), base_dir)?;
4063        writer.write_mesh_material(&prim.material, base_dir)?;
4064        writer.write_u32(prim.vertices.len() as u32)?;
4065        for vertex in &prim.vertices {
4066            writer.write_vertex(vertex)?;
4067        }
4068        writer.write_u32(prim.bones.len() as u32)?;
4069        for bone in &prim.bones {
4070            writer.write_string(&bone.frame_name)?;
4071            writer.write_u32(bone.frame_index as u32)?;
4072            writer.write_mat4(bone.offset_matrix)?;
4073        }
4074        writer.write_mesh_runtime_desc(&prim.runtime_desc)?;
4075    }
4076
4077    writer.write_u32(asset.animations.len() as u32)?;
4078    for clip in &asset.animations {
4079        writer.write_string(&clip.name)?;
4080        writer.write_i64(clip.max_time)?;
4081        writer.write_bool(clip.open_closed)?;
4082        writer.write_u32(clip.tracks.len() as u32)?;
4083        for (track_name, track) in &clip.tracks {
4084            writer.write_string(track_name)?;
4085            writer.write_u32(track.rotation_keys.len() as u32)?;
4086            for key in &track.rotation_keys {
4087                writer.write_i64(key.time)?;
4088                writer.write_quat(key.value)?;
4089            }
4090            writer.write_u32(track.scale_keys.len() as u32)?;
4091            for key in &track.scale_keys {
4092                writer.write_i64(key.time)?;
4093                writer.write_vec3(key.value)?;
4094            }
4095            writer.write_u32(track.position_keys.len() as u32)?;
4096            for key in &track.position_keys {
4097                writer.write_i64(key.time)?;
4098                writer.write_vec3(key.value)?;
4099            }
4100            writer.write_u32(track.matrix_keys.len() as u32)?;
4101            for key in &track.matrix_keys {
4102                writer.write_i64(key.time)?;
4103                writer.write_mat4(key.value)?;
4104            }
4105        }
4106    }
4107
4108    if let Some(parent) = path.parent() {
4109        fs::create_dir_all(parent)
4110            .with_context(|| format!("create mesh asset dir {:?}", parent))?;
4111    }
4112    fs::write(path, writer.finish())
4113        .with_context(|| format!("write internal mesh asset {:?}", path))?;
4114    Ok(())
4115}
4116
4117pub fn read_internal_mesh_asset(path: &Path) -> Result<MeshAsset> {
4118    let bytes = fs::read(path).with_context(|| format!("read internal mesh asset {:?}", path))?;
4119    let mut reader = AssetReader::new(&bytes);
4120    let magic = reader.read_fixed::<8>()?;
4121    if &magic != SIGLUS_INTERNAL_MESH_MAGIC {
4122        bail!("invalid internal mesh asset magic in {:?}", path);
4123    }
4124    let version = reader.read_u32()?;
4125    if version == 0 || version > SIGLUS_INTERNAL_MESH_VERSION {
4126        bail!(
4127            "unsupported internal mesh asset version {} in {:?}",
4128            version,
4129            path
4130        );
4131    }
4132    let base_dir = path.parent().unwrap_or_else(|| Path::new(""));
4133    let source_path = resolve_runtime_asset_path(base_dir, &reader.read_string()?);
4134    let texture_path = reader.read_opt_path(base_dir)?;
4135    let bone_count = reader.read_u32()? as usize;
4136    let ticks_per_second = reader.read_f32()?;
4137
4138    let frame_count = reader.read_u32()? as usize;
4139    let mut frames = Vec::with_capacity(frame_count);
4140    for _ in 0..frame_count {
4141        let name = reader.read_string()?;
4142        let base_local = reader.read_mat4()?;
4143        let child_count = reader.read_u32()? as usize;
4144        let mut children = Vec::with_capacity(child_count);
4145        for _ in 0..child_count {
4146            children.push(reader.read_u32()? as usize);
4147        }
4148        frames.push(FrameNode {
4149            name,
4150            base_local,
4151            children,
4152        });
4153    }
4154
4155    let prim_count = reader.read_u32()? as usize;
4156    let mut primitives = Vec::with_capacity(prim_count);
4157    for _ in 0..prim_count {
4158        let frame_index = reader.read_u32()? as usize;
4159        let texture_path = reader.read_opt_path(base_dir)?;
4160        let material = reader.read_mesh_material(base_dir)?;
4161        let vertex_count = reader.read_u32()? as usize;
4162        let mut vertices = Vec::with_capacity(vertex_count);
4163        for _ in 0..vertex_count {
4164            vertices.push(reader.read_vertex()?);
4165        }
4166        let bone_binding_count = reader.read_u32()? as usize;
4167        let mut bones = Vec::with_capacity(bone_binding_count);
4168        for _ in 0..bone_binding_count {
4169            bones.push(BoneBinding {
4170                frame_name: reader.read_string()?,
4171                frame_index: reader.read_u32()? as usize,
4172                offset_matrix: reader.read_mat4()?,
4173            });
4174        }
4175        let runtime_desc = if version >= 2 {
4176            reader.read_mesh_runtime_desc(version)?
4177        } else {
4178            build_mesh_primitive_runtime_desc_from_material(
4179                &material,
4180                texture_path.is_some(),
4181                !bones.is_empty(),
4182                vertex_count as u32,
4183                bone_binding_count as u32,
4184            )
4185        };
4186        primitives.push(MeshPrimitive {
4187            frame_index,
4188            texture_path,
4189            material,
4190            runtime_desc,
4191            vertices,
4192            bones,
4193        });
4194    }
4195
4196    let anim_count = reader.read_u32()? as usize;
4197    let mut animations = Vec::with_capacity(anim_count);
4198    for _ in 0..anim_count {
4199        let name = reader.read_string()?;
4200        let max_time = reader.read_i64()?;
4201        let open_closed = reader.read_bool()?;
4202        let track_count = reader.read_u32()? as usize;
4203        let mut tracks = HashMap::new();
4204        for _ in 0..track_count {
4205            let track_name = reader.read_string()?;
4206            let rotation_count = reader.read_u32()? as usize;
4207            let mut rotation_keys = Vec::with_capacity(rotation_count);
4208            for _ in 0..rotation_count {
4209                rotation_keys.push(QuatKey {
4210                    time: reader.read_i64()?,
4211                    value: reader.read_quat()?,
4212                });
4213            }
4214            let scale_count = reader.read_u32()? as usize;
4215            let mut scale_keys = Vec::with_capacity(scale_count);
4216            for _ in 0..scale_count {
4217                scale_keys.push(Vec3Key {
4218                    time: reader.read_i64()?,
4219                    value: reader.read_vec3()?,
4220                });
4221            }
4222            let position_count = reader.read_u32()? as usize;
4223            let mut position_keys = Vec::with_capacity(position_count);
4224            for _ in 0..position_count {
4225                position_keys.push(Vec3Key {
4226                    time: reader.read_i64()?,
4227                    value: reader.read_vec3()?,
4228                });
4229            }
4230            let matrix_count = reader.read_u32()? as usize;
4231            let mut matrix_keys = Vec::with_capacity(matrix_count);
4232            for _ in 0..matrix_count {
4233                matrix_keys.push(MatKey {
4234                    time: reader.read_i64()?,
4235                    value: reader.read_mat4()?,
4236                });
4237            }
4238            tracks.insert(
4239                track_name,
4240                AnimationTrack {
4241                    rotation_keys,
4242                    scale_keys,
4243                    position_keys,
4244                    matrix_keys,
4245                },
4246            );
4247        }
4248        animations.push(AnimationClip {
4249            name,
4250            tracks,
4251            max_time,
4252            open_closed,
4253        });
4254    }
4255
4256    let mut asset = MeshAsset {
4257        source_path,
4258        texture_path,
4259        vertices: Vec::new(),
4260        bone_count,
4261        ticks_per_second,
4262        primitives,
4263        frames,
4264        animations,
4265    };
4266    finalize_mesh_asset(&mut asset);
4267    Ok(asset)
4268}
4269
4270fn normalize_runtime_path_string(path: &Path) -> String {
4271    path.to_string_lossy().replace('\\', "/")
4272}
4273
4274fn normalize_runtime_rel_path(path: &Path, base_dir: &Path) -> String {
4275    if let Ok(rel) = path.strip_prefix(base_dir) {
4276        return normalize_runtime_path_string(rel);
4277    }
4278    normalize_runtime_path_string(path)
4279}
4280
4281fn resolve_runtime_asset_path(base_dir: &Path, raw: &str) -> PathBuf {
4282    let path = Path::new(raw);
4283    if path.is_absolute() {
4284        return path.to_path_buf();
4285    }
4286    base_dir.join(path)
4287}
4288
4289#[derive(Default)]
4290struct AssetWriter {
4291    bytes: Vec<u8>,
4292}
4293
4294impl AssetWriter {
4295    fn finish(self) -> Vec<u8> {
4296        self.bytes
4297    }
4298
4299    fn write_bytes(&mut self, raw: &[u8]) -> Result<()> {
4300        self.bytes.write_all(raw)?;
4301        Ok(())
4302    }
4303
4304    fn write_u8(&mut self, value: u8) -> Result<()> {
4305        self.bytes.write_all(&[value])?;
4306        Ok(())
4307    }
4308
4309    fn write_bool(&mut self, value: bool) -> Result<()> {
4310        self.write_u8(if value { 1 } else { 0 })
4311    }
4312
4313    fn write_u16(&mut self, value: u16) -> Result<()> {
4314        self.bytes.write_all(&value.to_le_bytes())?;
4315        Ok(())
4316    }
4317
4318    fn write_u32(&mut self, value: u32) -> Result<()> {
4319        self.bytes.write_all(&value.to_le_bytes())?;
4320        Ok(())
4321    }
4322
4323    fn write_i32(&mut self, value: i32) -> Result<()> {
4324        self.bytes.write_all(&value.to_le_bytes())?;
4325        Ok(())
4326    }
4327
4328    fn write_i64(&mut self, value: i64) -> Result<()> {
4329        self.bytes.write_all(&value.to_le_bytes())?;
4330        Ok(())
4331    }
4332
4333    fn write_f32(&mut self, value: f32) -> Result<()> {
4334        self.bytes.write_all(&value.to_le_bytes())?;
4335        Ok(())
4336    }
4337
4338    fn write_string(&mut self, value: &str) -> Result<()> {
4339        let raw = value.as_bytes();
4340        self.write_u32(raw.len() as u32)?;
4341        self.bytes.write_all(raw)?;
4342        Ok(())
4343    }
4344
4345    fn write_opt_string(&mut self, value: Option<&str>) -> Result<()> {
4346        match value {
4347            Some(v) => {
4348                self.write_bool(true)?;
4349                self.write_string(v)?;
4350                Ok(())
4351            }
4352            None => self.write_bool(false),
4353        }
4354    }
4355
4356    fn write_opt_path(&mut self, value: Option<&Path>, base_dir: &Path) -> Result<()> {
4357        self.write_opt_string(
4358            value
4359                .map(|p| normalize_runtime_rel_path(p, base_dir))
4360                .as_deref(),
4361        )
4362    }
4363
4364    fn write_vec3(&mut self, value: [f32; 3]) -> Result<()> {
4365        for v in value {
4366            self.write_f32(v)?;
4367        }
4368        Ok(())
4369    }
4370
4371    fn write_vec4(&mut self, value: [f32; 4]) -> Result<()> {
4372        for v in value {
4373            self.write_f32(v)?;
4374        }
4375        Ok(())
4376    }
4377
4378    fn write_mat4(&mut self, value: Mat4) -> Result<()> {
4379        for row in value.m {
4380            self.write_vec4(row)?;
4381        }
4382        Ok(())
4383    }
4384
4385    fn write_quat(&mut self, value: Quat) -> Result<()> {
4386        self.write_f32(value.x)?;
4387        self.write_f32(value.y)?;
4388        self.write_f32(value.z)?;
4389        self.write_f32(value.w)?;
4390        Ok(())
4391    }
4392
4393    fn write_vertex(&mut self, value: &MeshTriVertex) -> Result<()> {
4394        self.write_vec3(value.pos)?;
4395        self.write_f32(value.uv[0])?;
4396        self.write_f32(value.uv[1])?;
4397        self.write_vec3(value.normal)?;
4398        self.write_vec3(value.tangent)?;
4399        self.write_vec3(value.binormal)?;
4400        self.write_vec4(value.color)?;
4401        for index in value.bone_indices {
4402            self.write_u16(index)?;
4403        }
4404        for weight in value.bone_weights {
4405            self.write_f32(weight)?;
4406        }
4407        Ok(())
4408    }
4409
4410    fn write_mesh_material(&mut self, value: &MeshMaterial, base_dir: &Path) -> Result<()> {
4411        self.write_vec4(value.diffuse)?;
4412        self.write_vec4(value.ambient)?;
4413        self.write_vec4(value.specular)?;
4414        self.write_vec4(value.emissive)?;
4415        self.write_f32(value.power)?;
4416        self.write_u32(value.lighting_type as u32)?;
4417        self.write_u32(value.shading_type as u32)?;
4418        self.write_u32(value.shader_option)?;
4419        self.write_vec4(value.rim_light_color)?;
4420        self.write_f32(value.rim_light_power)?;
4421        self.write_f32(value.parallax_max_height)?;
4422        self.write_bool(value.alpha_test_enable)?;
4423        self.write_f32(value.alpha_ref)?;
4424        self.write_bool(value.cull_disable)?;
4425        self.write_bool(value.shadow_map_enable)?;
4426        self.write_bool(value.use_mesh_tex)?;
4427        self.write_bool(value.use_mrbd)?;
4428        self.write_vec4(value.mrbd)?;
4429        self.write_bool(value.use_rgb)?;
4430        self.write_vec4(value.rgb_rate)?;
4431        self.write_vec4(value.add_rgb)?;
4432        self.write_bool(value.use_mul_vertex_color)?;
4433        self.write_f32(value.mul_vertex_color_rate)?;
4434        self.write_f32(value.depth_buffer_shadow_bias)?;
4435        self.write_u32(value.directional_light_ids.len() as u32)?;
4436        for id in &value.directional_light_ids {
4437            self.write_i32(*id)?;
4438        }
4439        self.write_u32(value.point_light_ids.len() as u32)?;
4440        for id in &value.point_light_ids {
4441            self.write_i32(*id)?;
4442        }
4443        self.write_u32(value.spot_light_ids.len() as u32)?;
4444        for id in &value.spot_light_ids {
4445            self.write_i32(*id)?;
4446        }
4447        self.write_opt_path(value.normal_texture_path.as_deref(), base_dir)?;
4448        self.write_opt_path(value.toon_texture_path.as_deref(), base_dir)?;
4449        self.write_opt_string(value.effect_filename.as_deref())?;
4450        Ok(())
4451    }
4452
4453    fn write_mesh_runtime_material_key(&mut self, value: MeshRuntimeMaterialKey) -> Result<()> {
4454        self.write_bool(value.use_mesh_tex)?;
4455        self.write_bool(value.use_shadow_tex)?;
4456        self.write_bool(value.use_toon_tex)?;
4457        self.write_bool(value.use_normal_tex)?;
4458        self.write_bool(value.use_mul_vertex_color)?;
4459        self.write_bool(value.use_mrbd)?;
4460        self.write_bool(value.use_rgb)?;
4461        self.write_u32(value.lighting_type as u32)?;
4462        self.write_u32(value.shading_type as u32)?;
4463        self.write_u32(value.shader_option)?;
4464        self.write_bool(value.skinned)?;
4465        self.write_bool(value.alpha_test_enable)?;
4466        self.write_bool(value.cull_disable)?;
4467        self.write_bool(value.shadow_map_enable)?;
4468        Ok(())
4469    }
4470
4471    fn write_mesh_runtime_desc(&mut self, value: &MeshPrimitiveRuntimeDesc) -> Result<()> {
4472        self.write_u32(value.effect_profile as u32)?;
4473        self.write_string(&value.effect_key)?;
4474        self.write_string(&value.technique_name)?;
4475        self.write_string(&value.shadow_effect_key)?;
4476        self.write_string(&value.shadow_technique_name)?;
4477        self.write_bool(value.use_mesh_texture_slot)?;
4478        self.write_bool(value.use_normal_texture_slot)?;
4479        self.write_bool(value.use_toon_texture_slot)?;
4480        self.write_bool(value.use_shadow_texture_slot)?;
4481        self.write_mesh_runtime_material_key(value.material_key)?;
4482        self.write_u32(value.vertex_stride_bytes)?;
4483        self.write_u32(value.vertex_count)?;
4484        self.write_u32(value.bone_palette_len)?;
4485        Ok(())
4486    }
4487}
4488
4489struct AssetReader<'a> {
4490    cursor: IoCursor<&'a [u8]>,
4491}
4492
4493impl<'a> AssetReader<'a> {
4494    fn new(bytes: &'a [u8]) -> Self {
4495        Self {
4496            cursor: IoCursor::new(bytes),
4497        }
4498    }
4499
4500    fn read_exact_into<const N: usize>(&mut self) -> Result<[u8; N]> {
4501        let mut raw = [0u8; N];
4502        self.cursor.read_exact(&mut raw)?;
4503        Ok(raw)
4504    }
4505
4506    fn read_fixed<const N: usize>(&mut self) -> Result<[u8; N]> {
4507        self.read_exact_into::<N>()
4508    }
4509
4510    fn read_u8(&mut self) -> Result<u8> {
4511        Ok(self.read_exact_into::<1>()?[0])
4512    }
4513
4514    fn read_bool(&mut self) -> Result<bool> {
4515        Ok(self.read_u8()? != 0)
4516    }
4517
4518    fn read_u16(&mut self) -> Result<u16> {
4519        Ok(u16::from_le_bytes(self.read_exact_into::<2>()?))
4520    }
4521
4522    fn read_u32(&mut self) -> Result<u32> {
4523        Ok(u32::from_le_bytes(self.read_exact_into::<4>()?))
4524    }
4525
4526    fn read_i32(&mut self) -> Result<i32> {
4527        Ok(i32::from_le_bytes(self.read_exact_into::<4>()?))
4528    }
4529
4530    fn read_i64(&mut self) -> Result<i64> {
4531        Ok(i64::from_le_bytes(self.read_exact_into::<8>()?))
4532    }
4533
4534    fn read_f32(&mut self) -> Result<f32> {
4535        Ok(f32::from_le_bytes(self.read_exact_into::<4>()?))
4536    }
4537
4538    fn read_string(&mut self) -> Result<String> {
4539        let len = self.read_u32()? as usize;
4540        let mut raw = vec![0u8; len];
4541        self.cursor.read_exact(&mut raw)?;
4542        Ok(String::from_utf8_lossy(&raw).to_string())
4543    }
4544
4545    fn read_opt_string(&mut self) -> Result<Option<String>> {
4546        if self.read_bool()? {
4547            Ok(Some(self.read_string()?))
4548        } else {
4549            Ok(None)
4550        }
4551    }
4552
4553    fn read_opt_path(&mut self, base_dir: &Path) -> Result<Option<PathBuf>> {
4554        Ok(self
4555            .read_opt_string()?
4556            .map(|raw| resolve_runtime_asset_path(base_dir, &raw)))
4557    }
4558
4559    fn read_vec3(&mut self) -> Result<[f32; 3]> {
4560        Ok([self.read_f32()?, self.read_f32()?, self.read_f32()?])
4561    }
4562
4563    fn read_vec4(&mut self) -> Result<[f32; 4]> {
4564        Ok([
4565            self.read_f32()?,
4566            self.read_f32()?,
4567            self.read_f32()?,
4568            self.read_f32()?,
4569        ])
4570    }
4571
4572    fn read_mat4(&mut self) -> Result<Mat4> {
4573        Ok(Mat4 {
4574            m: [
4575                self.read_vec4()?,
4576                self.read_vec4()?,
4577                self.read_vec4()?,
4578                self.read_vec4()?,
4579            ],
4580        })
4581    }
4582
4583    fn read_quat(&mut self) -> Result<Quat> {
4584        Ok(Quat {
4585            x: self.read_f32()?,
4586            y: self.read_f32()?,
4587            z: self.read_f32()?,
4588            w: self.read_f32()?,
4589        })
4590    }
4591
4592    fn read_vertex(&mut self) -> Result<MeshTriVertex> {
4593        Ok(MeshTriVertex {
4594            pos: self.read_vec3()?,
4595            uv: [self.read_f32()?, self.read_f32()?],
4596            normal: self.read_vec3()?,
4597            tangent: self.read_vec3()?,
4598            binormal: self.read_vec3()?,
4599            color: self.read_vec4()?,
4600            bone_indices: [
4601                self.read_u16()?,
4602                self.read_u16()?,
4603                self.read_u16()?,
4604                self.read_u16()?,
4605            ],
4606            bone_weights: [
4607                self.read_f32()?,
4608                self.read_f32()?,
4609                self.read_f32()?,
4610                self.read_f32()?,
4611            ],
4612        })
4613    }
4614
4615    fn read_mesh_runtime_material_key(&mut self) -> Result<MeshRuntimeMaterialKey> {
4616        Ok(MeshRuntimeMaterialKey {
4617            use_mesh_tex: self.read_bool()?,
4618            use_shadow_tex: self.read_bool()?,
4619            use_toon_tex: self.read_bool()?,
4620            use_normal_tex: self.read_bool()?,
4621            use_mul_vertex_color: self.read_bool()?,
4622            use_mrbd: self.read_bool()?,
4623            use_rgb: self.read_bool()?,
4624            lighting_type: match self.read_u32()? {
4625                1 => MeshLightingType::Lambert,
4626                2 => MeshLightingType::BlinnPhong,
4627                3 => MeshLightingType::PerPixelBlinnPhong,
4628                4 => MeshLightingType::PerPixelHalfLambert,
4629                5 => MeshLightingType::Toon,
4630                6 => MeshLightingType::FixedFunction,
4631                7 => MeshLightingType::PerPixelFixedFunction,
4632                8 => MeshLightingType::Bump,
4633                9 => MeshLightingType::Parallax,
4634                _ => MeshLightingType::None,
4635            },
4636            shading_type: if self.read_u32()? == 1 {
4637                MeshShadingType::DepthBuffer
4638            } else {
4639                MeshShadingType::None
4640            },
4641            shader_option: self.read_u32()?,
4642            skinned: self.read_bool()?,
4643            alpha_test_enable: self.read_bool()?,
4644            cull_disable: self.read_bool()?,
4645            shadow_map_enable: self.read_bool()?,
4646        })
4647    }
4648
4649    fn read_mesh_runtime_desc(&mut self, version: u32) -> Result<MeshPrimitiveRuntimeDesc> {
4650        let effect_profile = match self.read_u32()? {
4651            1 => MeshEffectProfile::Mesh,
4652            2 => MeshEffectProfile::SkinnedMesh,
4653            3 => MeshEffectProfile::ShadowMap,
4654            _ => MeshEffectProfile::None,
4655        };
4656        let effect_key = self.read_string()?;
4657        let technique_name = self.read_string()?;
4658        let (shadow_effect_key, shadow_technique_name) = if version >= 3 {
4659            (self.read_string()?, self.read_string()?)
4660        } else {
4661            let skinned = effect_profile == MeshEffectProfile::SkinnedMesh
4662                || effect_key.starts_with("skinned");
4663            (
4664                mesh_effect_filename_from_runtime_key(
4665                    MeshEffectProfile::ShadowMap,
4666                    MeshRuntimeMaterialKey {
4667                        skinned,
4668                        shadow_map_enable: true,
4669                        ..MeshRuntimeMaterialKey::default()
4670                    },
4671                ),
4672                String::from("tech"),
4673            )
4674        };
4675        Ok(MeshPrimitiveRuntimeDesc {
4676            effect_profile,
4677            effect_key,
4678            technique_name,
4679            shadow_effect_key,
4680            shadow_technique_name,
4681            use_mesh_texture_slot: self.read_bool()?,
4682            use_normal_texture_slot: self.read_bool()?,
4683            use_toon_texture_slot: self.read_bool()?,
4684            use_shadow_texture_slot: self.read_bool()?,
4685            material_key: self.read_mesh_runtime_material_key()?,
4686            vertex_stride_bytes: self.read_u32()?,
4687            vertex_count: self.read_u32()?,
4688            bone_palette_len: self.read_u32()?,
4689        })
4690    }
4691
4692    fn read_mesh_material(&mut self, base_dir: &Path) -> Result<MeshMaterial> {
4693        let diffuse = self.read_vec4()?;
4694        let ambient = self.read_vec4()?;
4695        let specular = self.read_vec4()?;
4696        let emissive = self.read_vec4()?;
4697        let power = self.read_f32()?;
4698        let lighting_type = match self.read_u32()? {
4699            1 => MeshLightingType::Lambert,
4700            2 => MeshLightingType::BlinnPhong,
4701            3 => MeshLightingType::PerPixelBlinnPhong,
4702            4 => MeshLightingType::PerPixelHalfLambert,
4703            5 => MeshLightingType::Toon,
4704            6 => MeshLightingType::FixedFunction,
4705            7 => MeshLightingType::PerPixelFixedFunction,
4706            8 => MeshLightingType::Bump,
4707            9 => MeshLightingType::Parallax,
4708            _ => MeshLightingType::None,
4709        };
4710        let shading_type = match self.read_u32()? {
4711            1 => MeshShadingType::DepthBuffer,
4712            _ => MeshShadingType::None,
4713        };
4714        let shader_option = self.read_u32()?;
4715        let rim_light_color = self.read_vec4()?;
4716        let rim_light_power = self.read_f32()?;
4717        let parallax_max_height = self.read_f32()?;
4718        let alpha_test_enable = self.read_bool()?;
4719        let alpha_ref = self.read_f32()?;
4720        let cull_disable = self.read_bool()?;
4721        let shadow_map_enable = self.read_bool()?;
4722        let use_mesh_tex = self.read_bool()?;
4723        let use_mrbd = self.read_bool()?;
4724        let mrbd = self.read_vec4()?;
4725        let use_rgb = self.read_bool()?;
4726        let rgb_rate = self.read_vec4()?;
4727        let add_rgb = self.read_vec4()?;
4728        let use_mul_vertex_color = self.read_bool()?;
4729        let mul_vertex_color_rate = self.read_f32()?;
4730        let depth_buffer_shadow_bias = self.read_f32()?;
4731        let directional_count = self.read_u32()? as usize;
4732        let mut directional_light_ids = Vec::with_capacity(directional_count);
4733        for _ in 0..directional_count {
4734            directional_light_ids.push(self.read_i32()?);
4735        }
4736        let point_count = self.read_u32()? as usize;
4737        let mut point_light_ids = Vec::with_capacity(point_count);
4738        for _ in 0..point_count {
4739            point_light_ids.push(self.read_i32()?);
4740        }
4741        let spot_count = self.read_u32()? as usize;
4742        let mut spot_light_ids = Vec::with_capacity(spot_count);
4743        for _ in 0..spot_count {
4744            spot_light_ids.push(self.read_i32()?);
4745        }
4746        let normal_texture_path = self.read_opt_path(base_dir)?;
4747        let toon_texture_path = self.read_opt_path(base_dir)?;
4748        let effect_filename = self.read_opt_string()?;
4749        Ok(MeshMaterial {
4750            diffuse,
4751            ambient,
4752            specular,
4753            emissive,
4754            power,
4755            lighting_type,
4756            shading_type,
4757            shader_option,
4758            rim_light_color,
4759            rim_light_power,
4760            parallax_max_height,
4761            alpha_test_enable,
4762            alpha_ref,
4763            cull_disable,
4764            shadow_map_enable,
4765            use_mesh_tex,
4766            use_mrbd,
4767            mrbd,
4768            use_rgb,
4769            rgb_rate,
4770            add_rgb,
4771            use_mul_vertex_color,
4772            mul_vertex_color_rate,
4773            depth_buffer_shadow_bias,
4774            directional_light_ids,
4775            point_light_ids,
4776            spot_light_ids,
4777            normal_texture_path,
4778            toon_texture_path,
4779            effect_filename,
4780        })
4781    }
4782}