1use super::decode_obj_vertices_for_async;
2use crate::animation::{AnimationClip, Keyframe, SkeletonHierarchy, SkeletonJoint, Track};
3use crate::components::{Material, Mesh};
4use crate::renderer::Vertex;
5use gizmo_math::{Quat, Vec3};
6use std::sync::Arc;
7use wgpu::util::DeviceExt;
8
9pub struct GltfNodeData {
14 pub index: usize,
15 pub name: Option<String>,
16 pub skin_index: Option<usize>,
18 pub translation: [f32; 3],
19 pub rotation: [f32; 4],
20 pub scale: [f32; 3],
21 pub primitives: Vec<(Mesh, Option<Material>)>,
23 pub children: Vec<GltfNodeData>,
24}
25
26pub struct GltfSceneAsset {
27 pub roots: Vec<GltfNodeData>,
28 pub animations: Vec<AnimationClip>,
29 pub skeletons: Vec<SkeletonHierarchy>,
30}
31
32impl super::AssetManager {
37 pub fn install_obj_mesh(
44 &mut self,
45 device: &wgpu::Device,
46 file_path: &str,
47 vertices: Vec<Vertex>,
48 _aabb: gizmo_math::Aabb,
49 ) -> Mesh {
50 let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
51 label: Some(&format!("OBJ VBuf: {file_path}")),
52 contents: bytemuck::cast_slice(&vertices),
53 usage: wgpu::BufferUsages::VERTEX,
54 });
55 let mesh = Mesh::new(
56 device,
57 Arc::new(vbuf),
58 &vertices,
59 Vec3::ZERO,
60 format!("obj:{file_path}"),
61 );
62 self.mesh_cache.insert(file_path.to_string(), mesh.clone());
63 mesh
64 }
65
66 pub fn load_obj(&mut self, device: &wgpu::Device, file_path_or_uuid: &str) -> Mesh {
68 let file_path = match self.resolve_path_from_meta_source(file_path_or_uuid) {
69 Ok(p) => p,
70 Err(e) => {
71 tracing::error!("[AssetManager] ERROR: {e}");
72 return self.loading_placeholder_mesh(device);
73 }
74 };
75
76 let cache_key = self
78 .get_uuid(&file_path)
79 .map(|id| id.to_string())
80 .unwrap_or_else(|| file_path.clone());
81
82 if let Some(cached) = self.mesh_cache.get(&cache_key) {
83 return cached.clone();
84 }
85
86 let (vertices, aabb) = match decode_obj_vertices_for_async(&file_path) {
87 Ok(v) => v,
88 Err(e) => {
89 tracing::error!("[AssetManager] OBJ load failed: {file_path} — {e}");
90 let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
92 label: Some("Fallback VBuf (not found)"),
93 contents: &[],
94 usage: wgpu::BufferUsages::VERTEX,
95 });
96 return Mesh::empty(Arc::new(vbuf), format!("obj:missing_{file_path}"));
97 }
98 };
99
100 self.install_obj_mesh(device, &cache_key, vertices, aabb)
101 }
102
103 pub fn load_gltf_scene(
110 &mut self,
111 device: &wgpu::Device,
112 queue: &wgpu::Queue,
113 texture_bind_group_layout: &wgpu::BindGroupLayout,
114 default_tbind: Arc<wgpu::BindGroup>,
115 path_or_uuid: &str,
116 ) -> Result<GltfSceneAsset, String> {
117 let file_path = self.resolve_path_from_meta_source(path_or_uuid)?;
118 let cache_key = self
119 .get_uuid(&file_path)
120 .map(|id| id.to_string())
121 .unwrap_or_else(|| file_path.clone());
122
123 let import_result = if let Some(data) = self.embedded_assets.get(&file_path) {
124 gltf::import_slice(data.as_ref())
125 .map_err(|e| format!("Embedded glTF read failed ({file_path}): {e}"))
126 } else {
127 gltf::import(&file_path)
128 .map_err(|e| format!("glTF file load failed ({file_path}): {e}"))
129 };
130
131 let (document, buffers, images) = import_result?;
132 self.load_gltf_from_import(
133 device,
134 queue,
135 texture_bind_group_layout,
136 default_tbind,
137 &cache_key,
138 document,
139 buffers,
140 images,
141 )
142 }
143
144 pub fn load_gltf_from_import(
150 &mut self,
151 device: &wgpu::Device,
152 queue: &wgpu::Queue,
153 texture_bind_group_layout: &wgpu::BindGroupLayout,
154 default_tbind: Arc<wgpu::BindGroup>,
155 file_path: &str,
156 document: gltf::Document,
157 buffers: Vec<gltf::buffer::Data>,
158 images: Vec<gltf::image::Data>,
159 ) -> Result<GltfSceneAsset, String> {
160 let gltf_textures =
162 self.upload_gltf_textures(device, queue, texture_bind_group_layout, file_path, &images);
163
164 let gltf_materials = build_gltf_materials(&document, &gltf_textures, &default_tbind);
166
167 let mut roots = Vec::new();
169 for scene in document.scenes() {
170 for node in scene.nodes() {
171 roots.push(self.parse_gltf_node(
172 device,
173 &node,
174 &buffers,
175 &gltf_materials,
176 file_path,
177 ));
178 }
179 }
180
181 let animations = parse_animations(&document, &buffers);
183
184 let node_parents: std::collections::HashMap<usize, usize> = document
188 .nodes()
189 .flat_map(|parent| {
190 parent
191 .children()
192 .map(move |child| (child.index(), parent.index()))
193 })
194 .collect();
195
196 let nodes_by_index: Vec<gltf::Node> = document.nodes().collect();
198
199 let skeletons = parse_skeletons(&document, &buffers, &node_parents, &nodes_by_index);
200
201 Ok(GltfSceneAsset {
202 roots,
203 animations,
204 skeletons,
205 })
206 }
207
208 fn upload_gltf_textures(
211 &mut self,
212 device: &wgpu::Device,
213 queue: &wgpu::Queue,
214 texture_bind_group_layout: &wgpu::BindGroupLayout,
215 file_path: &str,
216 images: &[gltf::image::Data],
217 ) -> Vec<(Arc<wgpu::BindGroup>, String)> {
218 let mut gltf_textures = Vec::with_capacity(images.len());
219
220 for (i, image) in images.iter().enumerate() {
221 let (width, height) = (image.width, image.height);
222
223 let rgba: Vec<u8> = convert_image_to_rgba8(image, i, file_path);
225
226 let texture_size = wgpu::Extent3d {
227 width,
228 height,
229 depth_or_array_layers: 1,
230 };
231
232 let texture = device.create_texture(&wgpu::TextureDescriptor {
233 size: texture_size,
234 mip_level_count: 1,
235 sample_count: 1,
236 dimension: wgpu::TextureDimension::D2,
237 format: wgpu::TextureFormat::Rgba8UnormSrgb,
238 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
239 label: Some(&format!("{file_path}_tex_{i}")),
240 view_formats: &[],
241 });
242
243 queue.write_texture(
244 wgpu::ImageCopyTexture {
245 texture: &texture,
246 mip_level: 0,
247 origin: wgpu::Origin3d::ZERO,
248 aspect: wgpu::TextureAspect::All,
249 },
250 &rgba,
251 wgpu::ImageDataLayout {
252 offset: 0,
253 bytes_per_row: Some(4 * width),
254 rows_per_image: Some(height),
255 },
256 texture_size,
257 );
258
259 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
260 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
261 address_mode_u: wgpu::AddressMode::Repeat,
262 address_mode_v: wgpu::AddressMode::Repeat,
263 address_mode_w: wgpu::AddressMode::Repeat,
264 mag_filter: wgpu::FilterMode::Linear,
265 min_filter: wgpu::FilterMode::Linear,
266 mipmap_filter: wgpu::FilterMode::Linear,
267 ..Default::default()
268 });
269
270 let bg = Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
271 label: Some(&format!("{file_path}_bg_{i}")),
272 layout: texture_bind_group_layout,
273 entries: &[
274 wgpu::BindGroupEntry {
275 binding: 0,
276 resource: wgpu::BindingResource::TextureView(&view),
277 },
278 wgpu::BindGroupEntry {
279 binding: 1,
280 resource: wgpu::BindingResource::Sampler(&sampler),
281 },
282 ],
283 }));
284
285 let tex_source = format!("gltf_tex_{file_path}_{i}");
286 self.texture_cache.insert(tex_source.clone(), bg.clone());
287 gltf_textures.push((bg, tex_source));
288 }
289
290 gltf_textures
291 }
292
293 fn parse_gltf_node(
296 &mut self,
297 device: &wgpu::Device,
298 node: &gltf::Node,
299 buffers: &[gltf::buffer::Data],
300 materials: &[Material],
301 file_name: &str,
302 ) -> GltfNodeData {
303 let (translation, rotation, scale) = node.transform().decomposed();
304
305 let mut primitives = Vec::new();
306
307 if let Some(mesh) = node.mesh() {
308 for (prim_i, primitive) in mesh.primitives().enumerate() {
309 if primitive.mode() != gltf::mesh::Mode::Triangles {
311 tracing::error!(
312 "[GLTF WARN] Skipping non-triangle primitive (mode={:?}) on node '{}'",
313 primitive.mode(),
314 node.name().unwrap_or("<unnamed>"),
315 );
316 continue;
317 }
318
319 let reader = primitive.reader(|buf| Some(&buffers[buf.index()]));
320
321 let positions: Vec<[f32; 3]> = reader
322 .read_positions()
323 .map(|it| it.collect())
324 .unwrap_or_default();
325
326 if positions.is_empty() {
327 continue; }
329
330 let supplied_normals: Option<Vec<[f32; 3]>> =
331 reader.read_normals().map(|it| it.collect());
332
333 let tex_coords: Vec<[f32; 2]> = reader
334 .read_tex_coords(0)
335 .map(|it| it.into_f32().collect())
336 .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
337
338 let joints: Option<Vec<[u16; 4]>> =
339 reader.read_joints(0).map(|it| it.into_u16().collect());
340 let weights: Option<Vec<[f32; 4]>> =
341 reader.read_weights(0).map(|it| it.into_f32().collect());
342
343 let mut all_vertices: Vec<Vertex> = Vec::new();
345 let mut aabb = gizmo_math::Aabb::empty();
346
347 let make_vertex = |idx: usize| -> Vertex {
348 let pos = positions[idx];
349
350 let normal = supplied_normals
352 .as_ref()
353 .and_then(|n| n.get(idx).copied())
354 .unwrap_or([0.0, 1.0, 0.0]);
355 let uv = tex_coords.get(idx).copied().unwrap_or([0.0, 0.0]);
356 let j = joints
357 .as_ref()
358 .and_then(|js| js.get(idx))
359 .map(|&[a, b, c, d]| [a as u32, b as u32, c as u32, d as u32])
360 .unwrap_or([0; 4]);
361 let w = weights
362 .as_ref()
363 .and_then(|ws| ws.get(idx))
364 .copied()
365 .unwrap_or([0.0; 4]);
366
367 Vertex {
368 position: pos,
369 normal,
370 tex_coords: uv,
371 color: [1.0, 1.0, 1.0],
372 joint_indices: j,
373 joint_weights: w,
374 }
375 };
376
377 if let Some(indices) = reader.read_indices() {
378 for idx in indices.into_u32() {
379 let i = idx as usize;
380 if i < positions.len() {
381 let pos = positions[i];
382 aabb.extend(Vec3::new(pos[0], pos[1], pos[2]));
383 all_vertices.push(make_vertex(i));
384 }
385 }
386 } else {
387 for i in 0..positions.len() {
388 let pos = positions[i];
389 aabb.extend(Vec3::new(pos[0], pos[1], pos[2]));
390 all_vertices.push(make_vertex(i));
391 }
392 }
393
394 if supplied_normals.is_none() {
397 compute_flat_normals(&mut all_vertices);
398 }
399
400 let vbuf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
401 label: Some(&format!("GLTF VBuf: {file_name}_prim{prim_i}")),
402 contents: bytemuck::cast_slice(&all_vertices),
403 usage: wgpu::BufferUsages::VERTEX,
404 });
405
406 let mesh_source = format!(
408 "gltf_mesh_{file_name}_{}_p{prim_i}",
409 node.name().unwrap_or("<unnamed>")
410 );
411 let mesh_comp = Mesh::new(
412 device,
413 Arc::new(vbuf),
414 &all_vertices,
415 Vec3::ZERO,
416 mesh_source.clone(),
417 );
418 self.mesh_cache.insert(mesh_source, mesh_comp.clone());
419
420 let mat_opt = primitive
421 .material()
422 .index()
423 .and_then(|idx| materials.get(idx).cloned());
424
425 primitives.push((mesh_comp, mat_opt));
426 }
427 }
428
429 let children = node
430 .children()
431 .map(|child| self.parse_gltf_node(device, &child, buffers, materials, file_name))
432 .collect();
433
434 GltfNodeData {
435 index: node.index(),
436 name: node.name().map(str::to_owned),
437 skin_index: node.skin().map(|s| s.index()),
438 translation,
439 rotation,
440 scale,
441 primitives,
442 children,
443 }
444 }
445}
446
447fn convert_image_to_rgba8(image: &gltf::image::Data, idx: usize, file_path: &str) -> Vec<u8> {
453 let (w, h) = (image.width as usize, image.height as usize);
454 let pixel_count = w * h;
455
456 match image.format {
457 gltf::image::Format::R8G8B8A8 => {
458 let expected = pixel_count * 4;
461 if image.pixels.len() >= expected {
462 image.pixels[..expected].to_vec()
463 } else {
464 let mut out = image.pixels.clone();
466 out.resize(expected, 255);
467 out
468 }
469 }
470
471 gltf::image::Format::R8G8B8 => {
472 let mut out = Vec::with_capacity(pixel_count * 4);
476 for chunk in image.pixels.chunks_exact(3) {
477 out.extend_from_slice(&[chunk[0], chunk[1], chunk[2], 255]);
478 }
479 out.resize(pixel_count * 4, 255);
481 out
482 }
483
484 gltf::image::Format::R8G8 => {
485 let mut out = Vec::with_capacity(pixel_count * 4);
487 for chunk in image.pixels.chunks_exact(2) {
488 out.extend_from_slice(&[chunk[0], chunk[0], chunk[0], chunk[1]]);
489 }
490 out.resize(pixel_count * 4, 255);
491 out
492 }
493
494 gltf::image::Format::R8 => {
495 let mut out = Vec::with_capacity(pixel_count * 4);
497 for &lum in &image.pixels {
498 out.extend_from_slice(&[lum, lum, lum, 255]);
499 }
500 out.resize(pixel_count * 4, 255);
501 out
502 }
503
504 unknown => {
505 tracing::error!(
506 "[GLTF WARN] Unknown pixel format {unknown:?} on image {idx} in '{file_path}'. \
507 Falling back to RGBA8 with clamped copy."
508 );
509 let expected = pixel_count * 4;
510 let mut out = vec![0u8; expected];
512 for px in 0..pixel_count {
514 out[px * 4 + 3] = 255;
515 }
516 let copy_len = image.pixels.len().min(expected);
517 out[..copy_len].copy_from_slice(&image.pixels[..copy_len]);
518 out
519 }
520 }
521}
522
523fn compute_flat_normals(vertices: &mut [Vertex]) {
531 for tri in vertices.chunks_exact_mut(3) {
532 let v0 = Vec3::from(tri[0].position);
533 let v1 = Vec3::from(tri[1].position);
534 let v2 = Vec3::from(tri[2].position);
535
536 let edge1 = v1 - v0;
537 let edge2 = v2 - v0;
538 let cross = edge1.cross(edge2);
539
540 let normal = if cross.length_squared() > 1e-10 {
541 cross.normalize()
542 } else {
543 Vec3::Y };
545
546 let n = [normal.x, normal.y, normal.z];
547 tri[0].normal = n;
548 tri[1].normal = n;
549 tri[2].normal = n;
550 }
551}
552
553fn build_gltf_materials(
558 document: &gltf::Document,
559 gltf_textures: &[(Arc<wgpu::BindGroup>, String)],
560 default_tbind: &Arc<wgpu::BindGroup>,
561) -> Vec<Material> {
562 document
563 .materials()
564 .map(|material| {
565 let pbr = material.pbr_metallic_roughness();
566 let base_color = pbr.base_color_factor();
567
568 let mut mat = pbr
569 .base_color_texture()
570 .and_then(|ti| gltf_textures.get(ti.texture().source().index()))
571 .map(|(bg, src)| {
572 let mut m = Material::new(bg.clone());
573 m.texture_source = Some(src.clone());
574 m
575 })
576 .unwrap_or_else(|| Material::new(default_tbind.clone()));
577
578 let alpha = if material.alpha_mode() == gltf::material::AlphaMode::Opaque {
581 1.0
582 } else {
583 base_color[3]
584 };
585
586 mat.albedo = gizmo_math::Vec4::new(base_color[0], base_color[1], base_color[2], alpha);
587 mat.metallic = pbr.metallic_factor();
588 mat.roughness = pbr.roughness_factor();
589
590 mat.is_transparent = false;
591 mat.is_double_sided = material.double_sided();
592
593 mat
594 })
595 .collect()
596}
597
598fn parse_animations(
603 document: &gltf::Document,
604 buffers: &[gltf::buffer::Data],
605) -> Vec<AnimationClip> {
606 document
607 .animations()
608 .map(|anim| {
609 let mut translations = Vec::new();
610 let mut rotations = Vec::new();
611 let mut scales = Vec::new();
612
613 for channel in anim.channels() {
614 let target_node = channel.target().node().index();
615 let target_node_name = channel.target().node().name().map(str::to_owned);
616 let reader = channel.reader(|b| Some(&buffers[b.index()]));
617
618 let times: Vec<f32> = match reader.read_inputs() {
619 Some(it) => it.collect(),
620 None => continue,
621 };
622
623 let interp = match channel.sampler().interpolation() {
624 gltf::animation::Interpolation::Step => {
625 crate::animation::InterpolationMode::Step
626 }
627 gltf::animation::Interpolation::CubicSpline => {
628 crate::animation::InterpolationMode::CubicSpline
629 }
630 _ => crate::animation::InterpolationMode::Linear,
631 };
632
633 let outputs = match reader.read_outputs() {
634 Some(o) => o,
635 None => continue,
636 };
637
638 match outputs {
639 gltf::animation::util::ReadOutputs::Translations(tr) => {
640 let keyframes = times
641 .iter()
642 .zip(tr)
643 .map(|(&t, v)| Keyframe {
644 time: t,
645 value: Vec3::new(v[0], v[1], v[2]),
646 })
647 .collect();
648 translations.push(Track {
649 target_node,
650 target_node_name: target_node_name.clone(),
651 interpolation: interp,
652 keyframes,
653 });
654 }
655 gltf::animation::util::ReadOutputs::Rotations(rt) => {
656 let keyframes = times
657 .iter()
658 .zip(rt.into_f32())
659 .map(|(&t, v)| Keyframe {
660 time: t,
661 value: Quat::from_xyzw(v[0], v[1], v[2], v[3]),
662 })
663 .collect();
664 rotations.push(Track {
665 target_node,
666 target_node_name: target_node_name.clone(),
667 interpolation: interp,
668 keyframes,
669 });
670 }
671 gltf::animation::util::ReadOutputs::Scales(sc) => {
672 let keyframes = times
673 .iter()
674 .zip(sc)
675 .map(|(&t, v)| Keyframe {
676 time: t,
677 value: Vec3::new(v[0], v[1], v[2]),
678 })
679 .collect();
680 scales.push(Track {
681 target_node,
682 target_node_name,
683 interpolation: interp,
684 keyframes,
685 });
686 }
687 _ => {} }
689 }
690
691 let d_tr = translations
693 .iter()
694 .filter_map(|t| t.keyframes.last().map(|k| k.time))
695 .fold(0.0f32, f32::max);
696 let d_rot = rotations
697 .iter()
698 .filter_map(|t| t.keyframes.last().map(|k| k.time))
699 .fold(0.0f32, f32::max);
700 let d_scl = scales
701 .iter()
702 .filter_map(|t| t.keyframes.last().map(|k| k.time))
703 .fold(0.0f32, f32::max);
704 let duration = d_tr.max(d_rot).max(d_scl);
705
706 AnimationClip {
707 name: anim.name().unwrap_or("unnamed").to_string(),
708 duration,
709 translations,
710 rotations,
711 scales,
712 }
713 })
714 .collect()
715}
716
717fn parse_skeletons(
722 document: &gltf::Document,
723 buffers: &[gltf::buffer::Data],
724 node_parents: &std::collections::HashMap<usize, usize>,
725 nodes_by_index: &[gltf::Node],
726) -> Vec<SkeletonHierarchy> {
727 document
728 .skins()
729 .map(|skin| {
730 let reader = skin.reader(|b| Some(&buffers[b.index()]));
731
732 let identity_mat = [
733 [1.0, 0., 0., 0.],
734 [0., 1., 0., 0.],
735 [0., 0., 1., 0.],
736 [0., 0., 0., 1.],
737 ];
738 let ibm: Vec<[[f32; 4]; 4]> = reader
739 .read_inverse_bind_matrices()
740 .map(|v| v.collect())
741 .unwrap_or_else(|| vec![identity_mat; skin.joints().count()]);
742
743 let node_to_bone: std::collections::HashMap<usize, usize> = skin
745 .joints()
746 .enumerate()
747 .map(|(bone_idx, node)| (node.index(), bone_idx))
748 .collect();
749
750 let joints: Vec<SkeletonJoint> = skin
751 .joints()
752 .enumerate()
753 .map(|(bone_idx, joint_node)| {
754 let inverse_bind_matrix = gizmo_math::Mat4::from_cols_array_2d(&ibm[bone_idx]);
755
756 let parent_index = node_parents
757 .get(&joint_node.index())
758 .and_then(|p| node_to_bone.get(p).copied());
759
760 let (t, r, s) = joint_node.transform().decomposed();
761 let bind_translation = Vec3::new(t[0], t[1], t[2]);
762 let bind_rotation = Quat::from_array(r);
763 let bind_scale = Vec3::new(s[0], s[1], s[2]);
764
765 let local_bind_transform = gizmo_math::Mat4::from_translation(bind_translation)
766 * gizmo_math::Mat4::from_quat(bind_rotation)
767 * gizmo_math::Mat4::from_scale(bind_scale);
768
769 SkeletonJoint {
770 name: joint_node.name().unwrap_or("bone").to_string(),
771 node_index: joint_node.index(),
772 inverse_bind_matrix,
773 parent_index,
774 local_bind_transform,
775 bind_translation,
776 bind_rotation,
777 bind_scale,
778 }
779 })
780 .collect();
781
782 let root_transform =
788 compute_armature_root_transform(&skin, node_parents, &node_to_bone, nodes_by_index);
789
790 SkeletonHierarchy {
791 joints,
792 root_transform,
793 }
794 })
795 .collect()
796}
797
798fn compute_armature_root_transform(
801 skin: &gltf::Skin,
802 node_parents: &std::collections::HashMap<usize, usize>,
803 node_to_bone: &std::collections::HashMap<usize, usize>,
804 nodes_by_index: &[gltf::Node],
805) -> gizmo_math::Mat4 {
806 let mut root_transform = gizmo_math::Mat4::IDENTITY;
807
808 let first_joint = match skin.joints().next() {
809 Some(j) => j,
810 None => return root_transform,
811 };
812
813 let mut current_idx = first_joint.index();
814 let mut ancestor_transforms: Vec<gizmo_math::Mat4> = Vec::new();
815
816 while let Some(&parent_idx) = node_parents.get(¤t_idx) {
817 if node_to_bone.contains_key(&parent_idx) {
820 break;
821 }
822
823 if let Some(parent_node) = nodes_by_index.get(parent_idx) {
824 let (t, r, s) = parent_node.transform().decomposed();
825 let mat = gizmo_math::Mat4::from_translation(Vec3::new(t[0], t[1], t[2]))
826 * gizmo_math::Mat4::from_quat(Quat::from_array(r))
827 * gizmo_math::Mat4::from_scale(Vec3::new(s[0], s[1], s[2]));
828 ancestor_transforms.push(mat);
829 }
830
831 current_idx = parent_idx;
832 }
833
834 for mat in ancestor_transforms.into_iter().rev() {
836 root_transform *= mat;
837 }
838
839 root_transform
840}