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 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 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 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 let delta_pos1: Vector3<f32> = pos1 - pos0;
333 let delta_pos2: Vector3<f32> = pos2 - pos0;
334
335 let delta_uv1: Vector2<f32> = uv1 - uv0;
338 let delta_uv2: Vector2<f32> = uv2 - uv0;
339
340 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 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 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 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 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}