Skip to main content

ferrum_wgpu/assets/
resources.rs

1use crate::{
2    assets::{self, TypeModel},
3    renderer,
4};
5use {
6    cgmath::{Vector2, Vector3},
7    std::io::{BufReader, Cursor},
8    wgpu::{BindGroup, Buffer, util::DeviceExt},
9};
10
11#[cfg(not(target_arch = "wasm32"))]
12use std::path::{Path, PathBuf};
13
14#[derive(Clone)]
15pub struct Asset {
16    pub out_dir: String,
17}
18
19impl Default for Asset {
20    fn default() -> Self {
21        Self {
22            out_dir: "res".to_string(),
23        }
24    }
25}
26
27impl Asset {
28    pub fn new(out_dir: String) -> Self {
29        Self { out_dir }
30    }
31}
32
33pub trait AssetSource: Send + Sync {
34    #[cfg(target_arch = "wasm32")]
35    fn format_url(&self, file_name: &str) -> reqwest::Url;
36    #[cfg(not(target_arch = "wasm32"))]
37    fn resolve_resource_path(&self, file_name: &str) -> anyhow::Result<PathBuf>;
38    fn load_string(&self, file_name: &str) -> impl Future<Output = anyhow::Result<String>> + Send;
39    fn load_binary(&self, file_name: &str) -> impl Future<Output = anyhow::Result<Vec<u8>>> + Send;
40}
41
42impl AssetSource for Asset {
43    #[cfg(target_arch = "wasm32")]
44    fn format_url(&self, file_name: &str) -> reqwest::Url {
45        let base = reqwest::Url::parse(self.out_dir).unwrap();
46        base.join(file_name).unwrap()
47    }
48
49    #[cfg(not(target_arch = "wasm32"))]
50    fn resolve_resource_path(&self, file_name: &str) -> anyhow::Result<PathBuf> {
51        let out_dir_path = Path::new(self.out_dir.as_str()).join(file_name);
52        if out_dir_path.is_file() {
53            return Ok(out_dir_path);
54        }
55
56        if let Ok(exe) = std::env::current_exe()
57            && let Some(exe_dir) = exe.parent()
58        {
59            let exe_path = exe_dir.join("res").join(file_name);
60            if exe_path.is_file() {
61                return Ok(exe_path);
62            }
63        }
64
65        anyhow::bail!("Recurso no encontrado: '{file_name}'")
66    }
67
68    async fn load_string(&self, file_name: &str) -> anyhow::Result<String> {
69        log::debug!("Open: {:?}", file_name);
70
71        #[cfg(target_arch = "wasm32")]
72        let text = {
73            let url = self.format_url(file_name);
74            log::debug!("[load_string] GET {}", url);
75            let resp = reqwest::get(url.clone()).await.map_err(|e| {
76                log::error!("[load_string] fetch failed for '{}': {}", url, e);
77                e
78            })?;
79            if !resp.status().is_success() {
80                log::error!("[load_string] HTTP {} for '{}'", resp.status(), url);
81                anyhow::bail!("HTTP {} fetching '{}'", resp.status(), url);
82            }
83            resp.text().await?
84        };
85
86        #[cfg(not(target_arch = "wasm32"))]
87        let text: String = {
88            let path: PathBuf = self.resolve_resource_path(file_name).map_err(|e| {
89                log::error!("[load_string] resource not found '{}': {}", file_name, e);
90                e
91            })?;
92
93            log::debug!("[load_string] resolved '{}' -> {:?}", file_name, path);
94            std::fs::read_to_string(path)?
95        };
96
97        Ok(text)
98    }
99
100    async fn load_binary(&self, file_name: &str) -> anyhow::Result<Vec<u8>> {
101        #[cfg(target_arch = "wasm32")]
102        let data = {
103            let url = self.format_url(file_name);
104            log::debug!("[load_binary] GET {}", url);
105            let resp = reqwest::get(url.clone()).await.map_err(|e| {
106                log::error!("[load_binary] fetch failed for '{}': {}", url, e);
107                e
108            })?;
109            if !resp.status().is_success() {
110                log::error!("[load_binary] HTTP {} for '{}'", resp.status(), url);
111                anyhow::bail!("HTTP {} fetching '{}'", resp.status(), url);
112            }
113            resp.bytes().await?.to_vec()
114        };
115
116        #[cfg(not(target_arch = "wasm32"))]
117        let data = {
118            let path: PathBuf = self.resolve_resource_path(file_name).map_err(|e| {
119                log::error!("[load_binary] resource not found '{}': {}", file_name, e);
120                e
121            })?;
122
123            std::fs::read(path)?
124        };
125
126        Ok(data)
127    }
128}
129
130pub fn load_texture(
131    data: Vec<u8>,
132    file_name: &str,
133    device: &wgpu::Device,
134    queue: &wgpu::Queue,
135    is_normal_map: bool,
136) -> anyhow::Result<renderer::Texture> {
137    renderer::Texture::from_bytes(device, queue, &data, file_name, is_normal_map)
138}
139
140pub async fn load_model(
141    asset: &impl AssetSource,
142    file_name: &str,
143    device: &wgpu::Device,
144    queue: &wgpu::Queue,
145    layout: &wgpu::BindGroupLayout,
146    instances: Vec<assets::Instance>,
147    type_model: TypeModel,
148) -> anyhow::Result<assets::Model> {
149    let model_binary: Vec<u8> = asset.load_binary(file_name).await?;
150    let mut obj_reader = BufReader::new(Cursor::new(model_binary));
151
152    let parent_path: String = std::path::Path::new(file_name)
153        .parent()
154        .and_then(|p| p.to_str())
155        .unwrap_or("")
156        .to_string();
157
158    let (models, obj_materials): (
159        Vec<tobj::Model>,
160        Result<Vec<tobj::Material>, tobj::LoadError>,
161    ) = tobj::load_obj_buf_async(
162        &mut obj_reader,
163        &tobj::LoadOptions {
164            triangulate: true,
165            single_index: true,
166            ..Default::default()
167        },
168        |mtl_path| {
169            // diffuse_texture
170            let parent_path: String = parent_path.clone();
171            async move {
172                let full_mtl_path: String = if parent_path.is_empty() {
173                    mtl_path.clone()
174                } else {
175                    std::path::Path::new(&parent_path)
176                        .join(&mtl_path)
177                        .to_str()
178                        .unwrap()
179                        .to_string()
180                };
181                log::debug!("Buscando mtl: {}", full_mtl_path);
182                let mat_text: String = asset.load_string(&full_mtl_path).await.unwrap();
183                tobj::load_mtl_buf(&mut BufReader::new(Cursor::new(mat_text)))
184            }
185        },
186    )
187    .await?;
188
189    let mut materials: Vec<renderer::Material> = Vec::new();
190    for m in obj_materials? {
191        let full_diffuse_path: String = if parent_path.is_empty() {
192            m.diffuse_texture.clone()
193        } else {
194            std::path::Path::new(&parent_path)
195                .join(&m.diffuse_texture)
196                .to_str()
197                .unwrap()
198                .to_string()
199        };
200
201        let diffuse_binary = asset.load_binary(&full_diffuse_path).await?;
202        let diffuse_texture: renderer::Texture =
203            load_texture(diffuse_binary, &full_diffuse_path, device, queue, false)?;
204
205        let normal_texture: renderer::Texture = if m.normal_texture.is_empty() {
206            renderer::Texture::default_normal(device, queue)
207        } else {
208            let full_normal_path: String = if parent_path.is_empty() {
209                m.normal_texture.clone()
210            } else {
211                std::path::Path::new(&parent_path)
212                    .join(&m.normal_texture)
213                    .to_str()
214                    .unwrap()
215                    .to_string()
216            };
217            let texture_binary: Vec<u8> = asset.load_binary(&full_normal_path).await?;
218            load_texture(texture_binary, &full_normal_path, device, queue, true)?
219        };
220
221        let bind_group: BindGroup = device.create_bind_group(&wgpu::BindGroupDescriptor {
222            layout,
223            entries: &[
224                wgpu::BindGroupEntry {
225                    binding: 0,
226                    resource: wgpu::BindingResource::TextureView(&diffuse_texture.view),
227                },
228                wgpu::BindGroupEntry {
229                    binding: 1,
230                    resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler),
231                },
232                wgpu::BindGroupEntry {
233                    binding: 2,
234                    resource: wgpu::BindingResource::TextureView(&normal_texture.view),
235                },
236                wgpu::BindGroupEntry {
237                    binding: 3,
238                    resource: wgpu::BindingResource::Sampler(&normal_texture.sampler),
239                },
240            ],
241            label: None,
242        });
243
244        materials.push(renderer::Material {
245            name: m.name,
246            diffuse_texture,
247            normal_texture,
248            bind_group,
249        });
250    }
251
252    let meshes: Vec<renderer::Mesh> = models
253        .into_iter()
254        .map(|m| {
255            let has_colors: bool = !m.mesh.vertex_color.is_empty();
256            let has_normals: bool = !m.mesh.normals.is_empty();
257            let has_texcoords: bool = !m.mesh.texcoords.is_empty();
258            let has_normal_indices: bool = !m.mesh.normal_indices.is_empty();
259
260            let mut vertices: Vec<assets::ModelVertex> = (0..m.mesh.positions.len() / 3)
261                .map(|i| {
262                    let pi = i * 3;
263
264                    let position = [
265                        m.mesh.positions[pi],
266                        m.mesh.positions[pi + 1],
267                        m.mesh.positions[pi + 2],
268                    ];
269
270                    let text_cords = if has_texcoords {
271                        [m.mesh.texcoords[i * 2], 1.0 - m.mesh.texcoords[i * 2 + 1]]
272                    } else {
273                        [0.0, 0.0]
274                    };
275
276                    let normal = if has_normals {
277                        let ni = if has_normal_indices {
278                            m.mesh.normal_indices[i] as usize * 3
279                        } else {
280                            pi
281                        };
282                        [
283                            m.mesh.normals[ni],
284                            m.mesh.normals[ni + 1],
285                            m.mesh.normals[ni + 2],
286                        ]
287                    } else {
288                        [0.0, 0.0, 0.0]
289                    };
290
291                    let color = if has_colors {
292                        [
293                            m.mesh.vertex_color[pi],
294                            m.mesh.vertex_color[pi + 1],
295                            m.mesh.vertex_color[pi + 2],
296                        ]
297                    } else {
298                        [1.0, 1.0, 1.0]
299                    };
300
301                    let tangent: [f32; 3] = [0.0, 0.0, 0.0];
302
303                    let bitangent: [f32; 3] = [0.0, 0.0, 0.0];
304
305                    assets::ModelVertex {
306                        position,
307                        text_cords,
308                        normal,
309                        color,
310                        tangent,
311                        bitangent,
312                    }
313                })
314                .collect::<Vec<_>>();
315
316            let indices: &Vec<u32> = &m.mesh.indices;
317            let mut trangles_included = vec![0; vertices.len()];
318
319            for c in indices.chunks(3) {
320                let v0: assets::ModelVertex = vertices[c[0] as usize];
321                let v1: assets::ModelVertex = vertices[c[1] as usize];
322                let v2: assets::ModelVertex = vertices[c[2] as usize];
323
324                let pos0: Vector3<f32> = v0.position.into();
325                let pos1: Vector3<f32> = v1.position.into();
326                let pos2: Vector3<f32> = v2.position.into();
327
328                let uv0: Vector2<f32> = v0.text_cords.into();
329                let uv1: Vector2<f32> = v1.text_cords.into();
330                let uv2: Vector2<f32> = v2.text_cords.into();
331
332                // Calculate the edges of the triangle
333                let delta_pos1: Vector3<f32> = pos1 - pos0;
334                let delta_pos2: Vector3<f32> = pos2 - pos0;
335
336                // This will give us a direction to calculate the
337                // tangent and bitangent
338                let delta_uv1: Vector2<f32> = uv1 - uv0;
339                let delta_uv2: Vector2<f32> = uv2 - uv0;
340
341                // Solving the following system of equations will
342                // give us the tangent and bitangent.
343                //     delta_pos1 = delta_uv1.x * T + delta_u.y * B
344                //     delta_pos2 = delta_uv2.x * T + delta_uv2.y * B
345
346                let r: f32 = 1.0 / (delta_uv1.x * delta_uv2.y - delta_uv1.y * delta_uv2.x);
347                let tanget: Vector3<f32> =
348                    (delta_pos1 * delta_uv2.y - delta_pos2 * delta_uv1.y) * r;
349                let bitanget: Vector3<f32> =
350                    (delta_pos2 * delta_uv1.x - delta_pos1 * delta_uv2.x) * -r;
351
352                // Tangent
353                vertices[c[0] as usize].tangent =
354                    (tanget + Vector3::from(vertices[c[0] as usize].tangent)).into();
355                vertices[c[1] as usize].tangent =
356                    (tanget + Vector3::from(vertices[c[1] as usize].tangent)).into();
357                vertices[c[2] as usize].tangent =
358                    (tanget + Vector3::from(vertices[c[2] as usize].tangent)).into();
359
360                // Bitangent
361                vertices[c[0] as usize].bitangent =
362                    (bitanget + Vector3::from(vertices[c[0] as usize].bitangent)).into();
363                vertices[c[1] as usize].bitangent =
364                    (bitanget + Vector3::from(vertices[c[1] as usize].bitangent)).into();
365                vertices[c[2] as usize].bitangent =
366                    (bitanget + Vector3::from(vertices[c[2] as usize].bitangent)).into();
367
368                // Used to average the tangents/bitangents
369                trangles_included[c[0] as usize] += 1;
370                trangles_included[c[1] as usize] += 1;
371                trangles_included[c[2] as usize] += 1;
372            }
373
374            // Average the tangents/bitangents
375            for (i, n) in trangles_included.into_iter().enumerate() {
376                let denom: f32 = 1.0 / n as f32;
377                let v: &mut assets::ModelVertex = &mut vertices[i];
378                v.tangent = (Vector3::from(v.tangent) * denom).into();
379                v.bitangent = (Vector3::from(v.bitangent) * denom).into();
380            }
381
382            let vertex_buffer: Buffer =
383                device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
384                    label: Some(&format!("{:?} Vertex Buffer", file_name)),
385                    contents: bytemuck::cast_slice(&vertices),
386                    usage: wgpu::BufferUsages::VERTEX,
387                });
388
389            let index_buffer: Buffer =
390                device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
391                    label: Some(&format!("{:?} Index Buffer", file_name)),
392                    contents: bytemuck::cast_slice(&m.mesh.indices),
393                    usage: wgpu::BufferUsages::INDEX,
394                });
395
396            renderer::Mesh {
397                name: file_name.to_string(),
398                vertex_buffer,
399                index_buffer,
400                material: m.mesh.material_id.unwrap_or(0),
401                indices: m.mesh.indices.len() as u32,
402            }
403        })
404        .collect::<Vec<_>>();
405
406    let instances_raws: Vec<assets::InstanceRaw> = instances.iter().map(|i| i.to_raw()).collect();
407
408    let label: String = format!("{file_name}_instance_buffer");
409    let instance_buffer: Buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
410        label: Some(&label),
411        contents: bytemuck::cast_slice(&instances_raws),
412        usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
413    });
414
415    Ok(assets::Model {
416        meshes,
417        materials,
418        instances,
419        instance_buffer,
420        type_model,
421    })
422}