diff --git a/Cargo.lock b/Cargo.lock index d8faa547..0de4d2ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -535,6 +535,7 @@ dependencies = [ "pico-args", "threadpool", "walkdir", + "wgpu", ] [[package]] @@ -1340,6 +1341,17 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.12.1", +] + [[package]] name = "futures-io" version = "0.3.28" @@ -1986,6 +1998,7 @@ dependencies = [ "console", "cpal", "env_logger", + "futures-intrusive", "getrandom", "glam 0.24.1", "gltf", diff --git a/Cargo.toml b/Cargo.toml index e88f0f69..63192542 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,3 +66,4 @@ opt-level = 3 # incremental = false # codegen-units = 1 debug = true +debug-assertions = true diff --git a/README.md b/README.md index 9a819826..b65e70e3 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,9 @@ Hopefully one day it will be used in a real game 😃 ## TODO +- stop using release build, switch to dev but with optimizations enabled, but leave debug=true for release builds. +- fix resolve_path function in file_loader so it can be used outside the context of the engine! +- path in error messages in file loader module (see TODO: there) - renderer should not need to know about the game state! - lift state from the ui overlay into the game state to not need to pass state around - convert all paths to be relative to the bin diff --git a/clikari/Cargo.toml b/clikari/Cargo.toml index da33deb8..b949bd30 100644 --- a/clikari/Cargo.toml +++ b/clikari/Cargo.toml @@ -13,7 +13,7 @@ path = "src/main.rs" # winit.workspace = true ikari.workspace = true log.workspace = true -# wgpu.workspace = true +wgpu.workspace = true anyhow.workspace = true env_logger.workspace = true # profiling.workspace = true diff --git a/clikari/src/main.rs b/clikari/src/main.rs index 3b6a5b41..433b7fe9 100644 --- a/clikari/src/main.rs +++ b/clikari/src/main.rs @@ -1,29 +1,33 @@ mod skybox_processor; mod texture_compressor; +use std::str::FromStr; + use skybox_processor::SkyboxProcessorArgs; use texture_compressor::TextureCompressorArgs; const HELP: &str = "\ ikari cli -Usage: clikari [COMMAND] [OPTIONS] - -Commands: - compress_textures - process_skybox +Usage: clikari --command CMD [OPTIONS] Options: - --help Optional Display this help message + --command CMD Required The command to run. Possible values include: + compress_textures + process_skybox + --help Optional Display this help message "; const TEXTURE_COMPRESSOR_HELP: &str = "\ -Compress all textures found in a given folder by recursive search +Compress all textures found in a given folder by recursive search. The compressed textures +will be stored at the same path with the same name/extension but with the '_compressed' suffix. +It will work with gltf files (ikari will look for a '_compressed' counterpart) but only if the +texture is in a separate file and not embedded in the gltf file. Usage: clikari compress_textures --search_folder /path/to/folder [OPTIONS] Options: - --search_folder FOLDERNAME Required The folder to search in to find textures to compress + --search_folder FOLDER Required The folder to search in to find textures to compress --threads_per_texture VAL Optional The number of threads that will be used per texture. Textures will also be processed in parallel if possible, according to the formula: (cpu_count / threads_per_texture).ceil(). @@ -40,12 +44,30 @@ Pre-process skybox file(s) for use in ikari Usage: clikari process_skybox --background_path /path/to/background.jpg [OPTIONS] Options: - --background_path FILEPATH Required The background image of the skybox (this will be the background of your scene) - --environment_hdr_path FILEPATH Optional The hdr environment map (used for ambient lighting and reflections) - Background image is used if not defined - --help Optional Display this help message + --background_path FILE Required The background image of the skybox (this will be the background of your scene) + --environment_hdr_path FILE Optional The hdr environment map (used for ambient lighting and reflections) + Background image is used if option is not supplied + --out_path Required Output file path + --help Optional Display this help message "; +enum CommandName { + CompressTextures, + ProcessSkybox, +} + +impl FromStr for CommandName { + type Err = &'static str; + + fn from_str(input: &str) -> Result { + match input { + "compress_textures" => Ok(CommandName::CompressTextures), + "process_skybox" => Ok(CommandName::ProcessSkybox), + _ => Err("Command not recognized"), + } + } +} + enum Command { Help, CompressTextures(TextureCompressorArgs), @@ -64,41 +86,53 @@ impl Command { pub fn from_env() -> Result { let mut args = pico_args::Arguments::from_env(); - if args.contains("compress_textures") { - if args.contains("--help") { - return Ok(Self::CompressTexturesHelp); + let error_mapper = |err| ArgParseError::Root(format!("{err}")); + let command_result: Result = + args.value_from_str("--command").map_err(error_mapper); + + match command_result { + Ok(CommandName::CompressTextures) => { + if args.contains("--help") { + return Ok(Self::CompressTexturesHelp); + } + + let error_mapper = |err| ArgParseError::CompressTextures(format!("{err}")); + return Ok(Self::CompressTextures(TextureCompressorArgs { + search_folder: args + .value_from_str("--search_folder") + .map_err(error_mapper)?, + threads_per_texture: args + .opt_value_from_str("--threads_per_texture") + .map_err(error_mapper)?, + force: args.contains("--force"), + })); } - - return Ok(Self::CompressTextures(TextureCompressorArgs { - search_folder: args - .value_from_str("--search_folder") - .map_err(|err| ArgParseError::CompressTextures(format!("{err}")))?, - threads_per_texture: args - .opt_value_from_str("--threads_per_texture") - .map_err(|err| ArgParseError::CompressTextures(format!("{err}")))?, - force: args.contains("--force"), - })); - } - - if args.contains("process_skybox") { - if args.contains("--help") { - return Ok(Self::ProcessSkyboxHelp); + Ok(CommandName::ProcessSkybox) => { + if args.contains("--help") { + return Ok(Self::ProcessSkyboxHelp); + } + + let error_mapper = |err| ArgParseError::ProcessSkybox(format!("{err}")); + return Ok(Self::ProcessSkybox(SkyboxProcessorArgs { + background_path: args + .value_from_str("--background_path") + .map_err(error_mapper)?, + environment_hdr_path: args + .opt_value_from_str("--environment_hdr_path") + .map_err(error_mapper)?, + out_path: args.value_from_str("--out_path").map_err(error_mapper)?, + })); } - - return Ok(Self::ProcessSkybox(SkyboxProcessorArgs { - background_path: args - .value_from_str("--background_path") - .map_err(|err| ArgParseError::ProcessSkybox(format!("{err}")))?, - environment_hdr_path: args - .opt_value_from_str("--environment_hdr_path") - .map_err(|err| ArgParseError::ProcessSkybox(format!("{err}")))?, - })); - } + _ => {} + }; if args.contains("--help") { return Ok(Self::Help); } + // only show missing command error if --help is not supplied + command_result?; + Err(ArgParseError::Root(String::from("No command specified"))) } } diff --git a/clikari/src/skybox_processor.rs b/clikari/src/skybox_processor.rs index 0a633b46..10097d08 100644 --- a/clikari/src/skybox_processor.rs +++ b/clikari/src/skybox_processor.rs @@ -1,18 +1,130 @@ use std::path::PathBuf; +use ikari::{ + renderer::{ + BaseRenderer, BindedSkybox, Renderer, SkyboxBackgroundPath, SkyboxHDREnvironmentPath, + }, + texture::Texture, +}; +use image::{ImageBuffer, Rgba}; + +const DXC_PATH: &str = "dxc/"; + pub struct SkyboxProcessorArgs { pub background_path: PathBuf, pub environment_hdr_path: Option, + pub out_path: PathBuf, } pub fn run(args: SkyboxProcessorArgs) { - // let base_render_state = { - // let backends = if cfg!(target_os = "windows") { - // wgpu::Backends::from(wgpu::Backend::Dx12) - // // wgpu::Backends::PRIMARY - // } else { - // wgpu::Backends::PRIMARY - // }; - // BaseRenderer::new(&window, backends, wgpu::PresentMode::AutoNoVsync).await - // }; + if let Err(err) = ikari::block_on(run_internal(args)) { + log::error!("Error: {err}\n{}", err.backtrace()); + } +} + +pub async fn run_internal(args: SkyboxProcessorArgs) -> anyhow::Result<()> { + let backends = if cfg!(target_os = "windows") { + wgpu::Backends::from(wgpu::Backend::Dx12) + // wgpu::Backends::PRIMARY + } else { + wgpu::Backends::PRIMARY + }; + + let base_renderer = BaseRenderer::offscreen(backends, Some(DXC_PATH.into())).await?; + let mut renderer = + Renderer::new(base_renderer, wgpu::TextureFormat::Bgra8Unorm, (1, 1)).await?; + + renderer.base.device.start_capture(); + + let bindable_skybox = ikari::asset_loader::make_bindable_skybox( + SkyboxBackgroundPath::Equirectangular { + image_path: args + .background_path + .to_str() + .expect("background_path was not valid unicode"), + }, + args.environment_hdr_path + .as_ref() + .map( + |environment_hdr_path| SkyboxHDREnvironmentPath::Equirectangular { + image_path: environment_hdr_path + .to_str() + .expect("environment_hdr_path was not valid unicode"), + }, + ), + ) + .await?; + + let binded_skybox = + ikari::asset_loader::bind_skybox(&renderer.base, &renderer.constant_data, bindable_skybox)?; + + let BindedSkybox { + background, + diffuse_environment_map, + specular_environment_map, + } = binded_skybox; + + { + let texture = background; + let path = "background.png"; + + let texture_bytes = texture.to_bytes(&renderer.base).await?; + + // TODO: use ikari:: texture compression module to compress it as srgb + // basis_universal doesn't support BC6 at the moment + + // let buffer = ImageBuffer::, _>::from_raw( + // texture.size.width, + // texture.size.height * texture.size.depth_or_array_layers, + // texture + // .to_bytes(&renderer.base) + // .await? + // .iter() + // .flatten() + // .copied() + // .collect::>(), + // ) + // .unwrap(); + // buffer.save(path).unwrap(); + } + + renderer.base.device.stop_capture(); + + /* { + let texture = diffuse_environment_map; + let path = "diffuse_environment_map.png"; + let buffer = ImageBuffer::, _>::from_raw( + texture.size.width * texture.size.depth_or_array_layers, + texture.size.height, + texture + .to_bytes(&renderer.base) + .await? + .iter() + .flatten() + .copied() + .collect::>(), + ) + .unwrap(); + buffer.save(path).unwrap(); + } + + { + let texture = specular_environment_map; + let path = "specular_environment_map.png"; + let buffer = ImageBuffer::, _>::from_raw( + texture.size.width * texture.size.depth_or_array_layers, + texture.size.height, + texture + .to_bytes(&renderer.base) + .await? + .iter() + .flatten() + .copied() + .collect::>(), + ) + .unwrap(); + buffer.save(path).unwrap(); + } */ + + Ok(()) } diff --git a/ikari/dxc/dxcompiler.dll b/dxc/dxcompiler.dll similarity index 100% rename from ikari/dxc/dxcompiler.dll rename to dxc/dxcompiler.dll diff --git a/ikari/dxc/dxil.dll b/dxc/dxil.dll similarity index 100% rename from ikari/dxc/dxil.dll rename to dxc/dxil.dll diff --git a/example_game/src/main.rs b/example_game/src/main.rs index 3eeb365e..c9d251df 100644 --- a/example_game/src/main.rs +++ b/example_game/src/main.rs @@ -5,7 +5,7 @@ use ikari::scene::*; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; -const DXC_PATH: &str = "ikari/dxc/"; +const DXC_PATH: &str = "dxc/"; async fn start() { let run_result = async { @@ -55,7 +55,7 @@ async fn start() { let (base_renderer, surface_data) = { let backends = if cfg!(target_os = "windows") { - wgpu::Backends::from(wgpu::Backend::Dx12) + wgpu::Backends::from(wgpu::Backend::Vulkan) // wgpu::Backends::PRIMARY } else { wgpu::Backends::PRIMARY diff --git a/ikari/Cargo.toml b/ikari/Cargo.toml index 885531f1..d5ed8679 100644 --- a/ikari/Cargo.toml +++ b/ikari/Cargo.toml @@ -86,6 +86,7 @@ iced_aw = { git = "https://github.com/iced-rs/iced_aw.git", branch = "main", def "card", ] } wasm_thread = { version = "0.2.0", features = ["es_modules"] } +futures-intrusive = "0.5.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] basis-universal = "0.3.0" zstd = "0.12.4" diff --git a/ikari/src/asset_loader.rs b/ikari/src/asset_loader.rs index 0a892850..890df632 100644 --- a/ikari/src/asset_loader.rs +++ b/ikari/src/asset_loader.rs @@ -392,79 +392,11 @@ impl AssetLoader { let (next_skybox_id, next_skybox_background, next_skybox_env_hdr) = pending_skyboxes.lock().unwrap().remove(0); - let do_load = || async { - profiling::scope!("Load skybox", &next_skybox_id); - - let background = match next_skybox_background { - SkyboxBackgroundPath::Equirectangular { image_path } => { - BindableSkyboxBackground::Equirectangular { - image: image::load_from_memory( - &crate::file_loader::read(image_path).await?, - )? - .to_rgba8() - .into(), - } - } - SkyboxBackgroundPath::Cube { face_image_paths } => { - async fn to_img(img_path: &str) -> Result { - Ok(image::load_from_memory( - &crate::file_loader::read(img_path).await?, - )? - .to_rgba8() - .into()) - } - - BindableSkyboxBackground::Cube { - images: CubemapImages { - pos_x: to_img(face_image_paths[0]).await?, - neg_x: to_img(face_image_paths[1]).await?, - pos_y: to_img(face_image_paths[2]).await?, - neg_y: to_img(face_image_paths[3]).await?, - pos_z: to_img(face_image_paths[4]).await?, - neg_z: to_img(face_image_paths[5]).await?, - }, - } - } - }; - - let mut environment_hdr = None; - if let Some(SkyboxHDREnvironmentPath::Equirectangular { image_path }) = - next_skybox_env_hdr - { - let image_bytes = crate::file_loader::read(image_path).await?; - let skybox_rad_texture_decoder = - image::codecs::hdr::HdrDecoder::new(image_bytes.as_slice())?; - let skybox_rad_texture_dimensions = { - let md = skybox_rad_texture_decoder.metadata(); - (md.width, md.height) - }; - let skybox_rad_texture_decoded: Vec = { - let rgb_values = skybox_rad_texture_decoder.read_image_hdr()?; - rgb_values - .iter() - .copied() - .flat_map(|rbg| { - rbg.to_rgba() - .0 - .into_iter() - .map(|c| Float16(half::f16::from_f32(c))) - }) - .collect() - }; - - environment_hdr = - Some(BindableSkyboxHDREnvironment::Equirectangular { - image: skybox_rad_texture_decoded, - dimensions: skybox_rad_texture_dimensions, - }); - } + profiling::scope!("Load skybox", &next_skybox_id); - anyhow::Ok(BindableSkybox { - background, - environment_hdr, - }) - }; - match do_load().await { + match make_bindable_skybox(next_skybox_background, next_skybox_env_hdr) + .await + { Ok(result) => { let _replaced_ignored = bindable_skyboxes .lock() @@ -487,6 +419,75 @@ impl AssetLoader { } } +pub async fn make_bindable_skybox<'a>( + background: SkyboxBackgroundPath<'a>, + environment_hdr: Option>, +) -> Result { + let background = match background { + SkyboxBackgroundPath::Equirectangular { image_path } => { + BindableSkyboxBackground::Equirectangular { + image: image::load_from_memory(&crate::file_loader::read(image_path).await?)? + .to_rgba8() + .into(), + } + } + SkyboxBackgroundPath::Cube { face_image_paths } => { + async fn to_img(img_path: &str) -> Result { + Ok( + image::load_from_memory(&crate::file_loader::read(img_path).await?)? + .to_rgba8() + .into(), + ) + } + + BindableSkyboxBackground::Cube { + images: CubemapImages { + pos_x: to_img(face_image_paths[0]).await?, + neg_x: to_img(face_image_paths[1]).await?, + pos_y: to_img(face_image_paths[2]).await?, + neg_y: to_img(face_image_paths[3]).await?, + pos_z: to_img(face_image_paths[4]).await?, + neg_z: to_img(face_image_paths[5]).await?, + }, + } + } + }; + + let mut bindable_environment_hdr = None; + if let Some(SkyboxHDREnvironmentPath::Equirectangular { image_path }) = environment_hdr { + let image_bytes = crate::file_loader::read(image_path).await?; + let skybox_rad_texture_decoder = + image::codecs::hdr::HdrDecoder::new(image_bytes.as_slice())?; + let skybox_rad_texture_dimensions = { + let md = skybox_rad_texture_decoder.metadata(); + (md.width, md.height) + }; + let skybox_rad_texture_decoded: Vec = { + let rgb_values = skybox_rad_texture_decoder.read_image_hdr()?; + rgb_values + .iter() + .copied() + .flat_map(|rbg| { + rbg.to_rgba() + .0 + .into_iter() + .map(|c| Float16(half::f16::from_f32(c))) + }) + .collect() + }; + + bindable_environment_hdr = Some(BindableSkyboxHDREnvironment::Equirectangular { + image: skybox_rad_texture_decoded, + dimensions: skybox_rad_texture_dimensions, + }); + } + + Ok(BindableSkybox { + background, + environment_hdr: bindable_environment_hdr, + }) +} + impl ThreadedSceneBinder { pub fn new( bindable_scenes: Arc>>, @@ -734,111 +735,110 @@ impl BindScene for TimeSlicedSceneBinder { } } -impl ThreadedSkyboxBinder { - #[profiling::function] - fn bind_whole_skybox( - base_renderer: &BaseRenderer, - renderer_constant_data: &RendererConstantData, - bindable_skybox: BindableSkybox, - ) -> Result { - let start = crate::time::Instant::now(); - - let background = match bindable_skybox.background { - BindableSkyboxBackground::Equirectangular { image } => { - let er_background_texture = Texture::from_decoded_image( - base_renderer, - image.as_bytes(), - image.dimensions(), - 1, - None, - Some(wgpu::TextureFormat::Rgba8UnormSrgb), - false, - &SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }, - )?; - - Texture::create_cubemap_from_equirectangular( - base_renderer, - renderer_constant_data, - None, - &er_background_texture, - false, // an artifact occurs between the edges of the texture with mipmaps enabled - ) - } - BindableSkyboxBackground::Cube { images } => Texture::create_cubemap( +pub fn bind_skybox( + base_renderer: &BaseRenderer, + renderer_constant_data: &RendererConstantData, + bindable_skybox: BindableSkybox, +) -> Result { + let start = crate::time::Instant::now(); + + let background = match bindable_skybox.background { + BindableSkyboxBackground::Equirectangular { image } => { + let er_background_texture = Texture::from_decoded_image( base_renderer, - images, - Some("cubemap_skybox_texture"), - wgpu::TextureFormat::Rgba8UnormSrgb, + image.as_bytes(), + image.dimensions(), + 1, + Some("er_skybox_texture"), + Some(wgpu::TextureFormat::Rgba8UnormSrgb), false, - ), - }; + &SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }, + )?; + + Texture::create_cubemap_from_equirectangular( + base_renderer, + renderer_constant_data, + wgpu::TextureFormat::Rgba8UnormSrgb, + Some("cubemap_skybox_texture"), + &er_background_texture, + false, // an artifact occurs between the edges of the texture with mipmaps enabled + )? + } + BindableSkyboxBackground::Cube { images } => Texture::create_cubemap( + base_renderer, + images, + Some("cubemap_skybox_texture"), + wgpu::TextureFormat::Rgba8UnormSrgb, + false, + ), + }; - let er_to_cube_texture; - let hdr_env_texture = match bindable_skybox.environment_hdr { - Some(BindableSkyboxHDREnvironment::Equirectangular { image, dimensions }) => { - let er_hdr_env_texture = Texture::from_decoded_image( - base_renderer, - bytemuck::cast_slice(&image), - dimensions, - 1, - None, - Some(wgpu::TextureFormat::Rgba16Float), - false, - &SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }, - )?; - - er_to_cube_texture = Texture::create_cubemap_from_equirectangular( - base_renderer, - renderer_constant_data, - None, - &er_hdr_env_texture, - false, - ); + let er_to_cube_texture; + let hdr_env_texture = match bindable_skybox.environment_hdr { + Some(BindableSkyboxHDREnvironment::Equirectangular { image, dimensions }) => { + let er_hdr_env_texture = Texture::from_decoded_image( + base_renderer, + bytemuck::cast_slice(&image), + dimensions, + 1, + None, + Some(wgpu::TextureFormat::Rgba16Float), + false, + &SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }, + )?; + + er_to_cube_texture = Texture::create_cubemap_from_equirectangular( + base_renderer, + renderer_constant_data, + wgpu::TextureFormat::Rgba16Float, + None, + &er_hdr_env_texture, + false, + )?; - &er_to_cube_texture - } - None => &background, - }; + &er_to_cube_texture + } + None => &background, + }; - let diffuse_environment_map = Texture::create_diffuse_env_map( - base_renderer, - renderer_constant_data, - Some("diffuse env map"), - hdr_env_texture, - false, - ); + let diffuse_environment_map = Texture::create_diffuse_env_map( + base_renderer, + renderer_constant_data, + Some("diffuse env map"), + hdr_env_texture, + false, + ); - let specular_environment_map = Texture::create_specular_env_map( - base_renderer, - renderer_constant_data, - Some("specular env map"), - hdr_env_texture, - ); + let specular_environment_map = Texture::create_specular_env_map( + base_renderer, + renderer_constant_data, + Some("specular env map"), + hdr_env_texture, + ); - log::debug!("skybox bind time: {:?}", start.elapsed()); + log::debug!("skybox bind time: {:?}", start.elapsed()); - Ok(BindedSkybox { - background, - diffuse_environment_map, - specular_environment_map, - }) - } + Ok(BindedSkybox { + background, + diffuse_environment_map, + specular_environment_map, + }) } impl BindSkybox for ThreadedSkyboxBinder { @@ -863,12 +863,11 @@ impl BindSkybox for ThreadedSkyboxBinder { crate::thread::spawn(move || { for (skybox_id, bindable_skybox) in bindable_skyboxes { - let binded_skybox_result = Self::bind_whole_skybox( + match bind_skybox( &base_renderer.clone(), &renderer_constant_data, bindable_skybox, - ); - match binded_skybox_result { + ) { Ok(result) => { let _replaced_ignored = loaded_skyboxes_clone .lock() diff --git a/ikari/src/file_loader.rs b/ikari/src/file_loader.rs index 4d346a42..c309673b 100644 --- a/ikari/src/file_loader.rs +++ b/ikari/src/file_loader.rs @@ -66,7 +66,9 @@ pub fn map_js_err(result: std::result::Result) -> anyhow::Result< #[cfg(not(target_arch = "wasm32"))] pub async fn read(path: &str) -> anyhow::Result> { - Ok(std::fs::read(resolve_path(path))?) + let resolved_path = resolve_path(path); + // TODO: add the same path in error message everywhere here + Ok(std::fs::read(&resolved_path).map_err(|err| anyhow::anyhow!("{err} ({resolved_path})"))?) } #[cfg(target_arch = "wasm32")] diff --git a/ikari/src/game.rs b/ikari/src/game.rs index 71358edb..4cfc2420 100644 --- a/ikari/src/game.rs +++ b/ikari/src/game.rs @@ -177,6 +177,27 @@ pub async fn init_game_state( surface_data: &SurfaceData, window: &winit::window::Window, ) -> Result { + log::info!("Controls:"); + [ + "Look Around: Mouse", + "Move Around: WASD, Space Bar, Ctrl", + "Adjust Speed: Scroll or Up/Down Arrow Keys", + "Adjust Render Scale: Z / X", + "Adjust Exposure: E / R", + "Adjust Bloom Threshold: T / Y", + "Pause/Resume Animations: P", + "Toggle Bloom Effect: B", + "Toggle Shadows: M", + "Toggle Wireframe: F", + "Toggle Collision Boxes: C", + "Draw Bounding Spheres: J", + "Open Options Menu: Tab", + ] + .iter() + .for_each(|line| { + log::info!(" {line}"); + }); + let mut physics_state = PhysicsState::new(); // create player diff --git a/ikari/src/renderer.rs b/ikari/src/renderer.rs index 8db29d22..a6c362cb 100644 --- a/ikari/src/renderer.rs +++ b/ikari/src/renderer.rs @@ -1092,6 +1092,7 @@ pub struct RendererConstantData { pub bloom_threshold_pipeline: wgpu::RenderPipeline, pub bloom_blur_pipeline: wgpu::RenderPipeline, pub equirectangular_to_cubemap_pipeline: wgpu::RenderPipeline, + pub equirectangular_to_cubemap_hdr_pipeline: wgpu::RenderPipeline, pub diffuse_env_map_gen_pipeline: wgpu::RenderPipeline, pub specular_env_map_gen_pipeline: wgpu::RenderPipeline, } @@ -1125,27 +1126,6 @@ impl Renderer { framebuffer_format: wgpu::TextureFormat, framebuffer_size: (u32, u32), ) -> Result { - log::info!("Controls:"); - [ - "Look Around: Mouse", - "Move Around: WASD, Space Bar, Ctrl", - "Adjust Speed: Scroll or Up/Down Arrow Keys", - "Adjust Render Scale: Z / X", - "Adjust Exposure: E / R", - "Adjust Bloom Threshold: T / Y", - "Pause/Resume Animations: P", - "Toggle Bloom Effect: B", - "Toggle Shadows: M", - "Toggle Wireframe: F", - "Toggle Collision Boxes: C", - "Draw Bounding Spheres: J", - "Open Options Menu: Tab", - ] - .iter() - .for_each(|line| { - log::info!(" {line}"); - }); - let unlit_mesh_shader = base .device .create_shader_module(wgpu::ShaderModuleDescriptor { @@ -1609,7 +1589,7 @@ impl Renderer { .create_render_pipeline(&skybox_pipeline_descriptor); let equirectangular_to_cubemap_color_targets = &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba16Float, + format: wgpu::TextureFormat::Rgba8UnormSrgb, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })]; @@ -1647,6 +1627,21 @@ impl Renderer { .device .create_render_pipeline(&equirectangular_to_cubemap_pipeline_descriptor); + let mut equirectangular_to_cubemap_hdr_pipeline_descriptor = + equirectangular_to_cubemap_pipeline_descriptor.clone(); + equirectangular_to_cubemap_hdr_pipeline_descriptor + .fragment + .as_mut() + .unwrap() + .targets = &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba16Float, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })]; + let equirectangular_to_cubemap_hdr_pipeline = base + .device + .create_render_pipeline(&equirectangular_to_cubemap_hdr_pipeline_descriptor); + let diffuse_env_map_color_targets = &[Some(wgpu::ColorTargetState { format: wgpu::TextureFormat::Rgba16Float, blend: Some(wgpu::BlendState::REPLACE), @@ -1869,6 +1864,7 @@ impl Renderer { bloom_threshold_pipeline, bloom_blur_pipeline, equirectangular_to_cubemap_pipeline, + equirectangular_to_cubemap_hdr_pipeline, diffuse_env_map_gen_pipeline, specular_env_map_gen_pipeline, }; @@ -2125,10 +2121,11 @@ impl Renderer { let skybox_texture = Texture::create_cubemap_from_equirectangular( &base, &constant_data, + wgpu::TextureFormat::Rgba8UnormSrgb, None, &skybox_image, false, // an artifact occurs between the edges of the texture with mipmaps enabled - ); + )?; let skybox_rad_texture = { let pixel_count = skybox_dim * skybox_dim; @@ -2158,10 +2155,11 @@ impl Renderer { Texture::create_cubemap_from_equirectangular( &base, &constant_data, + wgpu::TextureFormat::Rgba16Float, None, &texture_er, false, - ) + )? }; let skybox_rad_texture = &skybox_rad_texture; diff --git a/ikari/src/texture.rs b/ikari/src/texture.rs index 827205c4..fb7e43fd 100644 --- a/ikari/src/texture.rs +++ b/ikari/src/texture.rs @@ -8,6 +8,7 @@ use crate::sampler_cache::*; use anyhow::*; use glam::f32::Vec3; +use image::EncodableLayout; use wgpu::util::DeviceExt; #[derive(Debug)] @@ -32,6 +33,17 @@ pub struct CubemapImages { impl Texture { pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; + pub fn unpadded_bytes_per_row(&self) -> u32 { + self.size.width * self.texture.format().block_size(None).unwrap() + } + + pub fn padded_bytes_per_row(&self) -> u32 { + let unpadded_bytes_per_row = self.unpadded_bytes_per_row(); + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + let padded_bytes_per_row_padding = (align - unpadded_bytes_per_row % align) % align; + unpadded_bytes_per_row + padded_bytes_per_row_padding + } + // supports jpg and png pub fn from_encoded_image( base_renderer: &BaseRenderer, @@ -89,6 +101,7 @@ impl Texture { dimension: wgpu::TextureDimension::D2, format, usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::RENDER_ATTACHMENT, view_formats: &[], @@ -100,9 +113,10 @@ impl Texture { mip_level: 0, origin: wgpu::Origin3d::ZERO, }, - std::hint::black_box(img_bytes), + img_bytes, wgpu::ImageDataLayout { offset: 0, + // queue.write_texture is exempt from COPY_BYTES_PER_ROW_ALIGNMENT requirement bytes_per_row: Some(format.block_size(None).unwrap() * dimensions.0), rows_per_image: Some(dimensions.1), }, @@ -227,6 +241,83 @@ impl Texture { Self::from_color(base_renderer, [127, 127, 255, 255]) } + /// only works if the texture had the COPY_SRC usage set at creation + /// returns one byte array per image in case of arrays and cubemaps + pub async fn to_bytes(&self, base_renderer: &BaseRenderer) -> Result>> { + let mut result = vec![]; + for depth in 0..self.size.depth_or_array_layers { + let mut encoder = + base_renderer + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: USE_LABELS.then_some("to_bytes encoder"), + }); + + let output_buffer = base_renderer.device.create_buffer(&wgpu::BufferDescriptor { + size: (self.padded_bytes_per_row() * self.size.height) as u64, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + label: USE_LABELS.then_some("to_bytes output buffer"), + mapped_at_creation: false, + }); + + encoder.copy_texture_to_buffer( + wgpu::ImageCopyTexture { + aspect: wgpu::TextureAspect::All, + texture: &self.texture, + mip_level: 0, + origin: wgpu::Origin3d { + z: depth, + ..wgpu::Origin3d::ZERO + }, + }, + wgpu::ImageCopyBuffer { + buffer: &output_buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: dbg!(Some(self.padded_bytes_per_row())), + rows_per_image: None, + }, + }, + wgpu::Extent3d { + width: self.size.width, + height: self.size.height, + depth_or_array_layers: 1, + }, + ); + + base_renderer.queue.submit(Some(encoder.finish())); + + // let result; + { + let buffer_slice = output_buffer.slice(..); + + // TODO: make sure this works on the web + let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel(); + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + tx.send(result).unwrap(); + }); + base_renderer.device.poll(wgpu::Maintain::Wait); + rx.receive().await.unwrap()?; + + let data = &buffer_slice.get_mapped_range(); + let mut unpadded_data = + Vec::with_capacity((self.unpadded_bytes_per_row() * self.size.height) as usize); + for i in 0..(self.size.height) { + let start_index = (i * self.padded_bytes_per_row()) as usize; + let end_index = start_index + self.unpadded_bytes_per_row() as usize; + if data[start_index..end_index].iter().all(|val| *val == 0) { + log::debug!("row {i} of depth {depth} was all zeros"); + } + unpadded_data.extend_from_slice(&data[start_index..end_index]); + } + result.push(unpadded_data.to_vec()); + } + output_buffer.unmap(); + } + + Ok(result) + } + pub fn create_scaled_surface_texture( base_renderer: &BaseRenderer, (width, height): (u32, u32), @@ -445,14 +536,24 @@ impl Texture { } } - #[allow(clippy::too_many_arguments)] pub fn create_cubemap_from_equirectangular( base_renderer: &BaseRenderer, renderer_constant_data: &RendererConstantData, + format: wgpu::TextureFormat, label: Option<&str>, er_texture: &Texture, generate_mipmaps: bool, - ) -> Self { + ) -> Result { + let equirectangular_to_cubemap_pipeline = match format { + wgpu::TextureFormat::Rgba8UnormSrgb => { + &renderer_constant_data.equirectangular_to_cubemap_pipeline + }, + wgpu::TextureFormat::Rgba16Float => { + &renderer_constant_data.equirectangular_to_cubemap_hdr_pipeline + } + _ => anyhow::bail!("create_cubemap_from_equirectangular called with unsupported texture format: {format:?}"), + }; + let size = wgpu::Extent3d { width: (er_texture.size.width / 3).max(1), height: (er_texture.size.width / 3).max(1), @@ -473,8 +574,9 @@ impl Texture { mip_level_count, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba16Float, + format, usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::RENDER_ATTACHMENT, view_formats: &[], }); @@ -581,7 +683,7 @@ impl Texture { })], depth_stencil_attachment: None, }); - rpass.set_pipeline(&renderer_constant_data.equirectangular_to_cubemap_pipeline); + rpass.set_pipeline(&equirectangular_to_cubemap_pipeline); rpass.set_bind_group(0, &er_texture_bind_group, &[]); rpass.set_bind_group(1, &camera_bind_group, &[]); rpass.set_vertex_buffer( @@ -640,12 +742,12 @@ impl Texture { }, ); - Self { + Ok(Self { texture: cubemap_texture, view, sampler_index, size, - } + }) } /// Each image should have the same dimensions! @@ -692,7 +794,9 @@ impl Texture { sample_count: 1, dimension: wgpu::TextureDimension::D2, format, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::COPY_DST, view_formats: &[], }, // pack images into one big byte array @@ -736,7 +840,6 @@ impl Texture { } } - #[allow(clippy::too_many_arguments)] pub fn create_diffuse_env_map( base_renderer: &BaseRenderer, renderer_constant_data: &RendererConstantData, @@ -766,6 +869,7 @@ impl Texture { dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba16Float, usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::RENDER_ATTACHMENT, view_formats: &[], }); @@ -954,6 +1058,7 @@ impl Texture { dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba16Float, usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::RENDER_ATTACHMENT, view_formats: &[], });