Skip to main content

libdqg/renderer/
model.rs

1use std::path::Path;
2use std::sync::Arc;
3
4use crate::renderer::extras::ModelVertex;
5use crate::renderer::types::{BufferInitDescriptor, BufferUsage};
6use crate::renderer::{Renderer, Texture};
7use crate::transform::Transformable;
8use crate::types::Color;
9
10/// Per-model uniform data: the model matrix plus an optional highlight overlay (see
11/// [`Model::highlight`]), laid out to match the `ModelTransform` struct declared in
12/// `model_pipeline_vs.wgsl`/`_fs.wgsl` and `model_outline_vs.wgsl`.
13#[repr(C)]
14#[derive(Debug, Clone, Copy)]
15pub(crate) struct ModelUniform {
16    pub(crate) model: [[f32; 4]; 4],
17    pub(crate) highlight: [f32; 4],
18}
19
20#[derive(Clone)]
21pub struct Model {
22    pub meshes: Vec<Mesh>,
23    pub materials: Vec<Material>,
24    /// Model matrix applied to every vertex in world space. Defaults to
25    /// [`glam::Mat4::IDENTITY`]. Prefer building it up through the [`Transformable`] methods.
26    pub transform: glam::Mat4,
27    /// RGBA overlay blended over the model's shaded color
28    /// (`mix(shaded, highlight.rgb, highlight.a)` in the fragment shader). Alpha 0 (the default)
29    /// means no highlight at all. Intended for editor-style hover/selection feedback rather than
30    /// gameplay use.
31    pub highlight: Color,
32    /// Center of the model's bounding sphere, in local (pre-transform) space.
33    pub bounding_center: glam::Vec3,
34    /// Radius of the model's bounding sphere, in local (pre-transform) space. Computed once at
35    /// load time since the raw vertex positions aren't retained after upload to the GPU.
36    pub bounding_radius: f32,
37    pub(crate) transform_buffer: wgpu::Buffer,
38    pub(crate) transform_bind_group: wgpu::BindGroup,
39}
40
41#[derive(Clone)]
42pub struct Mesh {
43    pub name: String,
44    pub vertex_buffer: wgpu::Buffer,
45    pub index_buffer: wgpu::Buffer,
46    pub num_indices: u32,
47    pub material: usize,
48    /// Local-space vertex positions, retained after upload to the GPU so callers (e.g. the
49    /// editor's ray/triangle picking) can test against the actual mesh geometry rather than just
50    /// its bounding sphere.
51    pub positions: Vec<glam::Vec3>,
52    /// Triangle-list indices into `positions`, retained alongside it for the same reason.
53    pub indices: Vec<u32>,
54}
55
56#[derive(Clone)]
57pub struct Material {
58    pub name: String,
59    pub diffuse_texture: Arc<Texture>,
60}
61
62impl Model {
63    /// Loads a Wavefront OBJ model (and its referenced `.mtl` materials and textures) from
64    /// `path`. Texture paths in the `.mtl` file are resolved relative to the OBJ's directory.
65    pub fn load(renderer: &Renderer, path: impl AsRef<Path>) -> anyhow::Result<Self> {
66        let path = crate::util::resolve_resource_path(path);
67        let parent = path.parent().unwrap_or_else(|| Path::new(""));
68
69        let (obj_models, obj_materials) = tobj::load_obj(
70            &path,
71            &tobj::LoadOptions {
72                triangulate: true,
73                single_index: true,
74                ..Default::default()
75            },
76        )?;
77        let obj_materials = obj_materials?;
78
79        let mut materials: Vec<Material> = obj_materials
80            .into_iter()
81            .map(|mat| {
82                let diffuse_texture = match &mat.diffuse_texture {
83                    Some(texture_file) => {
84                        Texture::from_path(renderer, parent.join(texture_file)).map_err(|e| anyhow::anyhow!(e))?
85                    }
86                    None => Texture::from_color(renderer, [255, 255, 255, 255]).map_err(|e| anyhow::anyhow!(e))?,
87                };
88                Ok(Material {
89                    name: mat.name,
90                    diffuse_texture: Arc::new(diffuse_texture),
91                })
92            })
93            .collect::<anyhow::Result<_>>()?;
94
95        if materials.is_empty() {
96            let diffuse_texture = Texture::from_color(renderer, [255, 255, 255, 255]).map_err(|e| anyhow::anyhow!(e))?;
97            materials.push(Material {
98                name: "default".to_string(),
99                diffuse_texture: Arc::new(diffuse_texture),
100            });
101        }
102        let fallback_material = materials.len() - 1;
103
104        let mut bounds_min = glam::Vec3::splat(f32::INFINITY);
105        let mut bounds_max = glam::Vec3::splat(f32::NEG_INFINITY);
106
107        let meshes = obj_models
108            .into_iter()
109            .map(|obj_model| {
110                let mesh = obj_model.mesh;
111                let vertex_count = mesh.positions.len() / 3;
112                let vertices: Vec<ModelVertex> = (0..vertex_count)
113                    .map(|i| {
114                        let position = [mesh.positions[i * 3], mesh.positions[i * 3 + 1], mesh.positions[i * 3 + 2]];
115                        let p = glam::Vec3::from_array(position);
116                        bounds_min = bounds_min.min(p);
117                        bounds_max = bounds_max.max(p);
118                        ModelVertex {
119                            position,
120                            tex_coords: if mesh.texcoords.is_empty() {
121                                [0.0, 0.0]
122                            } else {
123                                // OBJ texture coordinates are Y-up; wgpu textures are Y-down.
124                                [mesh.texcoords[i * 2], 1.0 - mesh.texcoords[i * 2 + 1]]
125                            },
126                            normal: if mesh.normals.is_empty() {
127                                [0.0, 0.0, 0.0]
128                            } else {
129                                [mesh.normals[i * 3], mesh.normals[i * 3 + 1], mesh.normals[i * 3 + 2]]
130                            },
131                        }
132                    })
133                    .collect();
134
135                let vertex_buffer = renderer.create_buffer_init(&BufferInitDescriptor {
136                    label: Some(&format!("{} Vertex Buffer", obj_model.name)),
137                    contents: crate::util::slice_to_bytes(&vertices),
138                    usage: BufferUsage::Vertex,
139                });
140                let index_buffer = renderer.create_buffer_init(&BufferInitDescriptor {
141                    label: Some(&format!("{} Index Buffer", obj_model.name)),
142                    contents: crate::util::slice_to_bytes(&mesh.indices),
143                    usage: BufferUsage::Index,
144                });
145
146                let positions: Vec<glam::Vec3> = vertices.iter().map(|v| glam::Vec3::from_array(v.position)).collect();
147
148                Mesh {
149                    name: obj_model.name,
150                    vertex_buffer,
151                    index_buffer,
152                    num_indices: mesh.indices.len() as u32,
153                    material: mesh.material_id.unwrap_or(fallback_material).min(fallback_material),
154                    positions,
155                    indices: mesh.indices,
156                }
157            })
158            .collect();
159
160        let (bounding_center, bounding_radius) = if bounds_min.x.is_finite() {
161            ((bounds_min + bounds_max) * 0.5, (bounds_max - bounds_min).length() * 0.5)
162        } else {
163            (glam::Vec3::ZERO, 0.0)
164        };
165
166        let transform_buffer = renderer.device.create_buffer(&wgpu::BufferDescriptor {
167            label: Some("Model Transform Buffer"),
168            size: std::mem::size_of::<ModelUniform>() as u64,
169            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
170            mapped_at_creation: false,
171        });
172        renderer.queue.write_buffer(
173            &transform_buffer,
174            0,
175            crate::util::slice_to_bytes(&[ModelUniform {
176                model: glam::Mat4::IDENTITY.to_cols_array_2d(),
177                highlight: [1.0, 1.0, 1.0, 0.0],
178            }]),
179        );
180        let transform_bind_group = renderer.device.create_bind_group(&wgpu::BindGroupDescriptor {
181            layout: &renderer.model_transform_bind_group_layout,
182            entries: &[wgpu::BindGroupEntry {
183                binding: 0,
184                resource: transform_buffer.as_entire_binding(),
185            }],
186            label: Some("model_transform_bind_group"),
187        });
188
189        Ok(Self {
190            meshes,
191            materials,
192            transform: glam::Mat4::IDENTITY,
193            highlight: Color::new(1.0, 1.0, 1.0, 0.0),
194            bounding_center,
195            bounding_radius,
196            transform_buffer,
197            transform_bind_group,
198        })
199    }
200}
201
202impl Transformable for Model {
203    fn transform(&self) -> glam::Mat4 {
204        self.transform
205    }
206
207    fn transform_mut(&mut self) -> &mut glam::Mat4 {
208        &mut self.transform
209    }
210}