Skip to main content

flow_ngin/resources/
texture.rs

1use std::io::{BufReader, Cursor};
2
3use crate::data_structures::{model, texture};
4
5pub fn diffuse_normal_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
6    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
7        entries: &[
8            wgpu::BindGroupLayoutEntry {
9                binding: 0,
10                visibility: wgpu::ShaderStages::FRAGMENT,
11                ty: wgpu::BindingType::Texture {
12                    multisampled: false,
13                    view_dimension: wgpu::TextureViewDimension::D2,
14                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
15                },
16                count: None,
17            },
18            wgpu::BindGroupLayoutEntry {
19                binding: 1,
20                visibility: wgpu::ShaderStages::FRAGMENT,
21                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
22                count: None,
23            },
24            wgpu::BindGroupLayoutEntry {
25                binding: 2,
26                visibility: wgpu::ShaderStages::FRAGMENT,
27                ty: wgpu::BindingType::Texture {
28                    multisampled: false,
29                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
30                    view_dimension: wgpu::TextureViewDimension::D2,
31                },
32                count: None,
33            },
34            wgpu::BindGroupLayoutEntry {
35                binding: 3,
36                visibility: wgpu::ShaderStages::FRAGMENT,
37                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
38                count: None,
39            },
40        ],
41        label: Some("Model texture_bind_group_layout"),
42    })
43}
44
45#[cfg(target_arch = "wasm32")]
46pub fn format_url(file_name: &str) -> reqwest::Url {
47    let window = web_sys::window().unwrap();
48    let location = window.location();
49    let mut origin = location.origin().unwrap();
50    if !origin.ends_with("learn-wgpu") {
51        origin = format!("{}/assets", origin);
52    }
53    let base = reqwest::Url::parse(&format!("{}/", origin,)).unwrap();
54    base.join(file_name).unwrap()
55}
56
57pub async fn load_string(file_name: &str) -> anyhow::Result<String> {
58    #[cfg(target_arch = "wasm32")]
59    let txt = {
60        let url = format_url(file_name);
61        reqwest::get(url).await?.text().await?
62    };
63    #[cfg(not(target_arch = "wasm32"))]
64    let txt = {
65        // TODO: pass env for absolute path from lib caller
66        let path = std::path::Path::new("./")
67            .join("assets")
68            .join(file_name);
69        tokio::fs::read_to_string(path).await?
70    };
71
72    Ok(txt)
73}
74
75pub async fn load_binary(file_name: &str) -> anyhow::Result<Vec<u8>> {
76    #[cfg(target_arch = "wasm32")]
77    let data = {
78        let url = format_url(file_name);
79        reqwest::get(url).await?.bytes().await?.to_vec()
80    };
81    #[cfg(not(target_arch = "wasm32"))]
82    // TODO make async
83    let data = {
84        // TODO: pass env for absolute path from lib caller
85        let path = std::path::Path::new("./")
86            .join("assets")
87            .join(file_name);
88        tokio::fs::read(path).await?
89    };
90
91    Ok(data)
92}
93
94pub async fn load_texture(
95    file_name: &str,
96    is_normal_map: bool,
97    device: &wgpu::Device,
98    queue: &wgpu::Queue,
99    format: Option<&str>,
100) -> anyhow::Result<texture::Texture> {
101    let data = load_binary(file_name).await?;
102    texture::Texture::from_bytes(device, queue, &data, file_name, format, is_normal_map)
103}
104
105pub async fn load_textures(
106    file_name: &str,
107    queue: &wgpu::Queue,
108    device: &wgpu::Device,
109    layout: &wgpu::BindGroupLayout,
110) -> anyhow::Result<(Vec<model::Material>, Vec<tobj::Model>)> {
111    let obj_text: String = load_string(file_name).await?;
112    // TODO: also make async if not wasm
113    let obj_cursor = Cursor::new(obj_text);
114    let mut obj_reader = BufReader::new(obj_cursor);
115
116    let (models, obj_materials) = tobj::load_obj_buf_async(
117        &mut obj_reader,
118        &tobj::LoadOptions {
119            triangulate: true,
120            single_index: true,
121            ..Default::default()
122        },
123        |p| async move {
124            let mat_text = load_string(&p)
125                .await
126                .expect(format!("Material Texture not found for {p}.").as_str());
127            tobj::load_mtl_buf(&mut BufReader::new(Cursor::new(mat_text)))
128        },
129    )
130    .await?;
131
132    // We rather use a default normal map when none is passed instead of changing the pipeline
133    let mut materials = Vec::new();
134    for m in obj_materials? {
135        if let Some(m_diffuse_texture) = &m.diffuse_texture {
136            let diffuse_texture =
137                load_texture(&m_diffuse_texture, false, device, queue, None).await?;
138            let normal_texture = match &m.normal_texture {
139                Some(m_normal_texture) => {
140                    load_texture(&m_normal_texture, true, device, queue, None).await?
141                },
142                None => texture::Texture::create_default_normal_map(1, 1, device, queue)
143            };
144            if let Ok(model) = model::Material::new(
145                device,
146                &m.name,
147                diffuse_texture,
148                normal_texture,
149                layout,
150            ) {
151                materials.push(model);
152            } else {
153                log::warn!("Failed to create material for mtl ({}) in obj ({})", m.name, file_name);
154            }
155        } else {
156            log::error!("This material's mtl ({file_name}) references no texture.");
157        }
158    }
159    Ok((materials, models))
160}