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