Skip to main content

ferrum_wgpu/assets/
resources.rs

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