use std::io::{BufReader, Cursor}; use cfg_if::cfg_if; use image::codecs::hdr::HdrDecoder; use wgpu::util::DeviceExt; use crate::{model, texture}; #[cfg(target_arch = "wasm32")] fn format_url(file_name: &str) -> reqwest::Url { let window = web_sys::window().unwrap(); let location = window.location(); let mut origin = location.origin().unwrap(); if !origin.ends_with("learn-wgpu") { origin = format!("{}/learn-wgpu", origin); } let base = reqwest::Url::parse(&format!("{}/", origin,)).unwrap(); base.join(file_name).unwrap() } pub async fn load_string(file_name: &str) -> anyhow::Result { cfg_if! { if #[cfg(target_arch = "wasm32")] { let url = format_url(file_name); let txt = reqwest::get(url) .await? .text() .await?; } else { let path = std::path::Path::new(env!("OUT_DIR")) .join("res") .join(file_name); let txt = std::fs::read_to_string(path)?; } } Ok(txt) } pub async fn load_binary(file_name: &str) -> anyhow::Result> { cfg_if! { if #[cfg(target_arch = "wasm32")] { let url = format_url(file_name); let data = reqwest::get(url) .await? .bytes() .await? .to_vec(); } else { let path = std::path::Path::new(env!("OUT_DIR")) .join("res") .join(file_name); let data = std::fs::read(path)?; } } Ok(data) } pub async fn load_texture( file_name: &str, is_normal_map: bool, device: &wgpu::Device, queue: &wgpu::Queue, ) -> anyhow::Result { let data = load_binary(file_name).await?; texture::Texture::from_bytes(device, queue, &data, file_name, is_normal_map) } pub async fn load_model( file_name: &str, device: &wgpu::Device, queue: &wgpu::Queue, layout: &wgpu::BindGroupLayout, ) -> anyhow::Result { let obj_text = load_string(file_name).await?; let obj_cursor = Cursor::new(obj_text); let mut obj_reader = BufReader::new(obj_cursor); let (models, obj_materials) = tobj::load_obj_buf_async( &mut obj_reader, &tobj::LoadOptions { triangulate: true, single_index: true, ..Default::default() }, |p| async move { let mat_text = load_string(&p).await.unwrap(); tobj::load_mtl_buf(&mut BufReader::new(Cursor::new(mat_text))) }, ) .await?; let mut materials = Vec::new(); for m in obj_materials? { let diffuse_texture = load_texture(&m.diffuse_texture, false, device, queue).await?; let normal_texture = load_texture(&m.normal_texture, true, device, queue).await?; materials.push(model::Material::new( device, &m.name, diffuse_texture, normal_texture, layout, )); } let meshes = models .into_iter() .map(|m| { let mut vertices = (0..m.mesh.positions.len() / 3) .map(|i| model::ModelVertex { position: [ m.mesh.positions[i * 3], m.mesh.positions[i * 3 + 1], m.mesh.positions[i * 3 + 2], ], tex_coords: [m.mesh.texcoords[i * 2], m.mesh.texcoords[i * 2 + 1]], normal: [ m.mesh.normals[i * 3], m.mesh.normals[i * 3 + 1], m.mesh.normals[i * 3 + 2], ], // We'll calculate these later tangent: [0.0; 3], bitangent: [0.0; 3], }) .collect::>(); let indices = &m.mesh.indices; let mut triangles_included = vec![0; vertices.len()]; // Calculate tangents and bitangets. We're going to // use the triangles, so we need to loop through the // indices in chunks of 3 for c in indices.chunks(3) { let v0 = vertices[c[0] as usize]; let v1 = vertices[c[1] as usize]; let v2 = vertices[c[2] as usize]; let pos0: cgmath::Vector3<_> = v0.position.into(); let pos1: cgmath::Vector3<_> = v1.position.into(); let pos2: cgmath::Vector3<_> = v2.position.into(); let uv0: cgmath::Vector2<_> = v0.tex_coords.into(); let uv1: cgmath::Vector2<_> = v1.tex_coords.into(); let uv2: cgmath::Vector2<_> = v2.tex_coords.into(); // Calculate the edges of the triangle let delta_pos1 = pos1 - pos0; let delta_pos2 = pos2 - pos0; // This will give us a direction to calculate the // tangent and bitangent let delta_uv1 = uv1 - uv0; let delta_uv2 = uv2 - uv0; // Solving the following system of equations will // give us the tangent and bitangent. // delta_pos1 = delta_uv1.x * T + delta_u.y * B // delta_pos2 = delta_uv2.x * T + delta_uv2.y * B // Luckily, the place I found this equation provided // the solution! let r = 1.0 / (delta_uv1.x * delta_uv2.y - delta_uv1.y * delta_uv2.x); let tangent = (delta_pos1 * delta_uv2.y - delta_pos2 * delta_uv1.y) * r; // We flip the bitangent to enable right-handed normal // maps with wgpu texture coordinate system let bitangent = (delta_pos2 * delta_uv1.x - delta_pos1 * delta_uv2.x) * -r; // We'll use the same tangent/bitangent for each vertex in the triangle vertices[c[0] as usize].tangent = (tangent + cgmath::Vector3::from(vertices[c[0] as usize].tangent)).into(); vertices[c[1] as usize].tangent = (tangent + cgmath::Vector3::from(vertices[c[1] as usize].tangent)).into(); vertices[c[2] as usize].tangent = (tangent + cgmath::Vector3::from(vertices[c[2] as usize].tangent)).into(); vertices[c[0] as usize].bitangent = (bitangent + cgmath::Vector3::from(vertices[c[0] as usize].bitangent)).into(); vertices[c[1] as usize].bitangent = (bitangent + cgmath::Vector3::from(vertices[c[1] as usize].bitangent)).into(); vertices[c[2] as usize].bitangent = (bitangent + cgmath::Vector3::from(vertices[c[2] as usize].bitangent)).into(); // Used to average the tangents/bitangents triangles_included[c[0] as usize] += 1; triangles_included[c[1] as usize] += 1; triangles_included[c[2] as usize] += 1; } // Average the tangents/bitangents for (i, n) in triangles_included.into_iter().enumerate() { let denom = 1.0 / n as f32; let mut v = &mut vertices[i]; v.tangent = (cgmath::Vector3::from(v.tangent) * denom).into(); v.bitangent = (cgmath::Vector3::from(v.bitangent) * denom).into(); } let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("{:?} Vertex Buffer", file_name)), contents: bytemuck::cast_slice(&vertices), usage: wgpu::BufferUsages::VERTEX, }); let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("{:?} Index Buffer", file_name)), contents: bytemuck::cast_slice(&m.mesh.indices), usage: wgpu::BufferUsages::INDEX, }); model::Mesh { name: file_name.to_string(), vertex_buffer, index_buffer, num_elements: m.mesh.indices.len() as u32, material: m.mesh.material_id.unwrap_or(0), } }) .collect::>(); Ok(model::Model { meshes, materials }) } pub struct HdrLoader { texture_format: wgpu::TextureFormat, equirect_layout: wgpu::BindGroupLayout, equirect_to_cubemap: wgpu::ComputePipeline, } impl HdrLoader { pub fn new(device: &wgpu::Device) -> Self { let module = device.create_shader_module(wgpu::include_wgsl!("equirectangular.wgsl")); let texture_format = wgpu::TextureFormat::Rgba32Float; let equirect_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("HdrLoader::equirect_layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: false }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::COMPUTE, ty: wgpu::BindingType::StorageTexture { access: wgpu::StorageTextureAccess::WriteOnly, format: texture_format, view_dimension: wgpu::TextureViewDimension::D2Array, }, count: None, }, ], }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, bind_group_layouts: &[&equirect_layout], push_constant_ranges: &[], }); let equirect_to_cubemap = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { label: Some("equirect_to_cubemap"), layout: Some(&pipeline_layout), module: &module, entry_point: "compute_equirect_to_cubemap", }); Self { equirect_to_cubemap, texture_format, equirect_layout, } } pub fn from_equirectangular_bytes( &self, device: &wgpu::Device, queue: &wgpu::Queue, data: &[u8], dst_size: u32, label: Option<&str>, ) -> anyhow::Result { let hdr_decoder = HdrDecoder::new(Cursor::new(data))?; let meta = hdr_decoder.metadata(); let mut pixels = vec![[0.0, 0.0, 0.0, 0.0]; meta.width as usize * meta.height as usize]; hdr_decoder.read_image_transform( |pix| { let rgb = pix.to_hdr(); [rgb.0[0], rgb.0[1], rgb.0[2], 1.0f32] }, &mut pixels[..], )?; let src = texture::Texture::create_2d_texture( device, meta.width, meta.height, self.texture_format, wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, wgpu::FilterMode::Linear, None, ); queue.write_texture( wgpu::ImageCopyTexture { texture: &src.texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, &bytemuck::cast_slice(&pixels), wgpu::ImageDataLayout { offset: 0, bytes_per_row: Some(src.size.width * std::mem::size_of::<[f32; 4]>() as u32), rows_per_image: Some(src.size.height), }, src.size, ); let dst = texture::CubeTexture::create_2d( device, dst_size, dst_size, self.texture_format, 1, wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING, wgpu::FilterMode::Nearest, label, ); let dst_view = dst.texture().create_view(&wgpu::TextureViewDescriptor { label, dimension: Some(wgpu::TextureViewDimension::D2Array), // array_layer_count: Some(6), ..Default::default() }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label, layout: &self.equirect_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&src.view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&dst_view), }, ], }); let mut encoder = device.create_command_encoder(&Default::default()); let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label }); let num_workgroups = (dst_size + 15) / 16; pass.set_pipeline(&self.equirect_to_cubemap); pass.set_bind_group(0, &bind_group, &[]); pass.dispatch_workgroups(num_workgroups, num_workgroups, 6); drop(pass); queue.submit([encoder.finish()]); Ok(dst) } }