#![allow(clippy::needless_range_loop)]
use super::functions::escape_json;
#[allow(unused_imports)]
use super::functions::*;
#[derive(Debug, Clone, PartialEq)]
pub enum LightType {
Point,
Directional,
Spot {
inner_cone_angle: f32,
outer_cone_angle: f32,
},
}
#[derive(Debug, Clone)]
pub enum CameraType {
Perspective {
yfov: f32,
znear: f32,
zfar: Option<f32>,
aspect_ratio: Option<f32>,
},
Orthographic {
xmag: f32,
ymag: f32,
znear: f32,
zfar: f32,
},
}
#[derive(Debug, Clone)]
pub struct ValidationIssue {
pub message: String,
pub is_error: bool,
}
impl ValidationIssue {
pub(crate) fn error(msg: impl Into<String>) -> Self {
ValidationIssue {
message: msg.into(),
is_error: true,
}
}
pub(crate) fn warning(msg: impl Into<String>) -> Self {
ValidationIssue {
message: msg.into(),
is_error: false,
}
}
}
#[derive(Debug, Clone)]
pub struct GltfAnimationChannel {
pub target: GltfAnimationTarget,
pub interpolation: String,
pub input_times: Vec<f32>,
pub output_values: Vec<f32>,
}
#[derive(Debug, Clone)]
pub struct JointChannel {
pub joint_index: usize,
pub path: String,
pub times: Vec<f32>,
pub values: Vec<f32>,
pub interpolation: String,
}
#[derive(Debug, Clone)]
pub struct GltfMaterial {
pub name: String,
pub base_color_factor: [f32; 4],
pub metallic_factor: f32,
pub roughness_factor: f32,
pub emissive_factor: [f32; 3],
pub alpha_mode: String,
pub alpha_cutoff: f32,
pub double_sided: bool,
}
impl GltfMaterial {
pub fn is_opaque(&self) -> bool {
self.alpha_mode == "OPAQUE" && (self.base_color_factor[3] - 1.0).abs() < 1e-6
}
pub fn is_emissive(&self) -> bool {
self.emissive_factor.iter().any(|&v| v > 0.0)
}
}
#[derive(Debug, Clone)]
pub struct Joint {
pub name: String,
pub translation: [f64; 3],
pub rotation: [f64; 4],
pub scale: [f64; 3],
pub children: Vec<usize>,
pub inverse_bind_matrix: [f32; 16],
}
#[derive(Debug, Clone)]
pub struct SceneLight {
pub name: String,
pub light_type: LightType,
pub color: [f32; 3],
pub intensity: f32,
}
impl SceneLight {
pub fn type_string(&self) -> &str {
match &self.light_type {
LightType::Point => "point",
LightType::Directional => "directional",
LightType::Spot { .. } => "spot",
}
}
}
pub struct GltfPrimitive {
pub positions: Vec<[f32; 3]>,
pub normals: Vec<[f32; 3]>,
pub texcoords: Vec<[f32; 2]>,
pub indices: Vec<u32>,
}
impl GltfPrimitive {
pub fn bounding_box(&self) -> ([f32; 3], [f32; 3]) {
let mut min = [f32::INFINITY; 3];
let mut max = [f32::NEG_INFINITY; 3];
for p in &self.positions {
for i in 0..3 {
if p[i] < min[i] {
min[i] = p[i];
}
if p[i] > max[i] {
max[i] = p[i];
}
}
}
(min, max)
}
pub fn triangle_count(&self) -> usize {
self.indices.len() / 3
}
pub fn vertex_count(&self) -> usize {
self.positions.len()
}
pub fn extract_triangles(&self) -> Vec<[[f32; 3]; 3]> {
let mut tris = Vec::new();
let mut i = 0;
while i + 2 < self.indices.len() {
let a = self.indices[i] as usize;
let b = self.indices[i + 1] as usize;
let c = self.indices[i + 2] as usize;
if a < self.positions.len() && b < self.positions.len() && c < self.positions.len() {
tris.push([self.positions[a], self.positions[b], self.positions[c]]);
}
i += 3;
}
tris
}
}
pub struct GltfMesh {
pub name: String,
pub primitives: Vec<GltfPrimitive>,
}
impl GltfMesh {
pub fn total_vertex_count(&self) -> usize {
self.primitives.iter().map(|p| p.vertex_count()).sum()
}
pub fn total_triangle_count(&self) -> usize {
self.primitives.iter().map(|p| p.triangle_count()).sum()
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessorType {
Scalar,
Vec2,
Vec3,
Vec4,
Mat2,
Mat3,
Mat4,
}
impl AccessorType {
pub fn num_components(self) -> usize {
match self {
AccessorType::Scalar => 1,
AccessorType::Vec2 => 2,
AccessorType::Vec3 => 3,
AccessorType::Vec4 => 4,
AccessorType::Mat2 => 4,
AccessorType::Mat3 => 9,
AccessorType::Mat4 => 16,
}
}
pub fn as_str(self) -> &'static str {
match self {
AccessorType::Scalar => "SCALAR",
AccessorType::Vec2 => "VEC2",
AccessorType::Vec3 => "VEC3",
AccessorType::Vec4 => "VEC4",
AccessorType::Mat2 => "MAT2",
AccessorType::Mat3 => "MAT3",
AccessorType::Mat4 => "MAT4",
}
}
}
#[derive(Debug, Clone)]
pub struct SkeletonAnimation {
pub name: String,
pub joint_channels: Vec<JointChannel>,
}
impl SkeletonAnimation {
pub fn duration(&self) -> f32 {
self.joint_channels
.iter()
.filter_map(|c| c.times.last().copied())
.fold(0.0f32, f32::max)
}
pub fn total_keyframes(&self) -> usize {
self.joint_channels.iter().map(|c| c.times.len()).sum()
}
}
#[derive(Debug, Clone)]
pub struct GltfBufferView {
pub buffer: usize,
pub byte_offset: usize,
pub byte_length: usize,
pub byte_stride: Option<usize>,
}
#[allow(dead_code)]
pub struct TypedAccessor {
pub name: String,
pub buffer_view_index: usize,
pub byte_offset: usize,
pub component_type: ComponentType,
pub accessor_type: AccessorType,
pub count: usize,
pub min_values: Vec<f64>,
pub max_values: Vec<f64>,
}
impl TypedAccessor {
#[allow(clippy::too_many_arguments)]
pub fn new(
name: impl Into<String>,
buffer_view_index: usize,
byte_offset: usize,
component_type: ComponentType,
accessor_type: AccessorType,
count: usize,
) -> Self {
Self {
name: name.into(),
buffer_view_index,
byte_offset,
component_type,
accessor_type,
count,
min_values: Vec::new(),
max_values: Vec::new(),
}
}
pub fn byte_length(&self) -> usize {
self.count * self.accessor_type.num_components() * self.component_type.byte_size()
}
pub fn to_json(&self) -> String {
format!(
r#"{{ "bufferView": {bv}, "byteOffset": {bo}, "componentType": {ct}, "type": "{at}", "count": {c} }}"#,
bv = self.buffer_view_index,
bo = self.byte_offset,
ct = self.component_type.component_type_code(),
at = self.accessor_type.as_str(),
c = self.count,
)
}
pub fn decode_f32(&self, buffer: &[u8]) -> Option<Vec<f32>> {
if self.component_type != ComponentType::Float {
return None;
}
let start = self.byte_offset;
let len = self.byte_length();
let end = start + len;
if end > buffer.len() {
return None;
}
let slice = &buffer[start..end];
let floats: Vec<f32> = slice
.chunks_exact(4)
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
.collect();
Some(floats)
}
pub fn decode_u32(&self, buffer: &[u8]) -> Option<Vec<u32>> {
if self.component_type != ComponentType::UnsignedInt {
return None;
}
let start = self.byte_offset;
let len = self.byte_length();
let end = start + len;
if end > buffer.len() {
return None;
}
let slice = &buffer[start..end];
let indices: Vec<u32> = slice
.chunks_exact(4)
.map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
.collect();
Some(indices)
}
}
#[derive(Debug, Clone)]
pub struct GltfAnimationTarget {
pub node: usize,
pub path: String,
}
pub struct GltfNode {
pub name: String,
pub mesh: Option<usize>,
pub translation: [f64; 3],
pub rotation: [f64; 4],
pub scale: [f64; 3],
pub children: Vec<usize>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Interpolation {
Linear,
Step,
CubicSpline,
}
impl Interpolation {
pub fn as_str(&self) -> &'static str {
match self {
Interpolation::Linear => "LINEAR",
Interpolation::Step => "STEP",
Interpolation::CubicSpline => "CUBICSPLINE",
}
}
}
pub struct Skeleton {
pub joints: Vec<Joint>,
}
impl Skeleton {
pub fn new() -> Self {
Skeleton { joints: Vec::new() }
}
pub fn add_joint(&mut self, joint: Joint) -> usize {
let idx = self.joints.len();
self.joints.push(joint);
idx
}
pub fn joint_count(&self) -> usize {
self.joints.len()
}
pub fn export_animation_json(&self, anim: &SkeletonAnimation) -> String {
let mut out = String::new();
out.push_str("{\n");
out.push_str(&format!(" \"name\": \"{}\",\n", escape_json(&anim.name)));
out.push_str(" \"channels\": [\n");
for (ci, ch) in anim.joint_channels.iter().enumerate() {
let joint_name = self
.joints
.get(ch.joint_index)
.map(|j| j.name.as_str())
.unwrap_or("unknown");
out.push_str(" {\n");
out.push_str(&format!(
" \"joint\": \"{}\",\n",
escape_json(joint_name)
));
out.push_str(&format!(" \"path\": \"{}\",\n", escape_json(&ch.path)));
out.push_str(&format!(
" \"interpolation\": \"{}\",\n",
escape_json(&ch.interpolation)
));
let times_str: Vec<String> = ch.times.iter().map(|t| format!("{t}")).collect();
out.push_str(&format!(" \"times\": [{}]\n", times_str.join(", ")));
if ci + 1 < anim.joint_channels.len() {
out.push_str(" },\n");
} else {
out.push_str(" }\n");
}
}
out.push_str(" ]\n");
out.push('}');
out
}
}
#[derive(Debug, Clone)]
pub struct SceneCamera {
pub name: String,
pub camera_type: CameraType,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComponentType {
UnsignedByte = 5121,
UnsignedShort = 5123,
UnsignedInt = 5125,
Float = 5126,
}
impl ComponentType {
pub fn byte_size(self) -> usize {
match self {
ComponentType::UnsignedByte => 1,
ComponentType::UnsignedShort => 2,
ComponentType::UnsignedInt => 4,
ComponentType::Float => 4,
}
}
pub fn component_type_code(self) -> u32 {
self as u32
}
}
#[derive(Debug, Clone)]
pub struct PbrMaterialBuilder {
pub name: String,
pub base_color_factor: [f32; 4],
pub metallic_factor: f32,
pub roughness_factor: f32,
pub emissive_factor: [f32; 3],
pub double_sided: bool,
pub alpha_mode: String,
pub alpha_cutoff: f32,
}
impl PbrMaterialBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
pub fn base_color(mut self, r: f32, g: f32, b: f32, a: f32) -> Self {
self.base_color_factor = [r, g, b, a];
self
}
pub fn metallic_roughness(mut self, metallic: f32, roughness: f32) -> Self {
self.metallic_factor = metallic;
self.roughness_factor = roughness;
self
}
pub fn emissive(mut self, r: f32, g: f32, b: f32) -> Self {
self.emissive_factor = [r, g, b];
self
}
pub fn double_sided(mut self, ds: bool) -> Self {
self.double_sided = ds;
self
}
pub fn alpha_mode(mut self, mode: impl Into<String>) -> Self {
self.alpha_mode = mode.into();
self
}
pub fn to_json(&self) -> String {
let bc = &self.base_color_factor;
let em = &self.emissive_factor;
format!(
r#"{{
"name": "{name}",
"pbrMetallicRoughness": {{
"baseColorFactor": [{r:.6}, {g:.6}, {b:.6}, {a:.6}],
"metallicFactor": {mf:.6},
"roughnessFactor": {rf:.6}
}},
"emissiveFactor": [{er:.6}, {eg:.6}, {eb:.6}],
"doubleSided": {ds},
"alphaMode": "{am}",
"alphaCutoff": {ac:.6}
}}"#,
name = self.name,
r = bc[0],
g = bc[1],
b = bc[2],
a = bc[3],
mf = self.metallic_factor,
rf = self.roughness_factor,
er = em[0],
eg = em[1],
eb = em[2],
ds = self.double_sided,
am = self.alpha_mode,
ac = self.alpha_cutoff,
)
}
pub fn build(&self) -> GltfMaterial {
GltfMaterial {
name: self.name.clone(),
base_color_factor: self.base_color_factor,
metallic_factor: self.metallic_factor,
roughness_factor: self.roughness_factor,
emissive_factor: self.emissive_factor,
double_sided: self.double_sided,
alpha_mode: self.alpha_mode.clone(),
alpha_cutoff: self.alpha_cutoff,
}
}
}
#[allow(dead_code)]
pub struct AnimationChannelBuilder {
pub node_index: usize,
pub path: String,
pub interpolation: Interpolation,
pub keyframes: Vec<Keyframe>,
}
impl AnimationChannelBuilder {
pub fn new(node_index: usize, path: impl Into<String>, interpolation: Interpolation) -> Self {
Self {
node_index,
path: path.into(),
interpolation,
keyframes: Vec::new(),
}
}
pub fn push(mut self, time: f32, value: Vec<f32>) -> Self {
self.keyframes.push(Keyframe::new(time, value));
self
}
pub fn duration(&self) -> f32 {
self.keyframes.last().map(|k| k.time).unwrap_or(0.0)
}
pub fn len(&self) -> usize {
self.keyframes.len()
}
pub fn is_empty(&self) -> bool {
self.keyframes.is_empty()
}
pub fn times(&self) -> Vec<f32> {
self.keyframes.iter().map(|k| k.time).collect()
}
pub fn values_flat(&self) -> Vec<f32> {
self.keyframes
.iter()
.flat_map(|k| k.value.iter().copied())
.collect()
}
pub fn to_json_fragments(
&self,
sampler_index: usize,
times_accessor: usize,
values_accessor: usize,
) -> (String, String) {
let sampler = format!(
r#"{{ "input": {ti}, "interpolation": "{interp}", "output": {vi} }}"#,
ti = times_accessor,
interp = self.interpolation.as_str(),
vi = values_accessor,
);
let channel = format!(
r#"{{ "sampler": {si}, "target": {{ "node": {ni}, "path": "{path}" }} }}"#,
si = sampler_index,
ni = self.node_index,
path = self.path,
);
(sampler, channel)
}
}
#[derive(Debug, Clone)]
pub struct GltfAnimation {
pub name: String,
pub channels: Vec<GltfAnimationChannel>,
}
impl GltfAnimation {
pub fn duration(&self) -> f32 {
self.channels
.iter()
.filter_map(|ch| ch.input_times.last().copied())
.fold(0.0f32, f32::max)
}
pub fn total_keyframe_count(&self) -> usize {
self.channels.iter().map(|ch| ch.input_times.len()).sum()
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Keyframe {
pub time: f32,
pub value: Vec<f32>,
}
impl Keyframe {
pub fn new(time: f32, value: Vec<f32>) -> Self {
Self { time, value }
}
}
#[derive(Debug, Clone)]
pub struct GltfAccessor {
pub buffer_view: usize,
pub byte_offset: usize,
pub component_type: u32,
pub count: usize,
pub element_type: String,
}
impl GltfAccessor {
pub fn components_per_element(&self) -> usize {
match self.element_type.as_str() {
"SCALAR" => 1,
"VEC2" => 2,
"VEC3" => 3,
"VEC4" => 4,
"MAT2" => 4,
"MAT3" => 9,
"MAT4" => 16,
_ => 1,
}
}
pub fn component_size(&self) -> usize {
match self.component_type {
5120 => 1,
5121 => 1,
5122 => 2,
5123 => 2,
5125 => 4,
5126 => 4,
_ => 4,
}
}
pub fn byte_length(&self) -> usize {
self.count * self.components_per_element() * self.component_size()
}
}
#[derive(Debug, Clone)]
pub struct MorphTarget {
pub name: String,
pub position_deltas: Vec<[f32; 3]>,
}
impl MorphTarget {
pub fn apply(&self, base_positions: &[[f32; 3]], weight: f32) -> Vec<[f32; 3]> {
let n = base_positions.len().min(self.position_deltas.len());
let mut out = base_positions.to_vec();
for i in 0..n {
out[i][0] += self.position_deltas[i][0] * weight;
out[i][1] += self.position_deltas[i][1] * weight;
out[i][2] += self.position_deltas[i][2] * weight;
}
out
}
}
pub struct GltfScene {
pub nodes: Vec<GltfNode>,
pub meshes: Vec<GltfMesh>,
pub accessors: Vec<GltfAccessor>,
pub buffer_views: Vec<GltfBufferView>,
pub animations: Vec<GltfAnimation>,
pub materials: Vec<GltfMaterial>,
pub cameras: Vec<SceneCamera>,
pub lights: Vec<SceneLight>,
}
impl GltfScene {
pub fn new() -> Self {
GltfScene {
nodes: Vec::new(),
meshes: Vec::new(),
accessors: Vec::new(),
buffer_views: Vec::new(),
animations: Vec::new(),
materials: Vec::new(),
cameras: Vec::new(),
lights: Vec::new(),
}
}
pub fn add_camera(&mut self, cam: SceneCamera) -> usize {
let idx = self.cameras.len();
self.cameras.push(cam);
idx
}
pub fn add_light(&mut self, light: SceneLight) -> usize {
let idx = self.lights.len();
self.lights.push(light);
idx
}
pub fn to_json_with_hierarchy(&self) -> String {
let mut out = self.to_json();
if out.ends_with('}') {
out.pop();
}
out.push_str(",\n");
if !self.cameras.is_empty() {
out.push_str(" \"cameras\": [\n");
for (ci, cam) in self.cameras.iter().enumerate() {
out.push_str(" {\n");
out.push_str(&format!(
" \"name\": \"{}\",\n",
escape_json(&cam.name)
));
match &cam.camera_type {
CameraType::Perspective {
yfov,
znear,
zfar,
aspect_ratio,
} => {
out.push_str(" \"type\": \"perspective\",\n");
out.push_str(" \"perspective\": {\n");
out.push_str(&format!(" \"yfov\": {},\n", yfov));
out.push_str(&format!(" \"znear\": {}", znear));
if let Some(zf) = zfar {
out.push_str(&format!(",\n \"zfar\": {}", zf));
}
if let Some(ar) = aspect_ratio {
out.push_str(&format!(",\n \"aspectRatio\": {}", ar));
}
out.push_str("\n }\n");
}
CameraType::Orthographic {
xmag,
ymag,
znear,
zfar,
} => {
out.push_str(" \"type\": \"orthographic\",\n");
out.push_str(" \"orthographic\": {\n");
out.push_str(&format!(" \"xmag\": {},\n", xmag));
out.push_str(&format!(" \"ymag\": {},\n", ymag));
out.push_str(&format!(" \"znear\": {},\n", znear));
out.push_str(&format!(" \"zfar\": {}\n", zfar));
out.push_str(" }\n");
}
}
if ci + 1 < self.cameras.len() {
out.push_str(" },\n");
} else {
out.push_str(" }\n");
}
}
out.push_str(" ],\n");
}
if !self.lights.is_empty() {
out.push_str(" \"extensions\": {\n");
out.push_str(" \"KHR_lights_punctual\": {\n");
out.push_str(" \"lights\": [\n");
for (li, light) in self.lights.iter().enumerate() {
out.push_str(" {\n");
out.push_str(&format!(
" \"name\": \"{}\",\n",
escape_json(&light.name)
));
out.push_str(&format!(
" \"type\": \"{}\",\n",
light.type_string()
));
out.push_str(&format!(
" \"color\": [{}, {}, {}],\n",
light.color[0], light.color[1], light.color[2]
));
out.push_str(&format!(" \"intensity\": {}", light.intensity));
if let LightType::Spot {
inner_cone_angle,
outer_cone_angle,
} = &light.light_type
{
out.push_str(
&format!(
",\n \"spot\": {{ \"innerConeAngle\": {}, \"outerConeAngle\": {} }}",
inner_cone_angle, outer_cone_angle
),
);
}
out.push('\n');
if li + 1 < self.lights.len() {
out.push_str(" },\n");
} else {
out.push_str(" }\n");
}
}
out.push_str(" ]\n");
out.push_str(" }\n");
out.push_str(" }\n");
} else if out.ends_with(",\n") {
out.truncate(out.len() - 2);
out.push('\n');
}
out.push('}');
out
}
pub fn add_mesh(&mut self, mesh: GltfMesh) -> usize {
let idx = self.meshes.len();
self.meshes.push(mesh);
idx
}
pub fn add_node(&mut self, node: GltfNode) {
self.nodes.push(node);
}
pub fn mesh_count(&self) -> usize {
self.meshes.len()
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn traverse_depth_first<F>(&self, mut visitor: F)
where
F: FnMut(usize, usize, [f64; 3]),
{
let mut is_child = vec![false; self.nodes.len()];
for node in &self.nodes {
for &child_idx in &node.children {
if child_idx < is_child.len() {
is_child[child_idx] = true;
}
}
}
let mut stack: Vec<(usize, usize, [f64; 3])> = Vec::new();
for i in (0..self.nodes.len()).rev() {
if !is_child[i] {
stack.push((i, 0, [0.0, 0.0, 0.0]));
}
}
while let Some((idx, depth, parent_translation)) = stack.pop() {
if idx >= self.nodes.len() {
continue;
}
let node = &self.nodes[idx];
let accumulated = [
parent_translation[0] + node.translation[0],
parent_translation[1] + node.translation[1],
parent_translation[2] + node.translation[2],
];
visitor(idx, depth, accumulated);
for &child_idx in node.children.iter().rev() {
stack.push((child_idx, depth + 1, accumulated));
}
}
}
pub fn collect_mesh_primitives(&self) -> Vec<(&str, &GltfPrimitive)> {
let mut result = Vec::new();
for node in &self.nodes {
if let Some(mesh_idx) = node.mesh
&& let Some(mesh) = self.meshes.get(mesh_idx)
{
for prim in &mesh.primitives {
result.push((node.name.as_str(), prim));
}
}
}
result
}
pub fn add_material(&mut self, mat: GltfMaterial) -> usize {
let idx = self.materials.len();
self.materials.push(mat);
idx
}
pub fn add_animation(&mut self, anim: GltfAnimation) -> usize {
let idx = self.animations.len();
self.animations.push(anim);
idx
}
pub fn nodes_using_mesh(&self, mesh_idx: usize) -> Vec<usize> {
self.nodes
.iter()
.enumerate()
.filter_map(|(i, n)| {
if n.mesh == Some(mesh_idx) {
Some(i)
} else {
None
}
})
.collect()
}
pub fn total_vertex_count(&self) -> usize {
self.meshes.iter().map(|m| m.total_vertex_count()).sum()
}
pub fn total_triangle_count(&self) -> usize {
self.meshes.iter().map(|m| m.total_triangle_count()).sum()
}
pub fn to_json(&self) -> String {
let mut out = String::new();
out.push_str("{\n");
out.push_str(" \"asset\": {\n");
out.push_str(" \"version\": \"2.0\",\n");
out.push_str(" \"generator\": \"OxiPhysics glTF writer\"\n");
out.push_str(" },\n");
out.push_str(" \"scene\": 0,\n");
let node_indices: Vec<String> = (0..self.nodes.len()).map(|i| i.to_string()).collect();
out.push_str(" \"scenes\": [\n {\n \"nodes\": [");
out.push_str(&node_indices.join(", "));
out.push_str("]\n }\n ],\n");
out.push_str(" \"nodes\": [\n");
for (i, node) in self.nodes.iter().enumerate() {
out.push_str(" {\n");
out.push_str(&format!(
" \"name\": \"{}\",\n",
escape_json(&node.name)
));
if let Some(mesh_idx) = node.mesh {
out.push_str(&format!(" \"mesh\": {},\n", mesh_idx));
}
if !node.children.is_empty() {
let children_str: Vec<String> =
node.children.iter().map(|c| c.to_string()).collect();
out.push_str(&format!(
" \"children\": [{}],\n",
children_str.join(", ")
));
}
out.push_str(&format!(
" \"translation\": [{}, {}, {}],\n",
node.translation[0], node.translation[1], node.translation[2]
));
out.push_str(&format!(
" \"rotation\": [{}, {}, {}, {}],\n",
node.rotation[0], node.rotation[1], node.rotation[2], node.rotation[3]
));
out.push_str(&format!(
" \"scale\": [{}, {}, {}]\n",
node.scale[0], node.scale[1], node.scale[2]
));
if i + 1 < self.nodes.len() {
out.push_str(" },\n");
} else {
out.push_str(" }\n");
}
}
out.push_str(" ],\n");
out.push_str(" \"meshes\": [\n");
for (mi, mesh) in self.meshes.iter().enumerate() {
out.push_str(" {\n");
out.push_str(&format!(
" \"name\": \"{}\",\n",
escape_json(&mesh.name)
));
out.push_str(" \"primitives\": [\n");
for (pi, _prim) in mesh.primitives.iter().enumerate() {
let pos_acc = pi * 3;
let nrm_acc = pi * 3 + 1;
let idx_acc = pi * 3 + 2;
out.push_str(" {\n");
out.push_str(" \"attributes\": {\n");
out.push_str(&format!(" \"POSITION\": {},\n", pos_acc));
out.push_str(&format!(" \"NORMAL\": {}\n", nrm_acc));
out.push_str(" },\n");
out.push_str(&format!(" \"indices\": {}\n", idx_acc));
if pi + 1 < mesh.primitives.len() {
out.push_str(" },\n");
} else {
out.push_str(" }\n");
}
}
out.push_str(" ]\n");
if mi + 1 < self.meshes.len() {
out.push_str(" },\n");
} else {
out.push_str(" }\n");
}
}
out.push_str(" ],\n");
if !self.materials.is_empty() {
out.push_str(" \"materials\": [\n");
for (mi, mat) in self.materials.iter().enumerate() {
out.push_str(" {\n");
out.push_str(&format!(
" \"name\": \"{}\",\n",
escape_json(&mat.name)
));
out.push_str(" \"pbrMetallicRoughness\": {\n");
out.push_str(&format!(
" \"baseColorFactor\": [{}, {}, {}, {}],\n",
mat.base_color_factor[0],
mat.base_color_factor[1],
mat.base_color_factor[2],
mat.base_color_factor[3]
));
out.push_str(&format!(
" \"metallicFactor\": {},\n",
mat.metallic_factor
));
out.push_str(&format!(
" \"roughnessFactor\": {}\n",
mat.roughness_factor
));
out.push_str(" },\n");
out.push_str(&format!(
" \"emissiveFactor\": [{}, {}, {}],\n",
mat.emissive_factor[0], mat.emissive_factor[1], mat.emissive_factor[2]
));
out.push_str(&format!(" \"alphaMode\": \"{}\",\n", mat.alpha_mode));
out.push_str(&format!(" \"doubleSided\": {}\n", mat.double_sided));
if mi + 1 < self.materials.len() {
out.push_str(" },\n");
} else {
out.push_str(" }\n");
}
}
out.push_str(" ],\n");
}
out.push_str(" \"accessors\": [],\n");
out.push_str(" \"bufferViews\": [],\n");
out.push_str(" \"buffers\": []\n");
out.push('}');
out
}
}
pub struct MorphPrimitive {
pub base: GltfPrimitive,
pub targets: Vec<MorphTarget>,
pub weights: Vec<f32>,
}
impl MorphPrimitive {
pub fn set_weight(&mut self, target_idx: usize, weight: f32) {
if target_idx < self.weights.len() {
self.weights[target_idx] = weight.clamp(0.0, 1.0);
}
}
pub fn blend(&self) -> Vec<[f32; 3]> {
let mut result = self.base.positions.clone();
for (target, &w) in self.targets.iter().zip(self.weights.iter()) {
let n = result.len().min(target.position_deltas.len());
for i in 0..n {
result[i][0] += target.position_deltas[i][0] * w;
result[i][1] += target.position_deltas[i][1] * w;
result[i][2] += target.position_deltas[i][2] * w;
}
}
result
}
}
pub struct GlbWriter {
pub include_empty_bin: bool,
}
impl GlbWriter {
pub fn new() -> Self {
GlbWriter {
include_empty_bin: false,
}
}
pub fn write(&self, scene: &GltfScene) -> Vec<u8> {
let json = scene.to_json();
let json_bytes = json.as_bytes();
let json_len = json_bytes.len();
let json_padded_len = (json_len + 3) & !3;
let json_padding = json_padded_len - json_len;
let chunk_header_size = 8usize;
let header_size = 12usize;
let total_len = header_size
+ chunk_header_size
+ json_padded_len
+ if self.include_empty_bin {
chunk_header_size
} else {
0
};
let mut out = Vec::with_capacity(total_len);
out.extend_from_slice(b"glTF");
out.extend_from_slice(&2u32.to_le_bytes());
out.extend_from_slice(&(total_len as u32).to_le_bytes());
out.extend_from_slice(&(json_padded_len as u32).to_le_bytes());
out.extend_from_slice(&0x4E4F534Au32.to_le_bytes());
out.extend_from_slice(json_bytes);
#[allow(clippy::same_item_push)]
for _ in 0..json_padding {
out.push(0x20);
}
if self.include_empty_bin {
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&0x004E4942u32.to_le_bytes());
}
out
}
pub fn write_glb(&self, scene: &GltfScene) -> Vec<u8> {
let primitives: Vec<&GltfPrimitive> = scene
.meshes
.iter()
.flat_map(|m| m.primitives.iter())
.collect();
if primitives.is_empty() {
return Vec::new();
}
let mut bin_buf: Vec<u8> = Vec::new();
let mut prim_meta: Vec<PrimMeta> = Vec::with_capacity(primitives.len());
for prim in &primitives {
let n_verts = prim.positions.len();
let n_indices = prim.indices.len();
let mut pos_min = [f32::INFINITY; 3];
let mut pos_max = [f32::NEG_INFINITY; 3];
for p in &prim.positions {
for i in 0..3 {
if p[i] < pos_min[i] {
pos_min[i] = p[i];
}
if p[i] > pos_max[i] {
pos_max[i] = p[i];
}
}
}
if n_verts == 0 {
pos_min = [0.0; 3];
pos_max = [0.0; 3];
}
let pos_byte_offset = bin_buf.len();
for p in &prim.positions {
for &v in p {
bin_buf.extend_from_slice(&v.to_le_bytes());
}
}
let norm_byte_offset = bin_buf.len();
for n in &prim.normals {
for &v in n {
bin_buf.extend_from_slice(&v.to_le_bytes());
}
}
let norm_count = prim.normals.len();
for _ in norm_count..n_verts {
bin_buf.extend_from_slice(&[0u8; 12]);
}
let uv_byte_offset = bin_buf.len();
if prim.texcoords.is_empty() {
for _ in 0..n_verts {
bin_buf.extend_from_slice(&[0u8; 8]);
}
} else {
for uv in &prim.texcoords {
for &v in uv {
bin_buf.extend_from_slice(&v.to_le_bytes());
}
}
let uv_count = prim.texcoords.len();
for _ in uv_count..n_verts {
bin_buf.extend_from_slice(&[0u8; 8]);
}
}
let pad_to_align = (4 - bin_buf.len() % 4) % 4;
bin_buf.extend(std::iter::repeat_n(0u8, pad_to_align));
let idx_byte_offset = bin_buf.len();
for &idx in &prim.indices {
bin_buf.extend_from_slice(&idx.to_le_bytes());
}
prim_meta.push(PrimMeta {
n_verts,
n_indices,
pos_byte_offset,
norm_byte_offset,
uv_byte_offset,
idx_byte_offset,
pos_min,
pos_max,
});
}
let bin_tail_pad = (4 - bin_buf.len() % 4) % 4;
bin_buf.extend(std::iter::repeat_n(0u8, bin_tail_pad));
let total_bin_len = bin_buf.len();
let json = Self::build_glb_json(scene, &prim_meta, total_bin_len);
let json_bytes = json.as_bytes();
let json_raw_len = json_bytes.len();
let json_padded_len = (json_raw_len + 3) & !3;
let json_padding = json_padded_len - json_raw_len;
let total_len = 12 + 8 + json_padded_len + 8 + total_bin_len;
let mut out = Vec::with_capacity(total_len);
out.extend_from_slice(b"glTF");
out.extend_from_slice(&2u32.to_le_bytes());
out.extend_from_slice(&(total_len as u32).to_le_bytes());
out.extend_from_slice(&(json_padded_len as u32).to_le_bytes());
out.extend_from_slice(&0x4E4F534Au32.to_le_bytes()); out.extend_from_slice(json_bytes);
out.extend(std::iter::repeat_n(0x20u8, json_padding));
out.extend_from_slice(&(total_bin_len as u32).to_le_bytes());
out.extend_from_slice(&0x004E4942u32.to_le_bytes()); out.extend_from_slice(&bin_buf);
out
}
fn build_glb_json(scene: &GltfScene, prim_meta: &[PrimMeta], total_bin_len: usize) -> String {
use std::fmt::Write as FmtWrite;
let mut out = String::new();
let _ = writeln!(out, "{{");
let _ = writeln!(
out,
" \"asset\": {{\"version\": \"2.0\", \"generator\": \"OxiPhysics glTF writer\"}},"
);
let _ = writeln!(out, " \"scene\": 0,");
let node_indices: Vec<String> = (0..scene.nodes.len()).map(|i| i.to_string()).collect();
let _ = writeln!(
out,
" \"scenes\": [{{\"nodes\": [{}]}}],",
node_indices.join(", ")
);
let _ = writeln!(out, " \"nodes\": [");
for (i, node) in scene.nodes.iter().enumerate() {
let mesh_str = node
.mesh
.map(|m| format!(", \"mesh\": {m}"))
.unwrap_or_default();
let comma = if i + 1 < scene.nodes.len() { "," } else { "" };
let _ = writeln!(
out,
" {{\"name\": \"{}\"{mesh_str}}}{comma}",
escape_json(&node.name)
);
}
let _ = writeln!(out, " ],");
let _ = writeln!(out, " \"meshes\": [");
let mut acc_idx = 0usize;
for (mi, mesh) in scene.meshes.iter().enumerate() {
let _ = writeln!(
out,
" {{\"name\": \"{}\", \"primitives\": [",
escape_json(&mesh.name)
);
for (pi, _prim) in mesh.primitives.iter().enumerate() {
let pos_acc = acc_idx;
let nrm_acc = acc_idx + 1;
let uv_acc = acc_idx + 2;
let idx_acc = acc_idx + 3;
acc_idx += 4;
let prim_comma = if pi + 1 < mesh.primitives.len() {
","
} else {
""
};
let _ = writeln!(
out,
" {{\"attributes\": {{\"POSITION\": {pos_acc}, \"NORMAL\": {nrm_acc}, \"TEXCOORD_0\": {uv_acc}}}, \"indices\": {idx_acc}}}{prim_comma}"
);
}
let mesh_comma = if mi + 1 < scene.meshes.len() { "," } else { "" };
let _ = writeln!(out, " ]}}{mesh_comma}");
}
let _ = writeln!(out, " ],");
let _ = writeln!(out, " \"buffers\": [{{\"byteLength\": {total_bin_len}}}],");
let n_meta = prim_meta.len();
let total_bv = n_meta * 4;
let mut bv_entries: Vec<String> = Vec::with_capacity(total_bv);
for meta in prim_meta.iter() {
let pos_size = meta.n_verts * 12; let norm_size = meta.n_verts * 12;
let uv_size = meta.n_verts * 8; let idx_size = meta.n_indices * 4; bv_entries.push(format!(
" {{\"buffer\": 0, \"byteOffset\": {}, \"byteLength\": {pos_size}, \"target\": 34962}}",
meta.pos_byte_offset
));
bv_entries.push(format!(
" {{\"buffer\": 0, \"byteOffset\": {}, \"byteLength\": {norm_size}, \"target\": 34962}}",
meta.norm_byte_offset
));
bv_entries.push(format!(
" {{\"buffer\": 0, \"byteOffset\": {}, \"byteLength\": {uv_size}, \"target\": 34962}}",
meta.uv_byte_offset
));
bv_entries.push(format!(
" {{\"buffer\": 0, \"byteOffset\": {}, \"byteLength\": {idx_size}, \"target\": 34963}}",
meta.idx_byte_offset
));
}
let _ = writeln!(out, " \"bufferViews\": [");
let _ = writeln!(out, "{}", bv_entries.join(",\n"));
let _ = writeln!(out, " ],");
let total_acc = n_meta * 4;
let mut acc_entries: Vec<String> = Vec::with_capacity(total_acc);
let mut bv_idx = 0usize;
for meta in prim_meta.iter() {
let pos_min = meta.pos_min;
let pos_max = meta.pos_max;
acc_entries.push(format!(
" {{\"bufferView\": {bv_idx}, \"componentType\": 5126, \"count\": {}, \"type\": \"VEC3\", \
\"min\": [{}, {}, {}], \"max\": [{}, {}, {}]}}",
meta.n_verts,
pos_min[0], pos_min[1], pos_min[2],
pos_max[0], pos_max[1], pos_max[2]
));
bv_idx += 1;
acc_entries.push(format!(
" {{\"bufferView\": {bv_idx}, \"componentType\": 5126, \"count\": {}, \"type\": \"VEC3\"}}",
meta.n_verts
));
bv_idx += 1;
acc_entries.push(format!(
" {{\"bufferView\": {bv_idx}, \"componentType\": 5126, \"count\": {}, \"type\": \"VEC2\"}}",
meta.n_verts
));
bv_idx += 1;
acc_entries.push(format!(
" {{\"bufferView\": {bv_idx}, \"componentType\": 5125, \"count\": {}, \"type\": \"SCALAR\"}}",
meta.n_indices
));
bv_idx += 1;
}
let _ = writeln!(out, " \"accessors\": [");
let _ = writeln!(out, "{}", acc_entries.join(",\n"));
let _ = writeln!(out, " ]");
let _ = write!(out, "}}");
out
}
}
struct PrimMeta {
n_verts: usize,
n_indices: usize,
pos_byte_offset: usize,
norm_byte_offset: usize,
uv_byte_offset: usize,
idx_byte_offset: usize,
pos_min: [f32; 3],
pos_max: [f32; 3],
}