Skip to main content

khora_io/asset/decoders/mesh/
obj.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! OBJ mesh decoder.
16
17use ahash::AHashMap;
18use anyhow::{Context, Result};
19use khora_core::{
20    math::{geometry::Aabb, Vec2, Vec3},
21    renderer::api::{
22        pipeline::enums::{PrimitiveTopology, VertexFormat},
23        pipeline::VertexAttributeDescriptor,
24        scene::Mesh,
25    },
26};
27use std::error::Error;
28
29use crate::asset::AssetDecoder;
30
31/// Decoder for OBJ mesh assets.
32#[derive(Clone, Default)]
33pub struct ObjDecoder;
34
35impl AssetDecoder<Mesh> for ObjDecoder {
36    fn load(&self, bytes: &[u8]) -> Result<Mesh, Box<dyn Error + Send + Sync>> {
37        let obj_text = std::str::from_utf8(bytes).context("OBJ file is not valid UTF-8")?;
38
39        let obj = tobj::load_obj_buf(
40            &mut std::io::Cursor::new(obj_text),
41            &tobj::LoadOptions {
42                triangulate: true,
43                single_index: true,
44                ..Default::default()
45            },
46            |_| Ok((Vec::new(), AHashMap::new())),
47        )
48        .context("Failed to parse OBJ file")?;
49
50        let (models, _materials) = obj;
51
52        if models.is_empty() {
53            return Err("No models found in OBJ file".into());
54        }
55
56        let model = &models[0];
57        let mesh = &model.mesh;
58
59        let positions = mesh
60            .positions
61            .chunks(3)
62            .map(|v| Vec3::new(v[0], v[1], v[2]))
63            .collect();
64
65        let normals = if !mesh.normals.is_empty() {
66            Some(
67                mesh.normals
68                    .chunks(3)
69                    .map(|n| Vec3::new(n[0], n[1], n[2]))
70                    .collect(),
71            )
72        } else {
73            None
74        };
75
76        let tex_coords = if !mesh.texcoords.is_empty() {
77            Some(
78                mesh.texcoords
79                    .chunks(2)
80                    .map(|t| Vec2::new(t[0], t[1]))
81                    .collect(),
82            )
83        } else {
84            None
85        };
86
87        let bounding_box = Aabb::from_points(
88            &mesh
89                .positions
90                .chunks(3)
91                .map(|v| Vec3::new(v[0], v[1], v[2]))
92                .collect::<Vec<_>>(),
93        )
94        .unwrap_or(Aabb::INVALID);
95
96        let mut vertex_layout = vec![VertexAttributeDescriptor {
97            shader_location: 0,
98            format: VertexFormat::Float32x3,
99            offset: 0,
100        }];
101
102        let mut next_location = 1;
103        if normals.is_some() {
104            vertex_layout.push(VertexAttributeDescriptor {
105                shader_location: next_location,
106                format: VertexFormat::Float32x3,
107                offset: std::mem::size_of::<Vec3>() as u64,
108            });
109            next_location += 1;
110        }
111
112        if tex_coords.is_some() {
113            vertex_layout.push(VertexAttributeDescriptor {
114                shader_location: next_location,
115                format: VertexFormat::Float32x2,
116                offset: (std::mem::size_of::<Vec3>() * (1 + normals.as_ref().map_or(0, |_| 1)))
117                    as u64,
118            });
119        }
120
121        Ok(Mesh {
122            positions,
123            normals,
124            tex_coords,
125            tangents: None,
126            colors: None,
127            indices: Some(mesh.indices.clone()),
128            primitive_type: PrimitiveTopology::TriangleList,
129            bounding_box,
130            vertex_layout,
131        })
132    }
133}