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 indices: Vec<u32>,
175}
176impl GltfPrimitive {
177 pub fn bounding_box(&self) -> ([f32; 3], [f32; 3]) {
180 let mut min = [f32::INFINITY; 3];
181 let mut max = [f32::NEG_INFINITY; 3];
182 for p in &self.positions {
183 for i in 0..3 {
184 if p[i] < min[i] {
185 min[i] = p[i];
186 }
187 if p[i] > max[i] {
188 max[i] = p[i];
189 }
190 }
191 }
192 (min, max)
193 }
194 pub fn triangle_count(&self) -> usize {
196 self.indices.len() / 3
197 }
198 pub fn vertex_count(&self) -> usize {
200 self.positions.len()
201 }
202 pub fn extract_triangles(&self) -> Vec<[[f32; 3]; 3]> {
204 let mut tris = Vec::new();
205 let mut i = 0;
206 while i + 2 < self.indices.len() {
207 let a = self.indices[i] as usize;
208 let b = self.indices[i + 1] as usize;
209 let c = self.indices[i + 2] as usize;
210 if a < self.positions.len() && b < self.positions.len() && c < self.positions.len() {
211 tris.push([self.positions[a], self.positions[b], self.positions[c]]);
212 }
213 i += 3;
214 }
215 tris
216 }
217}
218pub struct GltfMesh {
220 pub name: String,
222 pub primitives: Vec<GltfPrimitive>,
224}
225impl GltfMesh {
226 pub fn total_vertex_count(&self) -> usize {
228 self.primitives.iter().map(|p| p.vertex_count()).sum()
229 }
230 pub fn total_triangle_count(&self) -> usize {
232 self.primitives.iter().map(|p| p.triangle_count()).sum()
233 }
234}
235#[allow(dead_code)]
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub enum AccessorType {
239 Scalar,
241 Vec2,
243 Vec3,
245 Vec4,
247 Mat2,
249 Mat3,
251 Mat4,
253}
254impl AccessorType {
255 pub fn num_components(self) -> usize {
257 match self {
258 AccessorType::Scalar => 1,
259 AccessorType::Vec2 => 2,
260 AccessorType::Vec3 => 3,
261 AccessorType::Vec4 => 4,
262 AccessorType::Mat2 => 4,
263 AccessorType::Mat3 => 9,
264 AccessorType::Mat4 => 16,
265 }
266 }
267 pub fn as_str(self) -> &'static str {
269 match self {
270 AccessorType::Scalar => "SCALAR",
271 AccessorType::Vec2 => "VEC2",
272 AccessorType::Vec3 => "VEC3",
273 AccessorType::Vec4 => "VEC4",
274 AccessorType::Mat2 => "MAT2",
275 AccessorType::Mat3 => "MAT3",
276 AccessorType::Mat4 => "MAT4",
277 }
278 }
279}
280#[derive(Debug, Clone)]
282pub struct SkeletonAnimation {
283 pub name: String,
285 pub joint_channels: Vec<JointChannel>,
287}
288impl SkeletonAnimation {
289 pub fn duration(&self) -> f32 {
291 self.joint_channels
292 .iter()
293 .filter_map(|c| c.times.last().copied())
294 .fold(0.0f32, f32::max)
295 }
296 pub fn total_keyframes(&self) -> usize {
298 self.joint_channels.iter().map(|c| c.times.len()).sum()
299 }
300}
301#[derive(Debug, Clone)]
303pub struct GltfBufferView {
304 pub buffer: usize,
306 pub byte_offset: usize,
308 pub byte_length: usize,
310 pub byte_stride: Option<usize>,
312}
313#[allow(dead_code)]
318pub struct TypedAccessor {
319 pub name: String,
321 pub buffer_view_index: usize,
323 pub byte_offset: usize,
325 pub component_type: ComponentType,
327 pub accessor_type: AccessorType,
329 pub count: usize,
331 pub min_values: Vec<f64>,
333 pub max_values: Vec<f64>,
335}
336impl TypedAccessor {
337 #[allow(clippy::too_many_arguments)]
339 pub fn new(
340 name: impl Into<String>,
341 buffer_view_index: usize,
342 byte_offset: usize,
343 component_type: ComponentType,
344 accessor_type: AccessorType,
345 count: usize,
346 ) -> Self {
347 Self {
348 name: name.into(),
349 buffer_view_index,
350 byte_offset,
351 component_type,
352 accessor_type,
353 count,
354 min_values: Vec::new(),
355 max_values: Vec::new(),
356 }
357 }
358 pub fn byte_length(&self) -> usize {
360 self.count * self.accessor_type.num_components() * self.component_type.byte_size()
361 }
362 pub fn to_json(&self) -> String {
364 format!(
365 r#"{{ "bufferView": {bv}, "byteOffset": {bo}, "componentType": {ct}, "type": "{at}", "count": {c} }}"#,
366 bv = self.buffer_view_index,
367 bo = self.byte_offset,
368 ct = self.component_type.component_type_code(),
369 at = self.accessor_type.as_str(),
370 c = self.count,
371 )
372 }
373 pub fn decode_f32(&self, buffer: &[u8]) -> Option<Vec<f32>> {
377 if self.component_type != ComponentType::Float {
378 return None;
379 }
380 let start = self.byte_offset;
381 let len = self.byte_length();
382 let end = start + len;
383 if end > buffer.len() {
384 return None;
385 }
386 let slice = &buffer[start..end];
387 let floats: Vec<f32> = slice
388 .chunks_exact(4)
389 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
390 .collect();
391 Some(floats)
392 }
393 pub fn decode_u32(&self, buffer: &[u8]) -> Option<Vec<u32>> {
395 if self.component_type != ComponentType::UnsignedInt {
396 return None;
397 }
398 let start = self.byte_offset;
399 let len = self.byte_length();
400 let end = start + len;
401 if end > buffer.len() {
402 return None;
403 }
404 let slice = &buffer[start..end];
405 let indices: Vec<u32> = slice
406 .chunks_exact(4)
407 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
408 .collect();
409 Some(indices)
410 }
411}
412#[derive(Debug, Clone)]
414pub struct GltfAnimationTarget {
415 pub node: usize,
417 pub path: String,
419}
420pub struct GltfNode {
422 pub name: String,
424 pub mesh: Option<usize>,
426 pub translation: [f64; 3],
428 pub rotation: [f64; 4],
430 pub scale: [f64; 3],
432 pub children: Vec<usize>,
434}
435#[allow(dead_code)]
437#[derive(Debug, Clone, PartialEq, Eq)]
438pub enum Interpolation {
439 Linear,
441 Step,
443 CubicSpline,
445}
446impl Interpolation {
447 pub fn as_str(&self) -> &'static str {
449 match self {
450 Interpolation::Linear => "LINEAR",
451 Interpolation::Step => "STEP",
452 Interpolation::CubicSpline => "CUBICSPLINE",
453 }
454 }
455}
456pub struct Skeleton {
458 pub joints: Vec<Joint>,
460}
461impl Skeleton {
462 pub fn new() -> Self {
464 Skeleton { joints: Vec::new() }
465 }
466 pub fn add_joint(&mut self, joint: Joint) -> usize {
468 let idx = self.joints.len();
469 self.joints.push(joint);
470 idx
471 }
472 pub fn joint_count(&self) -> usize {
474 self.joints.len()
475 }
476 pub fn export_animation_json(&self, anim: &SkeletonAnimation) -> String {
480 let mut out = String::new();
481 out.push_str("{\n");
482 out.push_str(&format!(" \"name\": \"{}\",\n", escape_json(&anim.name)));
483 out.push_str(" \"channels\": [\n");
484 for (ci, ch) in anim.joint_channels.iter().enumerate() {
485 let joint_name = self
486 .joints
487 .get(ch.joint_index)
488 .map(|j| j.name.as_str())
489 .unwrap_or("unknown");
490 out.push_str(" {\n");
491 out.push_str(&format!(
492 " \"joint\": \"{}\",\n",
493 escape_json(joint_name)
494 ));
495 out.push_str(&format!(" \"path\": \"{}\",\n", escape_json(&ch.path)));
496 out.push_str(&format!(
497 " \"interpolation\": \"{}\",\n",
498 escape_json(&ch.interpolation)
499 ));
500 let times_str: Vec<String> = ch.times.iter().map(|t| format!("{t}")).collect();
501 out.push_str(&format!(" \"times\": [{}]\n", times_str.join(", ")));
502 if ci + 1 < anim.joint_channels.len() {
503 out.push_str(" },\n");
504 } else {
505 out.push_str(" }\n");
506 }
507 }
508 out.push_str(" ]\n");
509 out.push('}');
510 out
511 }
512}
513#[derive(Debug, Clone)]
515pub struct SceneCamera {
516 pub name: String,
518 pub camera_type: CameraType,
520}
521#[allow(dead_code)]
523#[derive(Debug, Clone, Copy, PartialEq, Eq)]
524pub enum ComponentType {
525 UnsignedByte = 5121,
527 UnsignedShort = 5123,
529 UnsignedInt = 5125,
531 Float = 5126,
533}
534impl ComponentType {
535 pub fn byte_size(self) -> usize {
537 match self {
538 ComponentType::UnsignedByte => 1,
539 ComponentType::UnsignedShort => 2,
540 ComponentType::UnsignedInt => 4,
541 ComponentType::Float => 4,
542 }
543 }
544 pub fn component_type_code(self) -> u32 {
546 self as u32
547 }
548}
549#[derive(Debug, Clone)]
553pub struct PbrMaterialBuilder {
554 pub name: String,
556 pub base_color_factor: [f32; 4],
558 pub metallic_factor: f32,
560 pub roughness_factor: f32,
562 pub emissive_factor: [f32; 3],
564 pub double_sided: bool,
566 pub alpha_mode: String,
568 pub alpha_cutoff: f32,
570}
571impl PbrMaterialBuilder {
572 pub fn new(name: impl Into<String>) -> Self {
574 Self {
575 name: name.into(),
576 ..Default::default()
577 }
578 }
579 pub fn base_color(mut self, r: f32, g: f32, b: f32, a: f32) -> Self {
581 self.base_color_factor = [r, g, b, a];
582 self
583 }
584 pub fn metallic_roughness(mut self, metallic: f32, roughness: f32) -> Self {
586 self.metallic_factor = metallic;
587 self.roughness_factor = roughness;
588 self
589 }
590 pub fn emissive(mut self, r: f32, g: f32, b: f32) -> Self {
592 self.emissive_factor = [r, g, b];
593 self
594 }
595 pub fn double_sided(mut self, ds: bool) -> Self {
597 self.double_sided = ds;
598 self
599 }
600 pub fn alpha_mode(mut self, mode: impl Into<String>) -> Self {
602 self.alpha_mode = mode.into();
603 self
604 }
605 pub fn to_json(&self) -> String {
607 let bc = &self.base_color_factor;
608 let em = &self.emissive_factor;
609 format!(
610 r#"{{
611 "name": "{name}",
612 "pbrMetallicRoughness": {{
613 "baseColorFactor": [{r:.6}, {g:.6}, {b:.6}, {a:.6}],
614 "metallicFactor": {mf:.6},
615 "roughnessFactor": {rf:.6}
616 }},
617 "emissiveFactor": [{er:.6}, {eg:.6}, {eb:.6}],
618 "doubleSided": {ds},
619 "alphaMode": "{am}",
620 "alphaCutoff": {ac:.6}
621}}"#,
622 name = self.name,
623 r = bc[0],
624 g = bc[1],
625 b = bc[2],
626 a = bc[3],
627 mf = self.metallic_factor,
628 rf = self.roughness_factor,
629 er = em[0],
630 eg = em[1],
631 eb = em[2],
632 ds = self.double_sided,
633 am = self.alpha_mode,
634 ac = self.alpha_cutoff,
635 )
636 }
637 pub fn build(&self) -> GltfMaterial {
639 GltfMaterial {
640 name: self.name.clone(),
641 base_color_factor: self.base_color_factor,
642 metallic_factor: self.metallic_factor,
643 roughness_factor: self.roughness_factor,
644 emissive_factor: self.emissive_factor,
645 double_sided: self.double_sided,
646 alpha_mode: self.alpha_mode.clone(),
647 alpha_cutoff: self.alpha_cutoff,
648 }
649 }
650}
651#[allow(dead_code)]
653pub struct AnimationChannelBuilder {
654 pub node_index: usize,
656 pub path: String,
658 pub interpolation: Interpolation,
660 pub keyframes: Vec<Keyframe>,
662}
663impl AnimationChannelBuilder {
664 pub fn new(node_index: usize, path: impl Into<String>, interpolation: Interpolation) -> Self {
666 Self {
667 node_index,
668 path: path.into(),
669 interpolation,
670 keyframes: Vec::new(),
671 }
672 }
673 pub fn push(mut self, time: f32, value: Vec<f32>) -> Self {
675 self.keyframes.push(Keyframe::new(time, value));
676 self
677 }
678 pub fn duration(&self) -> f32 {
680 self.keyframes.last().map(|k| k.time).unwrap_or(0.0)
681 }
682 pub fn len(&self) -> usize {
684 self.keyframes.len()
685 }
686 pub fn is_empty(&self) -> bool {
688 self.keyframes.is_empty()
689 }
690 pub fn times(&self) -> Vec<f32> {
692 self.keyframes.iter().map(|k| k.time).collect()
693 }
694 pub fn values_flat(&self) -> Vec<f32> {
696 self.keyframes
697 .iter()
698 .flat_map(|k| k.value.iter().copied())
699 .collect()
700 }
701 pub fn to_json_fragments(
703 &self,
704 sampler_index: usize,
705 times_accessor: usize,
706 values_accessor: usize,
707 ) -> (String, String) {
708 let sampler = format!(
709 r#"{{ "input": {ti}, "interpolation": "{interp}", "output": {vi} }}"#,
710 ti = times_accessor,
711 interp = self.interpolation.as_str(),
712 vi = values_accessor,
713 );
714 let channel = format!(
715 r#"{{ "sampler": {si}, "target": {{ "node": {ni}, "path": "{path}" }} }}"#,
716 si = sampler_index,
717 ni = self.node_index,
718 path = self.path,
719 );
720 (sampler, channel)
721 }
722}
723#[derive(Debug, Clone)]
725pub struct GltfAnimation {
726 pub name: String,
728 pub channels: Vec<GltfAnimationChannel>,
730}
731impl GltfAnimation {
732 pub fn duration(&self) -> f32 {
734 self.channels
735 .iter()
736 .filter_map(|ch| ch.input_times.last().copied())
737 .fold(0.0f32, f32::max)
738 }
739 pub fn total_keyframe_count(&self) -> usize {
741 self.channels.iter().map(|ch| ch.input_times.len()).sum()
742 }
743}
744#[allow(dead_code)]
746#[derive(Debug, Clone)]
747pub struct Keyframe {
748 pub time: f32,
750 pub value: Vec<f32>,
752}
753impl Keyframe {
754 pub fn new(time: f32, value: Vec<f32>) -> Self {
756 Self { time, value }
757 }
758}
759#[derive(Debug, Clone)]
761pub struct GltfAccessor {
762 pub buffer_view: usize,
764 pub byte_offset: usize,
766 pub component_type: u32,
768 pub count: usize,
770 pub element_type: String,
772}
773impl GltfAccessor {
774 pub fn components_per_element(&self) -> usize {
776 match self.element_type.as_str() {
777 "SCALAR" => 1,
778 "VEC2" => 2,
779 "VEC3" => 3,
780 "VEC4" => 4,
781 "MAT2" => 4,
782 "MAT3" => 9,
783 "MAT4" => 16,
784 _ => 1,
785 }
786 }
787 pub fn component_size(&self) -> usize {
789 match self.component_type {
790 5120 => 1,
791 5121 => 1,
792 5122 => 2,
793 5123 => 2,
794 5125 => 4,
795 5126 => 4,
796 _ => 4,
797 }
798 }
799 pub fn byte_length(&self) -> usize {
801 self.count * self.components_per_element() * self.component_size()
802 }
803}
804#[derive(Debug, Clone)]
806pub struct MorphTarget {
807 pub name: String,
809 pub position_deltas: Vec<[f32; 3]>,
811}
812impl MorphTarget {
813 pub fn apply(&self, base_positions: &[[f32; 3]], weight: f32) -> Vec<[f32; 3]> {
817 let n = base_positions.len().min(self.position_deltas.len());
818 let mut out = base_positions.to_vec();
819 for i in 0..n {
820 out[i][0] += self.position_deltas[i][0] * weight;
821 out[i][1] += self.position_deltas[i][1] * weight;
822 out[i][2] += self.position_deltas[i][2] * weight;
823 }
824 out
825 }
826}
827pub struct GltfScene {
829 pub nodes: Vec<GltfNode>,
831 pub meshes: Vec<GltfMesh>,
833 pub accessors: Vec<GltfAccessor>,
835 pub buffer_views: Vec<GltfBufferView>,
837 pub animations: Vec<GltfAnimation>,
839 pub materials: Vec<GltfMaterial>,
841 pub cameras: Vec<SceneCamera>,
843 pub lights: Vec<SceneLight>,
845}
846impl GltfScene {
847 pub fn new() -> Self {
849 GltfScene {
850 nodes: Vec::new(),
851 meshes: Vec::new(),
852 accessors: Vec::new(),
853 buffer_views: Vec::new(),
854 animations: Vec::new(),
855 materials: Vec::new(),
856 cameras: Vec::new(),
857 lights: Vec::new(),
858 }
859 }
860 pub fn add_camera(&mut self, cam: SceneCamera) -> usize {
862 let idx = self.cameras.len();
863 self.cameras.push(cam);
864 idx
865 }
866 pub fn add_light(&mut self, light: SceneLight) -> usize {
868 let idx = self.lights.len();
869 self.lights.push(light);
870 idx
871 }
872 pub fn to_json_with_hierarchy(&self) -> String {
877 let mut out = self.to_json();
878 if out.ends_with('}') {
879 out.pop();
880 }
881 out.push_str(",\n");
882 if !self.cameras.is_empty() {
883 out.push_str(" \"cameras\": [\n");
884 for (ci, cam) in self.cameras.iter().enumerate() {
885 out.push_str(" {\n");
886 out.push_str(&format!(
887 " \"name\": \"{}\",\n",
888 escape_json(&cam.name)
889 ));
890 match &cam.camera_type {
891 CameraType::Perspective {
892 yfov,
893 znear,
894 zfar,
895 aspect_ratio,
896 } => {
897 out.push_str(" \"type\": \"perspective\",\n");
898 out.push_str(" \"perspective\": {\n");
899 out.push_str(&format!(" \"yfov\": {},\n", yfov));
900 out.push_str(&format!(" \"znear\": {}", znear));
901 if let Some(zf) = zfar {
902 out.push_str(&format!(",\n \"zfar\": {}", zf));
903 }
904 if let Some(ar) = aspect_ratio {
905 out.push_str(&format!(",\n \"aspectRatio\": {}", ar));
906 }
907 out.push_str("\n }\n");
908 }
909 CameraType::Orthographic {
910 xmag,
911 ymag,
912 znear,
913 zfar,
914 } => {
915 out.push_str(" \"type\": \"orthographic\",\n");
916 out.push_str(" \"orthographic\": {\n");
917 out.push_str(&format!(" \"xmag\": {},\n", xmag));
918 out.push_str(&format!(" \"ymag\": {},\n", ymag));
919 out.push_str(&format!(" \"znear\": {},\n", znear));
920 out.push_str(&format!(" \"zfar\": {}\n", zfar));
921 out.push_str(" }\n");
922 }
923 }
924 if ci + 1 < self.cameras.len() {
925 out.push_str(" },\n");
926 } else {
927 out.push_str(" }\n");
928 }
929 }
930 out.push_str(" ],\n");
931 }
932 if !self.lights.is_empty() {
933 out.push_str(" \"extensions\": {\n");
934 out.push_str(" \"KHR_lights_punctual\": {\n");
935 out.push_str(" \"lights\": [\n");
936 for (li, light) in self.lights.iter().enumerate() {
937 out.push_str(" {\n");
938 out.push_str(&format!(
939 " \"name\": \"{}\",\n",
940 escape_json(&light.name)
941 ));
942 out.push_str(&format!(
943 " \"type\": \"{}\",\n",
944 light.type_string()
945 ));
946 out.push_str(&format!(
947 " \"color\": [{}, {}, {}],\n",
948 light.color[0], light.color[1], light.color[2]
949 ));
950 out.push_str(&format!(" \"intensity\": {}", light.intensity));
951 if let LightType::Spot {
952 inner_cone_angle,
953 outer_cone_angle,
954 } = &light.light_type
955 {
956 out.push_str(
957 &format!(
958 ",\n \"spot\": {{ \"innerConeAngle\": {}, \"outerConeAngle\": {} }}",
959 inner_cone_angle, outer_cone_angle
960 ),
961 );
962 }
963 out.push('\n');
964 if li + 1 < self.lights.len() {
965 out.push_str(" },\n");
966 } else {
967 out.push_str(" }\n");
968 }
969 }
970 out.push_str(" ]\n");
971 out.push_str(" }\n");
972 out.push_str(" }\n");
973 } else {
974 if out.ends_with(",\n") {
975 out.truncate(out.len() - 2);
976 out.push('\n');
977 }
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 pub fn write(&self, scene: &GltfScene) -> Vec<u8> {
1263 let json = scene.to_json();
1264 let json_bytes = json.as_bytes();
1265 let json_len = json_bytes.len();
1266 let json_padded_len = (json_len + 3) & !3;
1267 let json_padding = json_padded_len - json_len;
1268 let chunk_header_size = 8usize;
1269 let header_size = 12usize;
1270 let total_len = header_size
1271 + chunk_header_size
1272 + json_padded_len
1273 + if self.include_empty_bin {
1274 chunk_header_size
1275 } else {
1276 0
1277 };
1278 let mut out = Vec::with_capacity(total_len);
1279 out.extend_from_slice(b"glTF");
1280 out.extend_from_slice(&2u32.to_le_bytes());
1281 out.extend_from_slice(&(total_len as u32).to_le_bytes());
1282 out.extend_from_slice(&(json_padded_len as u32).to_le_bytes());
1283 out.extend_from_slice(&0x4E4F534Au32.to_le_bytes());
1284 out.extend_from_slice(json_bytes);
1285 #[allow(clippy::same_item_push)]
1286 for _ in 0..json_padding {
1287 out.push(0x20);
1288 }
1289 if self.include_empty_bin {
1290 out.extend_from_slice(&0u32.to_le_bytes());
1291 out.extend_from_slice(&0x004E4942u32.to_le_bytes());
1292 }
1293 out
1294 }
1295}