1#![allow(clippy::needless_range_loop)]
6use super::functions::escape_json;
7#[allow(unused_imports)]
8use super::functions::*;
9
10#[derive(Debug, Clone, PartialEq)]
12pub enum LightType {
13 Point,
15 Directional,
17 Spot {
19 inner_cone_angle: f32,
21 outer_cone_angle: f32,
23 },
24}
25#[derive(Debug, Clone)]
27pub enum CameraType {
28 Perspective {
30 yfov: f32,
32 znear: f32,
34 zfar: Option<f32>,
36 aspect_ratio: Option<f32>,
38 },
39 Orthographic {
41 xmag: f32,
43 ymag: f32,
45 znear: f32,
47 zfar: f32,
49 },
50}
51#[derive(Debug, Clone)]
53pub struct ValidationIssue {
54 pub message: String,
56 pub is_error: bool,
58}
59impl ValidationIssue {
60 pub(crate) fn error(msg: impl Into<String>) -> Self {
61 ValidationIssue {
62 message: msg.into(),
63 is_error: true,
64 }
65 }
66 pub(crate) fn warning(msg: impl Into<String>) -> Self {
67 ValidationIssue {
68 message: msg.into(),
69 is_error: false,
70 }
71 }
72}
73#[derive(Debug, Clone)]
75pub struct GltfAnimationChannel {
76 pub target: GltfAnimationTarget,
78 pub interpolation: String,
80 pub input_times: Vec<f32>,
82 pub output_values: Vec<f32>,
84}
85#[derive(Debug, Clone)]
87pub struct JointChannel {
88 pub joint_index: usize,
90 pub path: String,
92 pub times: Vec<f32>,
94 pub values: Vec<f32>,
96 pub interpolation: String,
98}
99#[derive(Debug, Clone)]
101pub struct GltfMaterial {
102 pub name: String,
104 pub base_color_factor: [f32; 4],
106 pub metallic_factor: f32,
108 pub roughness_factor: f32,
110 pub emissive_factor: [f32; 3],
112 pub alpha_mode: String,
114 pub alpha_cutoff: f32,
116 pub double_sided: bool,
118}
119impl GltfMaterial {
120 pub fn is_opaque(&self) -> bool {
122 self.alpha_mode == "OPAQUE" && (self.base_color_factor[3] - 1.0).abs() < 1e-6
123 }
124 pub fn is_emissive(&self) -> bool {
126 self.emissive_factor.iter().any(|&v| v > 0.0)
127 }
128}
129#[derive(Debug, Clone)]
131pub struct Joint {
132 pub name: String,
134 pub translation: [f64; 3],
136 pub rotation: [f64; 4],
138 pub scale: [f64; 3],
140 pub children: Vec<usize>,
142 pub inverse_bind_matrix: [f32; 16],
144}
145#[derive(Debug, Clone)]
147pub struct SceneLight {
148 pub name: String,
150 pub light_type: LightType,
152 pub color: [f32; 3],
154 pub intensity: f32,
156}
157impl SceneLight {
158 pub fn type_string(&self) -> &str {
160 match &self.light_type {
161 LightType::Point => "point",
162 LightType::Directional => "directional",
163 LightType::Spot { .. } => "spot",
164 }
165 }
166}
167pub struct GltfPrimitive {
169 pub positions: Vec<[f32; 3]>,
171 pub normals: Vec<[f32; 3]>,
173 pub texcoords: Vec<[f32; 2]>,
175 pub indices: Vec<u32>,
177}
178impl GltfPrimitive {
179 pub fn bounding_box(&self) -> ([f32; 3], [f32; 3]) {
182 let mut min = [f32::INFINITY; 3];
183 let mut max = [f32::NEG_INFINITY; 3];
184 for p in &self.positions {
185 for i in 0..3 {
186 if p[i] < min[i] {
187 min[i] = p[i];
188 }
189 if p[i] > max[i] {
190 max[i] = p[i];
191 }
192 }
193 }
194 (min, max)
195 }
196 pub fn triangle_count(&self) -> usize {
198 self.indices.len() / 3
199 }
200 pub fn vertex_count(&self) -> usize {
202 self.positions.len()
203 }
204 pub fn extract_triangles(&self) -> Vec<[[f32; 3]; 3]> {
206 let mut tris = Vec::new();
207 let mut i = 0;
208 while i + 2 < self.indices.len() {
209 let a = self.indices[i] as usize;
210 let b = self.indices[i + 1] as usize;
211 let c = self.indices[i + 2] as usize;
212 if a < self.positions.len() && b < self.positions.len() && c < self.positions.len() {
213 tris.push([self.positions[a], self.positions[b], self.positions[c]]);
214 }
215 i += 3;
216 }
217 tris
218 }
219}
220pub struct GltfMesh {
222 pub name: String,
224 pub primitives: Vec<GltfPrimitive>,
226}
227impl GltfMesh {
228 pub fn total_vertex_count(&self) -> usize {
230 self.primitives.iter().map(|p| p.vertex_count()).sum()
231 }
232 pub fn total_triangle_count(&self) -> usize {
234 self.primitives.iter().map(|p| p.triangle_count()).sum()
235 }
236}
237#[allow(dead_code)]
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum AccessorType {
241 Scalar,
243 Vec2,
245 Vec3,
247 Vec4,
249 Mat2,
251 Mat3,
253 Mat4,
255}
256impl AccessorType {
257 pub fn num_components(self) -> usize {
259 match self {
260 AccessorType::Scalar => 1,
261 AccessorType::Vec2 => 2,
262 AccessorType::Vec3 => 3,
263 AccessorType::Vec4 => 4,
264 AccessorType::Mat2 => 4,
265 AccessorType::Mat3 => 9,
266 AccessorType::Mat4 => 16,
267 }
268 }
269 pub fn as_str(self) -> &'static str {
271 match self {
272 AccessorType::Scalar => "SCALAR",
273 AccessorType::Vec2 => "VEC2",
274 AccessorType::Vec3 => "VEC3",
275 AccessorType::Vec4 => "VEC4",
276 AccessorType::Mat2 => "MAT2",
277 AccessorType::Mat3 => "MAT3",
278 AccessorType::Mat4 => "MAT4",
279 }
280 }
281}
282#[derive(Debug, Clone)]
284pub struct SkeletonAnimation {
285 pub name: String,
287 pub joint_channels: Vec<JointChannel>,
289}
290impl SkeletonAnimation {
291 pub fn duration(&self) -> f32 {
293 self.joint_channels
294 .iter()
295 .filter_map(|c| c.times.last().copied())
296 .fold(0.0f32, f32::max)
297 }
298 pub fn total_keyframes(&self) -> usize {
300 self.joint_channels.iter().map(|c| c.times.len()).sum()
301 }
302}
303#[derive(Debug, Clone)]
305pub struct GltfBufferView {
306 pub buffer: usize,
308 pub byte_offset: usize,
310 pub byte_length: usize,
312 pub byte_stride: Option<usize>,
314}
315#[allow(dead_code)]
320pub struct TypedAccessor {
321 pub name: String,
323 pub buffer_view_index: usize,
325 pub byte_offset: usize,
327 pub component_type: ComponentType,
329 pub accessor_type: AccessorType,
331 pub count: usize,
333 pub min_values: Vec<f64>,
335 pub max_values: Vec<f64>,
337}
338impl TypedAccessor {
339 #[allow(clippy::too_many_arguments)]
341 pub fn new(
342 name: impl Into<String>,
343 buffer_view_index: usize,
344 byte_offset: usize,
345 component_type: ComponentType,
346 accessor_type: AccessorType,
347 count: usize,
348 ) -> Self {
349 Self {
350 name: name.into(),
351 buffer_view_index,
352 byte_offset,
353 component_type,
354 accessor_type,
355 count,
356 min_values: Vec::new(),
357 max_values: Vec::new(),
358 }
359 }
360 pub fn byte_length(&self) -> usize {
362 self.count * self.accessor_type.num_components() * self.component_type.byte_size()
363 }
364 pub fn to_json(&self) -> String {
366 format!(
367 r#"{{ "bufferView": {bv}, "byteOffset": {bo}, "componentType": {ct}, "type": "{at}", "count": {c} }}"#,
368 bv = self.buffer_view_index,
369 bo = self.byte_offset,
370 ct = self.component_type.component_type_code(),
371 at = self.accessor_type.as_str(),
372 c = self.count,
373 )
374 }
375 pub fn decode_f32(&self, buffer: &[u8]) -> Option<Vec<f32>> {
379 if self.component_type != ComponentType::Float {
380 return None;
381 }
382 let start = self.byte_offset;
383 let len = self.byte_length();
384 let end = start + len;
385 if end > buffer.len() {
386 return None;
387 }
388 let slice = &buffer[start..end];
389 let floats: Vec<f32> = slice
390 .chunks_exact(4)
391 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
392 .collect();
393 Some(floats)
394 }
395 pub fn decode_u32(&self, buffer: &[u8]) -> Option<Vec<u32>> {
397 if self.component_type != ComponentType::UnsignedInt {
398 return None;
399 }
400 let start = self.byte_offset;
401 let len = self.byte_length();
402 let end = start + len;
403 if end > buffer.len() {
404 return None;
405 }
406 let slice = &buffer[start..end];
407 let indices: Vec<u32> = slice
408 .chunks_exact(4)
409 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
410 .collect();
411 Some(indices)
412 }
413}
414#[derive(Debug, Clone)]
416pub struct GltfAnimationTarget {
417 pub node: usize,
419 pub path: String,
421}
422pub struct GltfNode {
424 pub name: String,
426 pub mesh: Option<usize>,
428 pub translation: [f64; 3],
430 pub rotation: [f64; 4],
432 pub scale: [f64; 3],
434 pub children: Vec<usize>,
436}
437#[allow(dead_code)]
439#[derive(Debug, Clone, PartialEq, Eq)]
440pub enum Interpolation {
441 Linear,
443 Step,
445 CubicSpline,
447}
448impl Interpolation {
449 pub fn as_str(&self) -> &'static str {
451 match self {
452 Interpolation::Linear => "LINEAR",
453 Interpolation::Step => "STEP",
454 Interpolation::CubicSpline => "CUBICSPLINE",
455 }
456 }
457}
458pub struct Skeleton {
460 pub joints: Vec<Joint>,
462}
463impl Skeleton {
464 pub fn new() -> Self {
466 Skeleton { joints: Vec::new() }
467 }
468 pub fn add_joint(&mut self, joint: Joint) -> usize {
470 let idx = self.joints.len();
471 self.joints.push(joint);
472 idx
473 }
474 pub fn joint_count(&self) -> usize {
476 self.joints.len()
477 }
478 pub fn export_animation_json(&self, anim: &SkeletonAnimation) -> String {
482 let mut out = String::new();
483 out.push_str("{\n");
484 out.push_str(&format!(" \"name\": \"{}\",\n", escape_json(&anim.name)));
485 out.push_str(" \"channels\": [\n");
486 for (ci, ch) in anim.joint_channels.iter().enumerate() {
487 let joint_name = self
488 .joints
489 .get(ch.joint_index)
490 .map(|j| j.name.as_str())
491 .unwrap_or("unknown");
492 out.push_str(" {\n");
493 out.push_str(&format!(
494 " \"joint\": \"{}\",\n",
495 escape_json(joint_name)
496 ));
497 out.push_str(&format!(" \"path\": \"{}\",\n", escape_json(&ch.path)));
498 out.push_str(&format!(
499 " \"interpolation\": \"{}\",\n",
500 escape_json(&ch.interpolation)
501 ));
502 let times_str: Vec<String> = ch.times.iter().map(|t| format!("{t}")).collect();
503 out.push_str(&format!(" \"times\": [{}]\n", times_str.join(", ")));
504 if ci + 1 < anim.joint_channels.len() {
505 out.push_str(" },\n");
506 } else {
507 out.push_str(" }\n");
508 }
509 }
510 out.push_str(" ]\n");
511 out.push('}');
512 out
513 }
514}
515#[derive(Debug, Clone)]
517pub struct SceneCamera {
518 pub name: String,
520 pub camera_type: CameraType,
522}
523#[allow(dead_code)]
525#[derive(Debug, Clone, Copy, PartialEq, Eq)]
526pub enum ComponentType {
527 UnsignedByte = 5121,
529 UnsignedShort = 5123,
531 UnsignedInt = 5125,
533 Float = 5126,
535}
536impl ComponentType {
537 pub fn byte_size(self) -> usize {
539 match self {
540 ComponentType::UnsignedByte => 1,
541 ComponentType::UnsignedShort => 2,
542 ComponentType::UnsignedInt => 4,
543 ComponentType::Float => 4,
544 }
545 }
546 pub fn component_type_code(self) -> u32 {
548 self as u32
549 }
550}
551#[derive(Debug, Clone)]
555pub struct PbrMaterialBuilder {
556 pub name: String,
558 pub base_color_factor: [f32; 4],
560 pub metallic_factor: f32,
562 pub roughness_factor: f32,
564 pub emissive_factor: [f32; 3],
566 pub double_sided: bool,
568 pub alpha_mode: String,
570 pub alpha_cutoff: f32,
572}
573impl PbrMaterialBuilder {
574 pub fn new(name: impl Into<String>) -> Self {
576 Self {
577 name: name.into(),
578 ..Default::default()
579 }
580 }
581 pub fn base_color(mut self, r: f32, g: f32, b: f32, a: f32) -> Self {
583 self.base_color_factor = [r, g, b, a];
584 self
585 }
586 pub fn metallic_roughness(mut self, metallic: f32, roughness: f32) -> Self {
588 self.metallic_factor = metallic;
589 self.roughness_factor = roughness;
590 self
591 }
592 pub fn emissive(mut self, r: f32, g: f32, b: f32) -> Self {
594 self.emissive_factor = [r, g, b];
595 self
596 }
597 pub fn double_sided(mut self, ds: bool) -> Self {
599 self.double_sided = ds;
600 self
601 }
602 pub fn alpha_mode(mut self, mode: impl Into<String>) -> Self {
604 self.alpha_mode = mode.into();
605 self
606 }
607 pub fn to_json(&self) -> String {
609 let bc = &self.base_color_factor;
610 let em = &self.emissive_factor;
611 format!(
612 r#"{{
613 "name": "{name}",
614 "pbrMetallicRoughness": {{
615 "baseColorFactor": [{r:.6}, {g:.6}, {b:.6}, {a:.6}],
616 "metallicFactor": {mf:.6},
617 "roughnessFactor": {rf:.6}
618 }},
619 "emissiveFactor": [{er:.6}, {eg:.6}, {eb:.6}],
620 "doubleSided": {ds},
621 "alphaMode": "{am}",
622 "alphaCutoff": {ac:.6}
623}}"#,
624 name = self.name,
625 r = bc[0],
626 g = bc[1],
627 b = bc[2],
628 a = bc[3],
629 mf = self.metallic_factor,
630 rf = self.roughness_factor,
631 er = em[0],
632 eg = em[1],
633 eb = em[2],
634 ds = self.double_sided,
635 am = self.alpha_mode,
636 ac = self.alpha_cutoff,
637 )
638 }
639 pub fn build(&self) -> GltfMaterial {
641 GltfMaterial {
642 name: self.name.clone(),
643 base_color_factor: self.base_color_factor,
644 metallic_factor: self.metallic_factor,
645 roughness_factor: self.roughness_factor,
646 emissive_factor: self.emissive_factor,
647 double_sided: self.double_sided,
648 alpha_mode: self.alpha_mode.clone(),
649 alpha_cutoff: self.alpha_cutoff,
650 }
651 }
652}
653#[allow(dead_code)]
655pub struct AnimationChannelBuilder {
656 pub node_index: usize,
658 pub path: String,
660 pub interpolation: Interpolation,
662 pub keyframes: Vec<Keyframe>,
664}
665impl AnimationChannelBuilder {
666 pub fn new(node_index: usize, path: impl Into<String>, interpolation: Interpolation) -> Self {
668 Self {
669 node_index,
670 path: path.into(),
671 interpolation,
672 keyframes: Vec::new(),
673 }
674 }
675 pub fn push(mut self, time: f32, value: Vec<f32>) -> Self {
677 self.keyframes.push(Keyframe::new(time, value));
678 self
679 }
680 pub fn duration(&self) -> f32 {
682 self.keyframes.last().map(|k| k.time).unwrap_or(0.0)
683 }
684 pub fn len(&self) -> usize {
686 self.keyframes.len()
687 }
688 pub fn is_empty(&self) -> bool {
690 self.keyframes.is_empty()
691 }
692 pub fn times(&self) -> Vec<f32> {
694 self.keyframes.iter().map(|k| k.time).collect()
695 }
696 pub fn values_flat(&self) -> Vec<f32> {
698 self.keyframes
699 .iter()
700 .flat_map(|k| k.value.iter().copied())
701 .collect()
702 }
703 pub fn to_json_fragments(
705 &self,
706 sampler_index: usize,
707 times_accessor: usize,
708 values_accessor: usize,
709 ) -> (String, String) {
710 let sampler = format!(
711 r#"{{ "input": {ti}, "interpolation": "{interp}", "output": {vi} }}"#,
712 ti = times_accessor,
713 interp = self.interpolation.as_str(),
714 vi = values_accessor,
715 );
716 let channel = format!(
717 r#"{{ "sampler": {si}, "target": {{ "node": {ni}, "path": "{path}" }} }}"#,
718 si = sampler_index,
719 ni = self.node_index,
720 path = self.path,
721 );
722 (sampler, channel)
723 }
724}
725#[derive(Debug, Clone)]
727pub struct GltfAnimation {
728 pub name: String,
730 pub channels: Vec<GltfAnimationChannel>,
732}
733impl GltfAnimation {
734 pub fn duration(&self) -> f32 {
736 self.channels
737 .iter()
738 .filter_map(|ch| ch.input_times.last().copied())
739 .fold(0.0f32, f32::max)
740 }
741 pub fn total_keyframe_count(&self) -> usize {
743 self.channels.iter().map(|ch| ch.input_times.len()).sum()
744 }
745}
746#[allow(dead_code)]
748#[derive(Debug, Clone)]
749pub struct Keyframe {
750 pub time: f32,
752 pub value: Vec<f32>,
754}
755impl Keyframe {
756 pub fn new(time: f32, value: Vec<f32>) -> Self {
758 Self { time, value }
759 }
760}
761#[derive(Debug, Clone)]
763pub struct GltfAccessor {
764 pub buffer_view: usize,
766 pub byte_offset: usize,
768 pub component_type: u32,
770 pub count: usize,
772 pub element_type: String,
774}
775impl GltfAccessor {
776 pub fn components_per_element(&self) -> usize {
778 match self.element_type.as_str() {
779 "SCALAR" => 1,
780 "VEC2" => 2,
781 "VEC3" => 3,
782 "VEC4" => 4,
783 "MAT2" => 4,
784 "MAT3" => 9,
785 "MAT4" => 16,
786 _ => 1,
787 }
788 }
789 pub fn component_size(&self) -> usize {
791 match self.component_type {
792 5120 => 1,
793 5121 => 1,
794 5122 => 2,
795 5123 => 2,
796 5125 => 4,
797 5126 => 4,
798 _ => 4,
799 }
800 }
801 pub fn byte_length(&self) -> usize {
803 self.count * self.components_per_element() * self.component_size()
804 }
805}
806#[derive(Debug, Clone)]
808pub struct MorphTarget {
809 pub name: String,
811 pub position_deltas: Vec<[f32; 3]>,
813}
814impl MorphTarget {
815 pub fn apply(&self, base_positions: &[[f32; 3]], weight: f32) -> Vec<[f32; 3]> {
819 let n = base_positions.len().min(self.position_deltas.len());
820 let mut out = base_positions.to_vec();
821 for i in 0..n {
822 out[i][0] += self.position_deltas[i][0] * weight;
823 out[i][1] += self.position_deltas[i][1] * weight;
824 out[i][2] += self.position_deltas[i][2] * weight;
825 }
826 out
827 }
828}
829pub struct GltfScene {
831 pub nodes: Vec<GltfNode>,
833 pub meshes: Vec<GltfMesh>,
835 pub accessors: Vec<GltfAccessor>,
837 pub buffer_views: Vec<GltfBufferView>,
839 pub animations: Vec<GltfAnimation>,
841 pub materials: Vec<GltfMaterial>,
843 pub cameras: Vec<SceneCamera>,
845 pub lights: Vec<SceneLight>,
847}
848impl GltfScene {
849 pub fn new() -> Self {
851 GltfScene {
852 nodes: Vec::new(),
853 meshes: Vec::new(),
854 accessors: Vec::new(),
855 buffer_views: Vec::new(),
856 animations: Vec::new(),
857 materials: Vec::new(),
858 cameras: Vec::new(),
859 lights: Vec::new(),
860 }
861 }
862 pub fn add_camera(&mut self, cam: SceneCamera) -> usize {
864 let idx = self.cameras.len();
865 self.cameras.push(cam);
866 idx
867 }
868 pub fn add_light(&mut self, light: SceneLight) -> usize {
870 let idx = self.lights.len();
871 self.lights.push(light);
872 idx
873 }
874 pub fn to_json_with_hierarchy(&self) -> String {
879 let mut out = self.to_json();
880 if out.ends_with('}') {
881 out.pop();
882 }
883 out.push_str(",\n");
884 if !self.cameras.is_empty() {
885 out.push_str(" \"cameras\": [\n");
886 for (ci, cam) in self.cameras.iter().enumerate() {
887 out.push_str(" {\n");
888 out.push_str(&format!(
889 " \"name\": \"{}\",\n",
890 escape_json(&cam.name)
891 ));
892 match &cam.camera_type {
893 CameraType::Perspective {
894 yfov,
895 znear,
896 zfar,
897 aspect_ratio,
898 } => {
899 out.push_str(" \"type\": \"perspective\",\n");
900 out.push_str(" \"perspective\": {\n");
901 out.push_str(&format!(" \"yfov\": {},\n", yfov));
902 out.push_str(&format!(" \"znear\": {}", znear));
903 if let Some(zf) = zfar {
904 out.push_str(&format!(",\n \"zfar\": {}", zf));
905 }
906 if let Some(ar) = aspect_ratio {
907 out.push_str(&format!(",\n \"aspectRatio\": {}", ar));
908 }
909 out.push_str("\n }\n");
910 }
911 CameraType::Orthographic {
912 xmag,
913 ymag,
914 znear,
915 zfar,
916 } => {
917 out.push_str(" \"type\": \"orthographic\",\n");
918 out.push_str(" \"orthographic\": {\n");
919 out.push_str(&format!(" \"xmag\": {},\n", xmag));
920 out.push_str(&format!(" \"ymag\": {},\n", ymag));
921 out.push_str(&format!(" \"znear\": {},\n", znear));
922 out.push_str(&format!(" \"zfar\": {}\n", zfar));
923 out.push_str(" }\n");
924 }
925 }
926 if ci + 1 < self.cameras.len() {
927 out.push_str(" },\n");
928 } else {
929 out.push_str(" }\n");
930 }
931 }
932 out.push_str(" ],\n");
933 }
934 if !self.lights.is_empty() {
935 out.push_str(" \"extensions\": {\n");
936 out.push_str(" \"KHR_lights_punctual\": {\n");
937 out.push_str(" \"lights\": [\n");
938 for (li, light) in self.lights.iter().enumerate() {
939 out.push_str(" {\n");
940 out.push_str(&format!(
941 " \"name\": \"{}\",\n",
942 escape_json(&light.name)
943 ));
944 out.push_str(&format!(
945 " \"type\": \"{}\",\n",
946 light.type_string()
947 ));
948 out.push_str(&format!(
949 " \"color\": [{}, {}, {}],\n",
950 light.color[0], light.color[1], light.color[2]
951 ));
952 out.push_str(&format!(" \"intensity\": {}", light.intensity));
953 if let LightType::Spot {
954 inner_cone_angle,
955 outer_cone_angle,
956 } = &light.light_type
957 {
958 out.push_str(
959 &format!(
960 ",\n \"spot\": {{ \"innerConeAngle\": {}, \"outerConeAngle\": {} }}",
961 inner_cone_angle, outer_cone_angle
962 ),
963 );
964 }
965 out.push('\n');
966 if li + 1 < self.lights.len() {
967 out.push_str(" },\n");
968 } else {
969 out.push_str(" }\n");
970 }
971 }
972 out.push_str(" ]\n");
973 out.push_str(" }\n");
974 out.push_str(" }\n");
975 } else if out.ends_with(",\n") {
976 out.truncate(out.len() - 2);
977 out.push('\n');
978 }
979 out.push('}');
980 out
981 }
982 pub fn add_mesh(&mut self, mesh: GltfMesh) -> usize {
984 let idx = self.meshes.len();
985 self.meshes.push(mesh);
986 idx
987 }
988 pub fn add_node(&mut self, node: GltfNode) {
990 self.nodes.push(node);
991 }
992 pub fn mesh_count(&self) -> usize {
994 self.meshes.len()
995 }
996 pub fn node_count(&self) -> usize {
998 self.nodes.len()
999 }
1000 pub fn traverse_depth_first<F>(&self, mut visitor: F)
1003 where
1004 F: FnMut(usize, usize, [f64; 3]),
1005 {
1006 let mut is_child = vec![false; self.nodes.len()];
1007 for node in &self.nodes {
1008 for &child_idx in &node.children {
1009 if child_idx < is_child.len() {
1010 is_child[child_idx] = true;
1011 }
1012 }
1013 }
1014 let mut stack: Vec<(usize, usize, [f64; 3])> = Vec::new();
1015 for i in (0..self.nodes.len()).rev() {
1016 if !is_child[i] {
1017 stack.push((i, 0, [0.0, 0.0, 0.0]));
1018 }
1019 }
1020 while let Some((idx, depth, parent_translation)) = stack.pop() {
1021 if idx >= self.nodes.len() {
1022 continue;
1023 }
1024 let node = &self.nodes[idx];
1025 let accumulated = [
1026 parent_translation[0] + node.translation[0],
1027 parent_translation[1] + node.translation[1],
1028 parent_translation[2] + node.translation[2],
1029 ];
1030 visitor(idx, depth, accumulated);
1031 for &child_idx in node.children.iter().rev() {
1032 stack.push((child_idx, depth + 1, accumulated));
1033 }
1034 }
1035 }
1036 pub fn collect_mesh_primitives(&self) -> Vec<(&str, &GltfPrimitive)> {
1038 let mut result = Vec::new();
1039 for node in &self.nodes {
1040 if let Some(mesh_idx) = node.mesh
1041 && let Some(mesh) = self.meshes.get(mesh_idx)
1042 {
1043 for prim in &mesh.primitives {
1044 result.push((node.name.as_str(), prim));
1045 }
1046 }
1047 }
1048 result
1049 }
1050 pub fn add_material(&mut self, mat: GltfMaterial) -> usize {
1052 let idx = self.materials.len();
1053 self.materials.push(mat);
1054 idx
1055 }
1056 pub fn add_animation(&mut self, anim: GltfAnimation) -> usize {
1058 let idx = self.animations.len();
1059 self.animations.push(anim);
1060 idx
1061 }
1062 pub fn nodes_using_mesh(&self, mesh_idx: usize) -> Vec<usize> {
1064 self.nodes
1065 .iter()
1066 .enumerate()
1067 .filter_map(|(i, n)| {
1068 if n.mesh == Some(mesh_idx) {
1069 Some(i)
1070 } else {
1071 None
1072 }
1073 })
1074 .collect()
1075 }
1076 pub fn total_vertex_count(&self) -> usize {
1078 self.meshes.iter().map(|m| m.total_vertex_count()).sum()
1079 }
1080 pub fn total_triangle_count(&self) -> usize {
1082 self.meshes.iter().map(|m| m.total_triangle_count()).sum()
1083 }
1084 pub fn to_json(&self) -> String {
1088 let mut out = String::new();
1089 out.push_str("{\n");
1090 out.push_str(" \"asset\": {\n");
1091 out.push_str(" \"version\": \"2.0\",\n");
1092 out.push_str(" \"generator\": \"OxiPhysics glTF writer\"\n");
1093 out.push_str(" },\n");
1094 out.push_str(" \"scene\": 0,\n");
1095 let node_indices: Vec<String> = (0..self.nodes.len()).map(|i| i.to_string()).collect();
1096 out.push_str(" \"scenes\": [\n {\n \"nodes\": [");
1097 out.push_str(&node_indices.join(", "));
1098 out.push_str("]\n }\n ],\n");
1099 out.push_str(" \"nodes\": [\n");
1100 for (i, node) in self.nodes.iter().enumerate() {
1101 out.push_str(" {\n");
1102 out.push_str(&format!(
1103 " \"name\": \"{}\",\n",
1104 escape_json(&node.name)
1105 ));
1106 if let Some(mesh_idx) = node.mesh {
1107 out.push_str(&format!(" \"mesh\": {},\n", mesh_idx));
1108 }
1109 if !node.children.is_empty() {
1110 let children_str: Vec<String> =
1111 node.children.iter().map(|c| c.to_string()).collect();
1112 out.push_str(&format!(
1113 " \"children\": [{}],\n",
1114 children_str.join(", ")
1115 ));
1116 }
1117 out.push_str(&format!(
1118 " \"translation\": [{}, {}, {}],\n",
1119 node.translation[0], node.translation[1], node.translation[2]
1120 ));
1121 out.push_str(&format!(
1122 " \"rotation\": [{}, {}, {}, {}],\n",
1123 node.rotation[0], node.rotation[1], node.rotation[2], node.rotation[3]
1124 ));
1125 out.push_str(&format!(
1126 " \"scale\": [{}, {}, {}]\n",
1127 node.scale[0], node.scale[1], node.scale[2]
1128 ));
1129 if i + 1 < self.nodes.len() {
1130 out.push_str(" },\n");
1131 } else {
1132 out.push_str(" }\n");
1133 }
1134 }
1135 out.push_str(" ],\n");
1136 out.push_str(" \"meshes\": [\n");
1137 for (mi, mesh) in self.meshes.iter().enumerate() {
1138 out.push_str(" {\n");
1139 out.push_str(&format!(
1140 " \"name\": \"{}\",\n",
1141 escape_json(&mesh.name)
1142 ));
1143 out.push_str(" \"primitives\": [\n");
1144 for (pi, _prim) in mesh.primitives.iter().enumerate() {
1145 let pos_acc = pi * 3;
1146 let nrm_acc = pi * 3 + 1;
1147 let idx_acc = pi * 3 + 2;
1148 out.push_str(" {\n");
1149 out.push_str(" \"attributes\": {\n");
1150 out.push_str(&format!(" \"POSITION\": {},\n", pos_acc));
1151 out.push_str(&format!(" \"NORMAL\": {}\n", nrm_acc));
1152 out.push_str(" },\n");
1153 out.push_str(&format!(" \"indices\": {}\n", idx_acc));
1154 if pi + 1 < mesh.primitives.len() {
1155 out.push_str(" },\n");
1156 } else {
1157 out.push_str(" }\n");
1158 }
1159 }
1160 out.push_str(" ]\n");
1161 if mi + 1 < self.meshes.len() {
1162 out.push_str(" },\n");
1163 } else {
1164 out.push_str(" }\n");
1165 }
1166 }
1167 out.push_str(" ],\n");
1168 if !self.materials.is_empty() {
1169 out.push_str(" \"materials\": [\n");
1170 for (mi, mat) in self.materials.iter().enumerate() {
1171 out.push_str(" {\n");
1172 out.push_str(&format!(
1173 " \"name\": \"{}\",\n",
1174 escape_json(&mat.name)
1175 ));
1176 out.push_str(" \"pbrMetallicRoughness\": {\n");
1177 out.push_str(&format!(
1178 " \"baseColorFactor\": [{}, {}, {}, {}],\n",
1179 mat.base_color_factor[0],
1180 mat.base_color_factor[1],
1181 mat.base_color_factor[2],
1182 mat.base_color_factor[3]
1183 ));
1184 out.push_str(&format!(
1185 " \"metallicFactor\": {},\n",
1186 mat.metallic_factor
1187 ));
1188 out.push_str(&format!(
1189 " \"roughnessFactor\": {}\n",
1190 mat.roughness_factor
1191 ));
1192 out.push_str(" },\n");
1193 out.push_str(&format!(
1194 " \"emissiveFactor\": [{}, {}, {}],\n",
1195 mat.emissive_factor[0], mat.emissive_factor[1], mat.emissive_factor[2]
1196 ));
1197 out.push_str(&format!(" \"alphaMode\": \"{}\",\n", mat.alpha_mode));
1198 out.push_str(&format!(" \"doubleSided\": {}\n", mat.double_sided));
1199 if mi + 1 < self.materials.len() {
1200 out.push_str(" },\n");
1201 } else {
1202 out.push_str(" }\n");
1203 }
1204 }
1205 out.push_str(" ],\n");
1206 }
1207 out.push_str(" \"accessors\": [],\n");
1208 out.push_str(" \"bufferViews\": [],\n");
1209 out.push_str(" \"buffers\": []\n");
1210 out.push('}');
1211 out
1212 }
1213}
1214pub struct MorphPrimitive {
1216 pub base: GltfPrimitive,
1218 pub targets: Vec<MorphTarget>,
1220 pub weights: Vec<f32>,
1222}
1223impl MorphPrimitive {
1224 pub fn set_weight(&mut self, target_idx: usize, weight: f32) {
1226 if target_idx < self.weights.len() {
1227 self.weights[target_idx] = weight.clamp(0.0, 1.0);
1228 }
1229 }
1230 pub fn blend(&self) -> Vec<[f32; 3]> {
1232 let mut result = self.base.positions.clone();
1233 for (target, &w) in self.targets.iter().zip(self.weights.iter()) {
1234 let n = result.len().min(target.position_deltas.len());
1235 for i in 0..n {
1236 result[i][0] += target.position_deltas[i][0] * w;
1237 result[i][1] += target.position_deltas[i][1] * w;
1238 result[i][2] += target.position_deltas[i][2] * w;
1239 }
1240 }
1241 result
1242 }
1243}
1244pub struct GlbWriter {
1251 pub include_empty_bin: bool,
1253}
1254impl GlbWriter {
1255 pub fn new() -> Self {
1257 GlbWriter {
1258 include_empty_bin: false,
1259 }
1260 }
1261
1262 pub fn write(&self, scene: &GltfScene) -> Vec<u8> {
1266 let json = scene.to_json();
1267 let json_bytes = json.as_bytes();
1268 let json_len = json_bytes.len();
1269 let json_padded_len = (json_len + 3) & !3;
1270 let json_padding = json_padded_len - json_len;
1271 let chunk_header_size = 8usize;
1272 let header_size = 12usize;
1273 let total_len = header_size
1274 + chunk_header_size
1275 + json_padded_len
1276 + if self.include_empty_bin {
1277 chunk_header_size
1278 } else {
1279 0
1280 };
1281 let mut out = Vec::with_capacity(total_len);
1282 out.extend_from_slice(b"glTF");
1283 out.extend_from_slice(&2u32.to_le_bytes());
1284 out.extend_from_slice(&(total_len as u32).to_le_bytes());
1285 out.extend_from_slice(&(json_padded_len as u32).to_le_bytes());
1286 out.extend_from_slice(&0x4E4F534Au32.to_le_bytes());
1287 out.extend_from_slice(json_bytes);
1288 #[allow(clippy::same_item_push)]
1289 for _ in 0..json_padding {
1290 out.push(0x20);
1291 }
1292 if self.include_empty_bin {
1293 out.extend_from_slice(&0u32.to_le_bytes());
1294 out.extend_from_slice(&0x004E4942u32.to_le_bytes());
1295 }
1296 out
1297 }
1298
1299 pub fn write_glb(&self, scene: &GltfScene) -> Vec<u8> {
1314 let primitives: Vec<&GltfPrimitive> = scene
1316 .meshes
1317 .iter()
1318 .flat_map(|m| m.primitives.iter())
1319 .collect();
1320
1321 if primitives.is_empty() {
1322 return Vec::new();
1323 }
1324
1325 let mut bin_buf: Vec<u8> = Vec::new();
1327 let mut prim_meta: Vec<PrimMeta> = Vec::with_capacity(primitives.len());
1328
1329 for prim in &primitives {
1330 let n_verts = prim.positions.len();
1331 let n_indices = prim.indices.len();
1332
1333 let mut pos_min = [f32::INFINITY; 3];
1335 let mut pos_max = [f32::NEG_INFINITY; 3];
1336 for p in &prim.positions {
1337 for i in 0..3 {
1338 if p[i] < pos_min[i] {
1339 pos_min[i] = p[i];
1340 }
1341 if p[i] > pos_max[i] {
1342 pos_max[i] = p[i];
1343 }
1344 }
1345 }
1346 if n_verts == 0 {
1348 pos_min = [0.0; 3];
1349 pos_max = [0.0; 3];
1350 }
1351
1352 let pos_byte_offset = bin_buf.len();
1353 for p in &prim.positions {
1354 for &v in p {
1355 bin_buf.extend_from_slice(&v.to_le_bytes());
1356 }
1357 }
1358
1359 let norm_byte_offset = bin_buf.len();
1360 for n in &prim.normals {
1361 for &v in n {
1362 bin_buf.extend_from_slice(&v.to_le_bytes());
1363 }
1364 }
1365 let norm_count = prim.normals.len();
1367 for _ in norm_count..n_verts {
1368 bin_buf.extend_from_slice(&[0u8; 12]);
1369 }
1370
1371 let uv_byte_offset = bin_buf.len();
1372 if prim.texcoords.is_empty() {
1373 for _ in 0..n_verts {
1375 bin_buf.extend_from_slice(&[0u8; 8]);
1376 }
1377 } else {
1378 for uv in &prim.texcoords {
1379 for &v in uv {
1380 bin_buf.extend_from_slice(&v.to_le_bytes());
1381 }
1382 }
1383 let uv_count = prim.texcoords.len();
1385 for _ in uv_count..n_verts {
1386 bin_buf.extend_from_slice(&[0u8; 8]);
1387 }
1388 }
1389
1390 let pad_to_align = (4 - bin_buf.len() % 4) % 4;
1392 bin_buf.extend(std::iter::repeat_n(0u8, pad_to_align));
1393 let idx_byte_offset = bin_buf.len();
1394 for &idx in &prim.indices {
1395 bin_buf.extend_from_slice(&idx.to_le_bytes());
1396 }
1397
1398 prim_meta.push(PrimMeta {
1399 n_verts,
1400 n_indices,
1401 pos_byte_offset,
1402 norm_byte_offset,
1403 uv_byte_offset,
1404 idx_byte_offset,
1405 pos_min,
1406 pos_max,
1407 });
1408 }
1409
1410 let bin_tail_pad = (4 - bin_buf.len() % 4) % 4;
1412 bin_buf.extend(std::iter::repeat_n(0u8, bin_tail_pad));
1413 let total_bin_len = bin_buf.len();
1414
1415 let json = Self::build_glb_json(scene, &prim_meta, total_bin_len);
1417 let json_bytes = json.as_bytes();
1418 let json_raw_len = json_bytes.len();
1419 let json_padded_len = (json_raw_len + 3) & !3;
1420 let json_padding = json_padded_len - json_raw_len;
1421
1422 let total_len = 12 + 8 + json_padded_len + 8 + total_bin_len;
1424
1425 let mut out = Vec::with_capacity(total_len);
1426 out.extend_from_slice(b"glTF");
1428 out.extend_from_slice(&2u32.to_le_bytes());
1429 out.extend_from_slice(&(total_len as u32).to_le_bytes());
1430 out.extend_from_slice(&(json_padded_len as u32).to_le_bytes());
1432 out.extend_from_slice(&0x4E4F534Au32.to_le_bytes()); out.extend_from_slice(json_bytes);
1434 out.extend(std::iter::repeat_n(0x20u8, json_padding));
1435 out.extend_from_slice(&(total_bin_len as u32).to_le_bytes());
1437 out.extend_from_slice(&0x004E4942u32.to_le_bytes()); out.extend_from_slice(&bin_buf);
1439
1440 out
1441 }
1442
1443 fn build_glb_json(scene: &GltfScene, prim_meta: &[PrimMeta], total_bin_len: usize) -> String {
1445 use std::fmt::Write as FmtWrite;
1446 let mut out = String::new();
1447
1448 let _ = writeln!(out, "{{");
1449 let _ = writeln!(
1450 out,
1451 " \"asset\": {{\"version\": \"2.0\", \"generator\": \"OxiPhysics glTF writer\"}},"
1452 );
1453 let _ = writeln!(out, " \"scene\": 0,");
1454
1455 let node_indices: Vec<String> = (0..scene.nodes.len()).map(|i| i.to_string()).collect();
1457 let _ = writeln!(
1458 out,
1459 " \"scenes\": [{{\"nodes\": [{}]}}],",
1460 node_indices.join(", ")
1461 );
1462
1463 let _ = writeln!(out, " \"nodes\": [");
1465 for (i, node) in scene.nodes.iter().enumerate() {
1466 let mesh_str = node
1467 .mesh
1468 .map(|m| format!(", \"mesh\": {m}"))
1469 .unwrap_or_default();
1470 let comma = if i + 1 < scene.nodes.len() { "," } else { "" };
1471 let _ = writeln!(
1472 out,
1473 " {{\"name\": \"{}\"{mesh_str}}}{comma}",
1474 escape_json(&node.name)
1475 );
1476 }
1477 let _ = writeln!(out, " ],");
1478
1479 let _ = writeln!(out, " \"meshes\": [");
1481 let mut acc_idx = 0usize;
1482 for (mi, mesh) in scene.meshes.iter().enumerate() {
1483 let _ = writeln!(
1484 out,
1485 " {{\"name\": \"{}\", \"primitives\": [",
1486 escape_json(&mesh.name)
1487 );
1488 for (pi, _prim) in mesh.primitives.iter().enumerate() {
1489 let pos_acc = acc_idx;
1490 let nrm_acc = acc_idx + 1;
1491 let uv_acc = acc_idx + 2;
1492 let idx_acc = acc_idx + 3;
1493 acc_idx += 4;
1494 let prim_comma = if pi + 1 < mesh.primitives.len() {
1495 ","
1496 } else {
1497 ""
1498 };
1499 let _ = writeln!(
1500 out,
1501 " {{\"attributes\": {{\"POSITION\": {pos_acc}, \"NORMAL\": {nrm_acc}, \"TEXCOORD_0\": {uv_acc}}}, \"indices\": {idx_acc}}}{prim_comma}"
1502 );
1503 }
1504 let mesh_comma = if mi + 1 < scene.meshes.len() { "," } else { "" };
1505 let _ = writeln!(out, " ]}}{mesh_comma}");
1506 }
1507 let _ = writeln!(out, " ],");
1508
1509 let _ = writeln!(out, " \"buffers\": [{{\"byteLength\": {total_bin_len}}}],");
1511
1512 let n_meta = prim_meta.len();
1514 let total_bv = n_meta * 4;
1515 let mut bv_entries: Vec<String> = Vec::with_capacity(total_bv);
1516 for meta in prim_meta.iter() {
1517 let pos_size = meta.n_verts * 12; let norm_size = meta.n_verts * 12;
1519 let uv_size = meta.n_verts * 8; let idx_size = meta.n_indices * 4; bv_entries.push(format!(
1522 " {{\"buffer\": 0, \"byteOffset\": {}, \"byteLength\": {pos_size}, \"target\": 34962}}",
1523 meta.pos_byte_offset
1524 ));
1525 bv_entries.push(format!(
1526 " {{\"buffer\": 0, \"byteOffset\": {}, \"byteLength\": {norm_size}, \"target\": 34962}}",
1527 meta.norm_byte_offset
1528 ));
1529 bv_entries.push(format!(
1530 " {{\"buffer\": 0, \"byteOffset\": {}, \"byteLength\": {uv_size}, \"target\": 34962}}",
1531 meta.uv_byte_offset
1532 ));
1533 bv_entries.push(format!(
1534 " {{\"buffer\": 0, \"byteOffset\": {}, \"byteLength\": {idx_size}, \"target\": 34963}}",
1535 meta.idx_byte_offset
1536 ));
1537 }
1538 let _ = writeln!(out, " \"bufferViews\": [");
1539 let _ = writeln!(out, "{}", bv_entries.join(",\n"));
1540 let _ = writeln!(out, " ],");
1541
1542 let total_acc = n_meta * 4;
1544 let mut acc_entries: Vec<String> = Vec::with_capacity(total_acc);
1545 let mut bv_idx = 0usize;
1546 for meta in prim_meta.iter() {
1547 let pos_min = meta.pos_min;
1548 let pos_max = meta.pos_max;
1549 acc_entries.push(format!(
1551 " {{\"bufferView\": {bv_idx}, \"componentType\": 5126, \"count\": {}, \"type\": \"VEC3\", \
1552\"min\": [{}, {}, {}], \"max\": [{}, {}, {}]}}",
1553 meta.n_verts,
1554 pos_min[0], pos_min[1], pos_min[2],
1555 pos_max[0], pos_max[1], pos_max[2]
1556 ));
1557 bv_idx += 1;
1558 acc_entries.push(format!(
1560 " {{\"bufferView\": {bv_idx}, \"componentType\": 5126, \"count\": {}, \"type\": \"VEC3\"}}",
1561 meta.n_verts
1562 ));
1563 bv_idx += 1;
1564 acc_entries.push(format!(
1566 " {{\"bufferView\": {bv_idx}, \"componentType\": 5126, \"count\": {}, \"type\": \"VEC2\"}}",
1567 meta.n_verts
1568 ));
1569 bv_idx += 1;
1570 acc_entries.push(format!(
1572 " {{\"bufferView\": {bv_idx}, \"componentType\": 5125, \"count\": {}, \"type\": \"SCALAR\"}}",
1573 meta.n_indices
1574 ));
1575 bv_idx += 1;
1576 }
1577 let _ = writeln!(out, " \"accessors\": [");
1578 let _ = writeln!(out, "{}", acc_entries.join(",\n"));
1579 let _ = writeln!(out, " ]");
1580
1581 let _ = write!(out, "}}");
1582 out
1583 }
1584}
1585
1586struct PrimMeta {
1588 n_verts: usize,
1589 n_indices: usize,
1590 pos_byte_offset: usize,
1591 norm_byte_offset: usize,
1592 uv_byte_offset: usize,
1593 idx_byte_offset: usize,
1594 pos_min: [f32; 3],
1595 pos_max: [f32; 3],
1596}