tirbofish/dropbear · commit
67dbcbe58f0a5f52c4f59b40a36f5103691c15dc
feature: skybox
fix: lighting
todo: allow for customisable skyboxes (user defined).
Signature present but could not be verified.
Unverified
@@ -83,9 +83,9 @@ combine = "4.6" glyphon = { git = "https://github.com/grovesNL/glyphon", rev = "9dd9376" } [workspace.dependencies.image] -version = "0.25" +version = "0.24" default-features = false -features = ["png"] +features = ["png", "jpeg", "hdr"] [profile.dev] opt-level = 1 @@ -44,16 +44,12 @@ dashmap.workspace = true typetag.workspace = true postcard.workspace = true pollster.workspace = true +image.workspace = true #yakui-wgpu.workspace = true #yakui.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true -[dependencies.image] -version = "0.25" -default-features = false -features = ["png", "jpeg"] - [build-dependencies] slank = { path = "../slank", features = ["download-slang"] } @@ -5,15 +5,12 @@ fn main() { // let shader = Shader::from_slang(graphics.clone(), &slank::compiled::CompiledSlangShader::from_bytes("light cube", include_slang!("light_cube"))); SlangShaderBuilder::new("light_cube") - .add_source_path("src/pipelines/shaders/light.slang").unwrap() + .add_source_path("src/shaders/light.slang").unwrap() .compile_to_out_dir(SlangTarget::SpirV).unwrap(); SlangShaderBuilder::new("blit_shader") - .add_source_path("src/pipelines/shaders/blit.slang").unwrap() + .add_source_path("src/shaders/blit.slang").unwrap() .compile_to_out_dir(SlangTarget::SpirV).unwrap(); - // unable to do mipmap.slang because it required texture_storage_2d, one write and one read. - // just wasn't possible with slang :( - - + println!("cargo:rerun-if-changed=src/shaders"); } @@ -236,8 +236,9 @@ impl Camera { } pub fn update_view_proj(&mut self) { - let mvp = self.build_vp(); - self.uniform.view_proj = mvp.as_mat4().to_cols_array_2d(); + let mut uniform = self.uniform; + uniform.update(self); + self.uniform = uniform; } pub fn move_forwards(&mut self, dt: f32) { @@ -298,20 +299,40 @@ impl Camera { #[derive(Debug, Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)] pub struct CameraUniform { pub view_position: [f32; 4], + pub view: [[f32; 4]; 4], pub view_proj: [[f32; 4]; 4], + pub inv_proj: [[f32; 4]; 4], + pub inv_view: [[f32; 4]; 4], } impl CameraUniform { pub fn new() -> Self { Self { view_position: [0.0; 4], + view: Mat4::IDENTITY.to_cols_array_2d(), view_proj: Mat4::IDENTITY.to_cols_array_2d(), + inv_proj: Mat4::IDENTITY.to_cols_array_2d(), + inv_view: Mat4::IDENTITY.to_cols_array_2d(), } } pub fn update(&mut self, camera: &mut Camera) { self.view_position = camera.eye.as_vec3().extend(1.0).to_array(); - self.view_proj = (DMat4::from_cols_array_2d(&OPENGL_TO_WGPU_MATRIX) * camera.build_vp()) + + let vp = camera.build_vp(); + let view = camera.view_mat; + let proj = camera.proj_mat; + + let wgpu_matrix = DMat4::from_cols_array_2d(&OPENGL_TO_WGPU_MATRIX); + self.view = view.as_mat4().to_cols_array_2d(); + self.view_proj = vp.as_mat4().to_cols_array_2d(); + + self.inv_proj = (wgpu_matrix * proj) + .inverse() + .as_mat4() + .to_cols_array_2d(); + self.inv_view = view + .inverse() .as_mat4() .to_cols_array_2d(); } @@ -18,6 +18,7 @@ pub mod utils; pub mod texture; pub mod pipelines; pub mod mipmap; +pub mod sky; pub static WGPU_BACKEND: OnceLock<String> = OnceLock::new(); pub const PHYSICS_STEP_RATE: u32 = 120; @@ -73,6 +74,7 @@ pub struct BindGroupLayouts { pub light_bind_group_layout: BindGroupLayout, pub light_array_bind_group_layout: BindGroupLayout, pub light_cube_bind_group_layout: BindGroupLayout, + pub environment_bind_group_layout: BindGroupLayout, } /// The backend information, such as the device, queue, config, surface, renderer, window and more. @@ -146,15 +148,18 @@ impl State { let limits = wgpu::Limits { max_bind_groups: 8, - ..Default::default() + ..wgpu::Limits::defaults() }; + let supported_features = adapter.features(); + let features = supported_features & wgpu::Features::all_webgpu_mask(); + let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor { label: Some(format!("{} graphics device", title).as_str()), - required_features: wgpu::Features::empty(), + required_features: features, required_limits: limits, - experimental_features: unsafe { ExperimentalFeatures::enabled() }, + experimental_features: ExperimentalFeatures::default(), memory_hints: Default::default(), trace: wgpu::Trace::Off, }) @@ -354,6 +359,29 @@ Hardware: // wgpu::AddressMode::default(), // ); + let environment_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("environment_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::Cube, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), + count: None, + }, + ], + }); + let result = Self { surface: Arc::new(surface), surface_format, @@ -379,6 +407,7 @@ Hardware: light_bind_group_layout, light_array_bind_group_layout, light_cube_bind_group_layout, + environment_bind_group_layout, }), supports_storage: supports_storage_resources, // yakui_renderer, @@ -874,7 +903,7 @@ impl App { /// window config. fn new(app_data: AppInfo, future_queue: Option<Arc<FutureQueue>>) -> Self { let instance = Arc::new(Instance::new(&wgpu::InstanceDescriptor { - backends: wgpu::Backends::all(), + backends: wgpu::Backends::PRIMARY, ..Default::default() })); @@ -914,7 +943,13 @@ impl App { let window_id = window.id(); - let mut win_state = pollster::block_on(State::new(window, self.instance.clone(), self.future_queue.clone()))?; + let mut win_state = match pollster::block_on(State::new(window, self.instance.clone(), self.future_queue.clone())) { + Ok(v) => v, + Err(e) => { + // force a panic because pollster doesnt panic on error + panic!("Failed to create window: {}", e); + } + }; let size = win_state.window.inner_size(); win_state.resize(size.width, size.height); @@ -93,7 +93,7 @@ impl MipMapper { let compute_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("mipmap compute shader"), - source: wgpu::ShaderSource::Wgsl(include_str!("pipelines/shaders/mipmap.wgsl").into()), + source: wgpu::ShaderSource::Wgsl(include_str!("shaders/mipmap.wgsl").into()), }); let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { @@ -35,8 +35,6 @@ impl HdrPipeline { Some("Hdr::texture"), ); - - let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("Hdr::layout"), entries: &[ @@ -75,7 +73,7 @@ impl HdrPipeline { }); // We'll cover the shader next - let shader = wgpu::include_wgsl!("shaders/hdr.wgsl"); + let shader = wgpu::include_wgsl!("../shaders/hdr.wgsl"); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, bind_group_layouts: &[&layout], @@ -23,7 +23,7 @@ pub trait DropbearShaderPipeline { fn pipeline(&self) -> &wgpu::RenderPipeline; } -fn create_render_pipeline( +pub fn create_render_pipeline( label: Option<&str>, device: &wgpu::Device, layout: &wgpu::PipelineLayout, @@ -33,6 +33,32 @@ fn create_render_pipeline( topology: wgpu::PrimitiveTopology, shader: wgpu::ShaderModuleDescriptor, ) -> wgpu::RenderPipeline { + create_render_pipeline_ex( + label, + device, + layout, + color_format, + depth_format, + vertex_layouts, + topology, + shader, + true, // depth_write_enabled + wgpu::CompareFunction::LessEqual, + ) +} + +pub fn create_render_pipeline_ex( + label: Option<&str>, + device: &wgpu::Device, + layout: &wgpu::PipelineLayout, + color_format: wgpu::TextureFormat, + depth_format: Option<wgpu::TextureFormat>, + vertex_layouts: &[wgpu::VertexBufferLayout], + topology: wgpu::PrimitiveTopology, + shader: wgpu::ShaderModuleDescriptor, + depth_write_enabled: bool, + depth_compare: wgpu::CompareFunction, +) -> wgpu::RenderPipeline { let shader = device.create_shader_module(shader); device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { @@ -68,8 +94,8 @@ fn create_render_pipeline( }, depth_stencil: depth_format.map(|format| wgpu::DepthStencilState { format, - depth_write_enabled: true, - depth_compare: wgpu::CompareFunction::LessEqual, // UDPATED! + depth_write_enabled, + depth_compare, stencil: wgpu::StencilState::default(), bias: wgpu::DepthBiasState::default(), }), @@ -18,7 +18,7 @@ impl DropbearShaderPipeline for MainRenderPipeline { fn new(graphics: Arc<SharedGraphicsContext>) -> Self { let shader = Shader::new( graphics.clone(), - include_str!("shaders/shader.wgsl"), + include_str!("../shaders/shader.wgsl"), Some("viewport shaders"), ); @@ -1,35 +0,0 @@ -#include "dropbear.slang" - -struct VertexOutput { - float4 clip_position : SV_Position; - - [[vk_location(0)]] - float2 uv; -}; - -[[vk_binding(0, 0)]] -Texture2D tex; - -[[vk_binding(1, 0)]] -SamplerState tex_sampler; - -[shader("vertex")] -VertexOutput vs_main( - uint in_vertex_index: SV_VulkanVertexID -) { - VertexOutput output; - - float x = float((in_vertex_index << 1u) & 2u); - float y = float(in_vertex_index & 2u); - - output.clip_position = float4(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0); - output.uv = float2(x, 1.0 - y); - - return output; -} - -[shader("fragment")] -float4 fs_main(VertexOutput input) : SV_Target0 { - float4 s = tex.Sample(tex_sampler, input.uv); - return s; -} @@ -1,45 +0,0 @@ -struct Globals { - uint num_lights; - float ambient_strength; -}; - -/// A standard light descriptor struct. -struct Light { - float4 position; - float4 direction; // x, y, z, outer_cutoff_angle - float4 color; // r, g, b, light_type (0, 1, 2) - - float constant; - float lin; - float quadratic; - - float cutoff; -}; - -struct CameraUniform { - float4 view_pos; - float4x4 view_proj; -}; - -struct MaterialUniform { - // for stuff like tinting - float4 colour; - - // scales incoming UVs before sampling - float2 uv_tiling; -}; - -/// Converts a wgpu matrix (in the form of 4 float4) into a matrix usable for Slang. -float4x4 vs_input_matrix( - float4 mat1, - float4 mat2, - float4 mat3, - float4 mat4 -) { - return transpose(float4x4( - mat1, - mat2, - mat3, - mat4 - )); -} @@ -1,55 +0,0 @@ -// Maps HDR values to linear values -// Based on http://www.oscars.org/science-technology/sci-tech-projects/aces -fn aces_tone_map(hdr: vec3<f32>) -> vec3<f32> { - let m1 = mat3x3( - 0.59719, 0.07600, 0.02840, - 0.35458, 0.90834, 0.13383, - 0.04823, 0.01566, 0.83777, - ); - let m2 = mat3x3( - 1.60475, -0.10208, -0.00327, - -0.53108, 1.10813, -0.07276, - -0.07367, -0.00605, 1.07602, - ); - let v = m1 * hdr; - let a = v * (v + 0.0245786) - 0.000090537; - let b = v * (0.983729 * v + 0.4329510) + 0.238081; - return clamp(m2 * (a / b), vec3(0.0), vec3(1.0)); -} - -struct VertexOutput { - @location(0) uv: vec2<f32>, - @builtin(position) clip_position: vec4<f32>, -}; - -@vertex -fn vs_main( - @builtin(vertex_index) vi: u32, -) -> VertexOutput { - var out: VertexOutput; - // Generate a triangle that covers the whole screen - out.uv = vec2<f32>( - f32((vi << 1u) & 2u), - f32(vi & 2u), - ); - out.clip_position = vec4<f32>(out.uv * 2.0 - 1.0, 0.0, 1.0); - // We need to invert the y coordinate so the image - // is not upside down - out.uv.y = 1.0 - out.uv.y; - return out; -} - -@group(0) -@binding(0) -var hdr_image: texture_2d<f32>; - -@group(0) -@binding(1) -var hdr_sampler: sampler; - -@fragment -fn fs_main(vs: VertexOutput) -> @location(0) vec4<f32> { - let hdr = textureSample(hdr_image, hdr_sampler, vs.uv); - let sdr = aces_tone_map(hdr.rgb); - return vec4(sdr, hdr.a); -} @@ -1,51 +0,0 @@ -#include "dropbear.slang" - -[[vk::binding(0, 0)]] -ConstantBuffer<CameraUniform> camera; - -[[vk::binding(0, 1)]] -ConstantBuffer<Light> light; - -struct VertexInput { - [[vk::location(0)]] - float3 position : POSITION; - - [[vk::location(5)]] - float4 model_matrix_0 : TEXCOORD5; - - [[vk::location(6)]] - float4 model_matrix_1 : TEXCOORD6; - - [[vk::location(7)]] - float4 model_matrix_2 : TEXCOORD7; - - [[vk::location(8)]] - float4 model_matrix_3 : TEXCOORD8; -}; - -struct VertexOutput { - float4 clip_position : SV_Position; - - [[vk::location(0)]] - float3 color : COLOR0; -}; - -[shader("vertex")] -VertexOutput vs_main(VertexInput input) { - float4x4 model_matrix = vs_input_matrix( - input.model_matrix_0, - input.model_matrix_1, - input.model_matrix_2, - input.model_matrix_3, - ); - - VertexOutput output; - output.clip_position = mul(camera.view_proj, mul(model_matrix, float4(input.position, 1.0))); - output.color = light.color.xyz; - return output; -} - -[shader("fragment")] -float4 fs_main(VertexOutput input) : SV_Target0 { - return float4(input.color, 1.0); -} @@ -1,31 +0,0 @@ -@group(0) -@binding(0) -var src: texture_storage_2d<rgba8unorm, read>; -@group(0) -@binding(1) -var dst: texture_storage_2d<rgba8unorm, write>; - -@compute -@workgroup_size(16, 16, 1) -fn compute_mipmap( - @builtin(global_invocation_id) gid: vec3<u32>, -) { - let dstPos = gid.xy; - let srcPos = gid.xy * 2; - - let dim = textureDimensions(src); - - if (dstPos.x >= dim.x || dstPos.y >= dim.y) { - return; - } - - let t00 = textureLoad(src, srcPos); - let t01 = textureLoad(src, srcPos + vec2(0, 1)); - let t10 = textureLoad(src, srcPos + vec2(1, 0)); - let t11 = textureLoad(src, srcPos + vec2(1, 1)); - - // A simple linear average of 4 adjacent pixels - let t = (t00 + t01 + t10 + t11) * 0.25; - - textureStore(dst, dstPos, t); -} @@ -1,290 +0,0 @@ -// Main shaders for standard objects. - -struct Globals { - num_lights: u32, - ambient_strength: f32, -} - -struct CameraUniform { - view_pos: vec4<f32>, - view_proj: mat4x4<f32>, -}; - -struct Light { - position: vec4<f32>, - direction: vec4<f32>, // x, y, z, outer_cutoff_angle - color: vec4<f32>, // r, g, b, light_type (0, 1, 2) - constant: f32, - lin: f32, - quadratic: f32, - cutoff: f32, -} - -struct MaterialUniform { - // for stuff like tinting - colour: vec4<f32>, - - // scales incoming UVs before sampling - uv_tiling: vec2<f32>, -} - -const c_max_lights: u32 = 10u; - -@group(0) @binding(0) -var t_diffuse: texture_2d<f32>; -@group(0) @binding(1) -var s_diffuse: sampler; -@group(0) @binding(2) -var t_normal: texture_2d<f32>; -@group(0) @binding(3) -var s_normal: sampler; - -@group(1) @binding(0) -var<uniform> u_camera: CameraUniform; - -@group(2) @binding(0) -var<storage, read> s_light_array: array<Light>; -@group(2) @binding(0) -var<uniform> u_light_array: array<Light, 10>; // when storage is not available - -@group(3) @binding(0) -var<uniform> u_material: MaterialUniform; - -@group(4) @binding(0) -var<uniform> u_globals: Globals; - -struct InstanceInput { - @location(5) model_matrix_0: vec4<f32>, - @location(6) model_matrix_1: vec4<f32>, - @location(7) model_matrix_2: vec4<f32>, - @location(8) model_matrix_3: vec4<f32>, - - @location(9) normal_matrix_0: vec3<f32>, - @location(10) normal_matrix_1: vec3<f32>, - @location(11) normal_matrix_2: vec3<f32>, -}; - -struct VertexInput { - @location(0) position: vec3<f32>, - @location(1) tex_coords: vec2<f32>, - @location(2) normal: vec3<f32>, - @location(3) tangent: vec3<f32>, - @location(4) bitangent: vec3<f32>, -}; - -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, - @location(0) tex_coords: vec2<f32>, - @location(1) world_normal: vec3<f32>, - @location(2) world_position: vec3<f32>, - @location(3) world_tangent: vec3<f32>, - @location(4) world_bitangent: vec3<f32>, -}; - -@vertex -fn vs_main( - model: VertexInput, - instance: InstanceInput, -) -> VertexOutput { - let model_matrix = mat4x4<f32>( - instance.model_matrix_0, - instance.model_matrix_1, - instance.model_matrix_2, - instance.model_matrix_3, - ); - let normal_matrix = mat3x3<f32>( - instance.normal_matrix_0, - instance.normal_matrix_1, - instance.normal_matrix_2, - ); - - let world_normal = normalize(normal_matrix * model.normal); - let world_tangent = normalize(normal_matrix * model.tangent); - let world_bitangent = normalize(normal_matrix * model.bitangent); - let world_position = model_matrix * vec4<f32>(model.position, 1.0); - - var out: VertexOutput; - out.clip_position = u_camera.view_proj * world_position; - out.tex_coords = model.tex_coords; - out.world_normal = world_normal; - out.world_position = world_position.xyz; - out.world_tangent = world_tangent; - out.world_bitangent = world_bitangent; - return out; -} - -fn directional_light( - light: Light, - world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32>, - world_pos: vec3<f32> -) -> vec3<f32> { - let light_dir = normalize(-light.direction.xyz); - - let ambient = light.color.xyz * u_globals.ambient_strength * tex_color; - - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color; - - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color; - - return ambient + diffuse + specular; -} - -fn point_light( - light: Light, - world_pos: vec3<f32>, - world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32> -) -> vec3<f32> { - let light_dir = normalize(light.position.xyz - world_pos); - - let distance = length(light.position.xyz - world_pos); - let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); - - let ambient = light.color.xyz * u_globals.ambient_strength * tex_color; - - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color; - - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color; - - return (ambient + diffuse + specular) * attenuation; -} - -fn spot_light( - light: Light, - world_pos: vec3<f32>, - world_normal: vec3<f32>, - view_dir: vec3<f32>, - tex_color: vec3<f32> -) -> vec3<f32> { - let light_dir = normalize(light.position.xyz - world_pos); - let theta = dot(light_dir, normalize(-light.direction.xyz)); - let outer_cutoff = light.direction.w; - - let epsilon = light.cutoff - outer_cutoff; - let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0); - - let distance = length(light.position.xyz - world_pos); - let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); - - let ambient = light.color.xyz * u_globals.ambient_strength * tex_color; - - let diff = max(dot(world_normal, light_dir), 0.0); - let diffuse = light.color.xyz * diff * tex_color * intensity; - - let reflect_dir = reflect(-light_dir, world_normal); - let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - let specular = light.color.xyz * spec * tex_color * intensity; - - return (ambient + diffuse + specular) * attenuation; -} - -fn apply_normal_map( - world_normal_in: vec3<f32>, - world_tangent_in: vec3<f32>, - world_bitangent_in: vec3<f32>, - normal_sample_rgb: vec3<f32>, -) -> vec3<f32> { - let normal_ts = normalize(normal_sample_rgb * 2.0 - vec3<f32>(1.0)); - - let n = normalize(world_normal_in); - var t = normalize(world_tangent_in); - t = normalize(t - n * dot(n, t)); - - let b_in = normalize(world_bitangent_in); - let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0); - let b = cross(n, t) * handedness; - - let tbn = mat3x3<f32>(t, b, n); - return normalize(tbn * normal_ts); -} - -// when storage is supported -@fragment -fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - let uv = in.tex_coords * u_material.uv_tiling; - var tex_color = textureSample(t_diffuse, s_diffuse, uv); - var object_normal = textureSample(t_normal, s_normal, uv); - - let base_colour = tex_color * u_material.colour; - - if (base_colour.a < 0.1) { - discard; - } - - let view_dir = normalize(u_camera.view_pos.xyz - in.world_position); - - let world_normal = apply_normal_map( - in.world_normal, - in.world_tangent, - in.world_bitangent, - object_normal.xyz, - ); - - var final_color = vec3<f32>(0.0); - - for(var i = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) { - let light = s_light_array[i]; - - let light_type = i32(light.color.w + 0.1); - - if (light_type == 0) { - final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position); - } else if (light_type == 1) { - final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); - } else if (light_type == 2) { - final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); - } - } - - return vec4<f32>(final_color, base_colour.a); -} - -// when storage is NOT supported -@fragment -fn u_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - let uv = in.tex_coords * u_material.uv_tiling; - var tex_color = textureSample(t_diffuse, s_diffuse, uv); - var object_normal = textureSample(t_normal, s_normal, uv); - - let base_colour = tex_color * u_material.colour; - - if (base_colour.a < 0.1) { - discard; - } - - let view_dir = normalize(u_camera.view_pos.xyz - in.world_position); - - let world_normal = apply_normal_map( - in.world_normal, - in.world_tangent, - in.world_bitangent, - object_normal.xyz, - ); - - var final_color = vec3<f32>(0.0); - - for(var i = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) { - let light = u_light_array[i]; - - let light_type = i32(light.color.w + 0.1); - - if (light_type == 0) { - final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position); - } else if (light_type == 1) { - final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); - } else if (light_type == 2) { - final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); - } - } - - return vec4<f32>(final_color, base_colour.a); -} @@ -0,0 +1,35 @@ +#include "dropbear.slang" + +struct VertexOutput { + float4 clip_position : SV_Position; + + [[vk_location(0)]] + float2 uv; +}; + +[[vk_binding(0, 0)]] +Texture2D tex; + +[[vk_binding(1, 0)]] +SamplerState tex_sampler; + +[shader("vertex")] +VertexOutput vs_main( + uint in_vertex_index: SV_VulkanVertexID +) { + VertexOutput output; + + float x = float((in_vertex_index << 1u) & 2u); + float y = float(in_vertex_index & 2u); + + output.clip_position = float4(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0); + output.uv = float2(x, 1.0 - y); + + return output; +} + +[shader("fragment")] +float4 fs_main(VertexOutput input) : SV_Target0 { + float4 s = tex.Sample(tex_sampler, input.uv); + return s; +} @@ -0,0 +1,48 @@ +struct Globals { + uint num_lights; + float ambient_strength; +}; + +/// A standard light descriptor struct. +struct Light { + float4 position; + float4 direction; // x, y, z, outer_cutoff_angle + float4 color; // r, g, b, light_type (0, 1, 2) + + float constant; + float lin; + float quadratic; + + float cutoff; +}; + +struct CameraUniform { + float4 view_pos; + float4x4 view; + float4x4 view_proj; + float4x4 inv_proj; + float4x4 inv_view; +}; + +struct MaterialUniform { + // for stuff like tinting + float4 colour; + + // scales incoming UVs before sampling + float2 uv_tiling; +}; + +/// Converts a wgpu matrix (in the form of 4 float4) into a matrix usable for Slang. +float4x4 vs_input_matrix( + float4 mat1, + float4 mat2, + float4 mat3, + float4 mat4 +) { + return transpose(float4x4( + mat1, + mat2, + mat3, + mat4 + )); +} @@ -0,0 +1,86 @@ +const PI: f32 = 3.1415926535897932384626433832795; + +struct Face { + forward: vec3<f32>, + up: vec3<f32>, + right: vec3<f32>, +} + +@group(0) +@binding(0) +var src: texture_2d<f32>; + +@group(0) +@binding(1) +var dst: texture_storage_2d_array<rgba32float, write>; + +@compute +@workgroup_size(16, 16, 1) +fn compute_equirect_to_cubemap( + @builtin(global_invocation_id) + gid: vec3<u32>, +) { + // If texture size is not divisible by 32, we + // need to make sure we don't try to write to + // pixels that don't exist. + if gid.x >= u32(textureDimensions(dst).x) { + return; + } + + var FACES: array<Face, 6> = array( + // FACES +X + Face( + vec3(1.0, 0.0, 0.0), // forward + vec3(0.0, 1.0, 0.0), // up + vec3(0.0, 0.0, -1.0), // right + ), + // FACES -X + Face ( + vec3(-1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, 1.0), + ), + // FACES +Y + Face ( + vec3(0.0, -1.0, 0.0), + vec3(0.0, 0.0, 1.0), + vec3(1.0, 0.0, 0.0), + ), + // FACES -Y + Face ( + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, -1.0), + vec3(1.0, 0.0, 0.0), + ), + // FACES +Z + Face ( + vec3(0.0, 0.0, 1.0), + vec3(0.0, 1.0, 0.0), + vec3(1.0, 0.0, 0.0), + ), + // FACES -Z + Face ( + vec3(0.0, 0.0, -1.0), + vec3(0.0, 1.0, 0.0), + vec3(-1.0, 0.0, 0.0), + ), + ); + + // Get texture coords relative to cubemap face + let dst_dimensions = vec2<f32>(textureDimensions(dst)); + let cube_uv = vec2<f32>(gid.xy) / dst_dimensions * 2.0 - 1.0; + + // Get spherical coordinate from cube_uv + let face = FACES[gid.z]; + let spherical = normalize(face.forward + face.right * cube_uv.x + face.up * cube_uv.y); + + // Get coordinate on the equirectangular texture + let inv_atan = vec2(0.1591, 0.3183); + let eq_uv = vec2(atan2(spherical.z, spherical.x), asin(spherical.y)) * inv_atan + 0.5; + let eq_pixel = vec2<i32>(eq_uv * vec2<f32>(textureDimensions(src))); + + // We use textureLoad() as textureSample() is not allowed in compute shaders + var sample = textureLoad(src, eq_pixel, 0); + + textureStore(dst, gid.xy, gid.z, sample); +} @@ -0,0 +1,55 @@ +// Maps HDR values to linear values +// Based on http://www.oscars.org/science-technology/sci-tech-projects/aces +fn aces_tone_map(hdr: vec3<f32>) -> vec3<f32> { + let m1 = mat3x3( + 0.59719, 0.07600, 0.02840, + 0.35458, 0.90834, 0.13383, + 0.04823, 0.01566, 0.83777, + ); + let m2 = mat3x3( + 1.60475, -0.10208, -0.00327, + -0.53108, 1.10813, -0.07276, + -0.07367, -0.00605, 1.07602, + ); + let v = m1 * hdr; + let a = v * (v + 0.0245786) - 0.000090537; + let b = v * (0.983729 * v + 0.4329510) + 0.238081; + return clamp(m2 * (a / b), vec3(0.0), vec3(1.0)); +} + +struct VertexOutput { + @location(0) uv: vec2<f32>, + @builtin(position) clip_position: vec4<f32>, +}; + +@vertex +fn vs_main( + @builtin(vertex_index) vi: u32, +) -> VertexOutput { + var out: VertexOutput; + // Generate a triangle that covers the whole screen + out.uv = vec2<f32>( + f32((vi << 1u) & 2u), + f32(vi & 2u), + ); + out.clip_position = vec4<f32>(out.uv * 2.0 - 1.0, 0.0, 1.0); + // We need to invert the y coordinate so the image + // is not upside down + out.uv.y = 1.0 - out.uv.y; + return out; +} + +@group(0) +@binding(0) +var hdr_image: texture_2d<f32>; + +@group(0) +@binding(1) +var hdr_sampler: sampler; + +@fragment +fn fs_main(vs: VertexOutput) -> @location(0) vec4<f32> { + let hdr = textureSample(hdr_image, hdr_sampler, vs.uv); + let sdr = aces_tone_map(hdr.rgb); + return vec4(sdr, hdr.a); +} @@ -0,0 +1,51 @@ +#include "dropbear.slang" + +[[vk::binding(0, 0)]] +ConstantBuffer<CameraUniform> camera; + +[[vk::binding(0, 1)]] +ConstantBuffer<Light> light; + +struct VertexInput { + [[vk::location(0)]] + float3 position : POSITION; + + [[vk::location(5)]] + float4 model_matrix_0 : TEXCOORD5; + + [[vk::location(6)]] + float4 model_matrix_1 : TEXCOORD6; + + [[vk::location(7)]] + float4 model_matrix_2 : TEXCOORD7; + + [[vk::location(8)]] + float4 model_matrix_3 : TEXCOORD8; +}; + +struct VertexOutput { + float4 clip_position : SV_Position; + + [[vk::location(0)]] + float3 color : COLOR0; +}; + +[shader("vertex")] +VertexOutput vs_main(VertexInput input) { + float4x4 model_matrix = vs_input_matrix( + input.model_matrix_0, + input.model_matrix_1, + input.model_matrix_2, + input.model_matrix_3, + ); + + VertexOutput output; + output.clip_position = mul(camera.view_proj, mul(model_matrix, float4(input.position, 1.0))); + output.color = light.color.xyz; + return output; +} + +[shader("fragment")] +float4 fs_main(VertexOutput input) : SV_Target0 { + return float4(input.color, 1.0); +} @@ -0,0 +1,31 @@ +@group(0) +@binding(0) +var src: texture_storage_2d<rgba8unorm, read>; +@group(0) +@binding(1) +var dst: texture_storage_2d<rgba8unorm, write>; + +@compute +@workgroup_size(16, 16, 1) +fn compute_mipmap( + @builtin(global_invocation_id) gid: vec3<u32>, +) { + let dstPos = gid.xy; + let srcPos = gid.xy * 2; + + let dim = textureDimensions(src); + + if (dstPos.x >= dim.x || dstPos.y >= dim.y) { + return; + } + + let t00 = textureLoad(src, srcPos); + let t01 = textureLoad(src, srcPos + vec2(0, 1)); + let t10 = textureLoad(src, srcPos + vec2(1, 0)); + let t11 = textureLoad(src, srcPos + vec2(1, 1)); + + // A simple linear average of 4 adjacent pixels + let t = (t00 + t01 + t10 + t11) * 0.25; + + textureStore(dst, dstPos, t); +} @@ -0,0 +1,288 @@ +// Main shaders for standard objects. + +struct Globals { + num_lights: u32, + ambient_strength: f32, +} + +struct CameraUniform { + view_pos: vec4<f32>, + view: mat4x4<f32>, + view_proj: mat4x4<f32>, + inv_proj: mat4x4<f32>, + inv_view: mat4x4<f32>, +} +struct Light { + position: vec4<f32>, + direction: vec4<f32>, // x, y, z, outer_cutoff_angle + color: vec4<f32>, // r, g, b, light_type (0, 1, 2) + constant: f32, + lin: f32, + quadratic: f32, + cutoff: f32, +} + +struct MaterialUniform { + // for stuff like tinting + colour: vec4<f32>, + + // scales incoming UVs before sampling + uv_tiling: vec2<f32>, +} + +const c_max_lights: u32 = 10u; + +@group(0) @binding(0) +var t_diffuse: texture_2d<f32>; +@group(0) @binding(1) +var s_diffuse: sampler; +@group(0) @binding(2) +var t_normal: texture_2d<f32>; +@group(0) @binding(3) +var s_normal: sampler; + +@group(1) @binding(0) +var<uniform> u_camera: CameraUniform; + +@group(2) @binding(0) +var<storage, read> s_light_array: array<Light>; +@group(2) @binding(0) +var<uniform> u_light_array: array<Light, 10>; // when storage is not available + +@group(3) @binding(0) +var<uniform> u_material: MaterialUniform; + +@group(4) @binding(0) +var<uniform> u_globals: Globals; + +struct InstanceInput { + @location(5) model_matrix_0: vec4<f32>, + @location(6) model_matrix_1: vec4<f32>, + @location(7) model_matrix_2: vec4<f32>, + @location(8) model_matrix_3: vec4<f32>, + + @location(9) normal_matrix_0: vec3<f32>, + @location(10) normal_matrix_1: vec3<f32>, + @location(11) normal_matrix_2: vec3<f32>, +}; + +struct VertexInput { + @location(0) position: vec3<f32>, + @location(1) tex_coords: vec2<f32>, + @location(2) normal: vec3<f32>, + @location(3) tangent: vec3<f32>, + @location(4) bitangent: vec3<f32>, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4<f32>, + @location(0) tex_coords: vec2<f32>, + @location(1) world_normal: vec3<f32>, + @location(2) world_position: vec3<f32>, + @location(3) world_tangent: vec3<f32>, + @location(4) world_bitangent: vec3<f32>, +}; + +@vertex +fn vs_main( + model: VertexInput, + instance: InstanceInput, +) -> VertexOutput { + let model_matrix = mat4x4<f32>( + instance.model_matrix_0, + instance.model_matrix_1, + instance.model_matrix_2, + instance.model_matrix_3, + ); + let normal_matrix = mat3x3<f32>( + instance.normal_matrix_0, + instance.normal_matrix_1, + instance.normal_matrix_2, + ); + + let world_normal = normalize(normal_matrix * model.normal); + let world_tangent = normalize(normal_matrix * model.tangent); + let world_bitangent = normalize(normal_matrix * model.bitangent); + let world_position = model_matrix * vec4<f32>(model.position, 1.0); + + var out: VertexOutput; + out.clip_position = u_camera.view_proj * world_position; + out.tex_coords = model.tex_coords; + out.world_normal = world_normal; + out.world_position = world_position.xyz; + out.world_tangent = world_tangent; + out.world_bitangent = world_bitangent; + return out; +} + +fn directional_light( + light: Light, + world_normal: vec3<f32>, + view_dir: vec3<f32>, + tex_color: vec3<f32>, + world_pos: vec3<f32> +) -> vec3<f32> { + let light_dir = normalize(-light.direction.xyz); + + let diff = max(dot(world_normal, light_dir), 0.0); + let diffuse = light.color.xyz * diff * tex_color; + + let reflect_dir = reflect(-light_dir, world_normal); + let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); + let specular = light.color.xyz * spec * tex_color; + + return diffuse + specular; +} + +fn point_light( + light: Light, + world_pos: vec3<f32>, + world_normal: vec3<f32>, + view_dir: vec3<f32>, + tex_color: vec3<f32> +) -> vec3<f32> { + let light_dir = normalize(light.position.xyz - world_pos); + + let distance = length(light.position.xyz - world_pos); + let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); + + let diff = max(dot(world_normal, light_dir), 0.0); + let diffuse = light.color.xyz * diff * tex_color; + + let reflect_dir = reflect(-light_dir, world_normal); + let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); + let specular = light.color.xyz * spec * tex_color; + + return (diffuse + specular) * attenuation; +} + +fn spot_light( + light: Light, + world_pos: vec3<f32>, + world_normal: vec3<f32>, + view_dir: vec3<f32>, + tex_color: vec3<f32> +) -> vec3<f32> { + let light_dir = normalize(light.position.xyz - world_pos); + let theta = dot(light_dir, normalize(-light.direction.xyz)); + let outer_cutoff = light.direction.w; + + let epsilon = light.cutoff - outer_cutoff; + let intensity = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0); + + let distance = length(light.position.xyz - world_pos); + let attenuation = 1.0 / (light.constant + (light.lin * distance) + (light.quadratic * (distance * distance))); + + let diff = max(dot(world_normal, light_dir), 0.0); + let diffuse = light.color.xyz * diff * tex_color * intensity; + + let reflect_dir = reflect(-light_dir, world_normal); + let spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); + let specular = light.color.xyz * spec * tex_color * intensity; + + return (diffuse + specular) * attenuation; +} + +fn apply_normal_map( + world_normal_in: vec3<f32>, + world_tangent_in: vec3<f32>, + world_bitangent_in: vec3<f32>, + normal_sample_rgb: vec3<f32>, +) -> vec3<f32> { + let normal_ts = normalize(normal_sample_rgb * 2.0 - vec3<f32>(1.0)); + + let n = normalize(world_normal_in); + var t = normalize(world_tangent_in); + t = normalize(t - n * dot(n, t)); + + let b_in = normalize(world_bitangent_in); + let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0); + let b = cross(n, t) * handedness; + + let tbn = mat3x3<f32>(t, b, n); + return normalize(tbn * normal_ts); +} + +// when storage is supported +@fragment +fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + let uv = in.tex_coords * u_material.uv_tiling; + var tex_color = textureSample(t_diffuse, s_diffuse, uv); + var object_normal = textureSample(t_normal, s_normal, uv); + + let base_colour = tex_color * u_material.colour; + + if (base_colour.a < 0.1) { + discard; + } + + let view_dir = normalize(u_camera.view_pos.xyz - in.world_position); + + let world_normal = apply_normal_map( + in.world_normal, + in.world_tangent, + in.world_bitangent, + object_normal.xyz, + ); + + let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.xyz; + var final_color = ambient; + + for(var i = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) { + let light = s_light_array[i]; + + let light_type = i32(light.color.w + 0.1); + + if (light_type == 0) { + final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position); + } else if (light_type == 1) { + final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); + } else if (light_type == 2) { + final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); + } + } + + return vec4<f32>(final_color, base_colour.a); +} + +// when storage is NOT supported +@fragment +fn u_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + let uv = in.tex_coords * u_material.uv_tiling; + var tex_color = textureSample(t_diffuse, s_diffuse, uv); + var object_normal = textureSample(t_normal, s_normal, uv); + + let base_colour = tex_color * u_material.colour; + + if (base_colour.a < 0.1) { + discard; + } + + let view_dir = normalize(u_camera.view_pos.xyz - in.world_position); + + let world_normal = apply_normal_map( + in.world_normal, + in.world_tangent, + in.world_bitangent, + object_normal.xyz, + ); + + let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.xyz; + var final_color = ambient; + + for(var i = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) { + let light = u_light_array[i]; + + let light_type = i32(light.color.w + 0.1); + + if (light_type == 0) { + final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position); + } else if (light_type == 1) { + final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); + } else if (light_type == 2) { + final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); + } + } + + return vec4<f32>(final_color, base_colour.a); +} @@ -0,0 +1,46 @@ +struct Camera { + view_pos: vec4<f32>, + view: mat4x4<f32>, + view_proj: mat4x4<f32>, + inv_proj: mat4x4<f32>, + inv_view: mat4x4<f32>, +} +@group(0) @binding(0) +var<uniform> camera: Camera; + +@group(1) +@binding(0) +var env_map: texture_cube<f32>; +@group(1) +@binding(1) +var env_sampler: sampler; + +struct VertexOutput { + @builtin(position) frag_position: vec4<f32>, + @location(0) clip_position: vec4<f32>, +} + +@vertex +fn vs_main( + @builtin(vertex_index) id: u32, +) -> VertexOutput { + let uv = vec2<f32>(vec2<u32>( + id & 1u, + (id >> 1u) & 1u, + )); + var out: VertexOutput; + let ndc = uv * 4.0 - 1.0; + out.frag_position = vec4(ndc, 0.0, 1.0); + out.clip_position = vec4(ndc, 1.0, 1.0); + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + let view_pos_homogeneous = camera.inv_proj * in.clip_position; + let view_ray_direction = view_pos_homogeneous.xyz / view_pos_homogeneous.w; + var ray_direction = normalize((camera.inv_view * vec4(view_ray_direction, 0.0)).xyz); + + let smpl = textureSample(env_map, env_sampler, ray_direction); + return smpl; +} @@ -0,0 +1,298 @@ +use std::io::Cursor; +use std::sync::Arc; +use crate::texture::Texture; +use image::codecs::hdr::HdrDecoder; +use crate::graphics::SharedGraphicsContext; +use crate::pipelines::{create_render_pipeline_ex}; + +pub struct CubeTexture { + texture: wgpu::Texture, + sampler: wgpu::Sampler, + view: wgpu::TextureView, +} + +impl CubeTexture { + pub fn create_2d( + device: &wgpu::Device, + width: u32, + height: u32, + format: wgpu::TextureFormat, + mip_level_count: u32, + usage: wgpu::TextureUsages, + mag_filter: wgpu::FilterMode, + label: Option<&str>, + ) -> Self { + let texture = device.create_texture(&wgpu::TextureDescriptor { + label, + size: wgpu::Extent3d { + width, + height, + // A cube has 6 sides, so we need 6 layers + depth_or_array_layers: 6, + }, + mip_level_count, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage, + view_formats: &[], + }); + + let view = texture.create_view(&wgpu::TextureViewDescriptor { + label, + dimension: Some(wgpu::TextureViewDimension::Cube), + array_layer_count: Some(6), + ..Default::default() + }); + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label, + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + Self { + texture, + sampler, + view, + } + } + + pub fn texture(&self) -> &wgpu::Texture { &self.texture } + + pub fn view(&self) -> &wgpu::TextureView { &self.view } + + pub fn sampler(&self) -> &wgpu::Sampler { &self.sampler } + +} + +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!("shaders/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: Some("compute_equirect_to_cubemap"), + compilation_options: Default::default(), + cache: None, + }); + + Self { + equirect_to_cubemap, + texture_format, + equirect_layout, + } + } + + pub fn from_equirectangular_bytes( + device: &wgpu::Device, + queue: &wgpu::Queue, + data: &[u8], + dst_size: u32, + label: Option<&str>, + ) -> anyhow::Result<CubeTexture> { + let loader = Self::new(device); + + let hdr_decoder = HdrDecoder::new(Cursor::new(data))?; + let meta = hdr_decoder.metadata(); + + #[cfg(not(target_arch="wasm32"))] + let pixels = { + 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[..], + )?; + pixels + }; + #[cfg(target_arch="wasm32")] + let pixels = hdr_decoder.read_image_native()? + .into_iter() + .map(|pix| { + let rgb = pix.to_hdr(); + [rgb.0[0], rgb.0[1], rgb.0[2], 1.0f32] + }) + .collect::<Vec<_>>(); + + let src = Texture::create_2d_texture( + device, + meta.width, + meta.height, + loader.texture_format, + wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + wgpu::FilterMode::Linear, + None, + ); + + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &src.texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &bytemuck::cast_slice(&pixels), + wgpu::TexelCopyBufferLayout { + 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 = CubeTexture::create_2d( + device, + dst_size, + dst_size, + loader.texture_format, + 1, + // We are going to write to `dst` texture so we + // need to use a `STORAGE_BINDING`. + wgpu::TextureUsages::STORAGE_BINDING + | wgpu::TextureUsages::TEXTURE_BINDING, + wgpu::FilterMode::Nearest, + label, + ); + + let dst_view = dst.texture().create_view(&wgpu::TextureViewDescriptor { + label, + // Normally, you'd use `TextureViewDimension::Cube` + // for a cube texture, but we can't use that + // view dimension with a `STORAGE_BINDING`. + // We need to access the cube texture layers + // directly. + dimension: Some(wgpu::TextureViewDimension::D2Array), + ..Default::default() + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label, + layout: &loader.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, timestamp_writes: None }); + + let num_workgroups = (dst_size + 15) / 16; + pass.set_pipeline(&loader.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) + } +} + +pub struct SkyPipeline { + pub texture: CubeTexture, + pub pipeline: wgpu::RenderPipeline, + pub bind_group: wgpu::BindGroup, +} + +impl SkyPipeline { + pub fn new(graphics: Arc<SharedGraphicsContext>, sky_texture: CubeTexture) -> Self { + let environment_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("environment_bind_group"), + layout: &graphics.layouts.environment_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&sky_texture.view()), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(sky_texture.sampler()), + }, + ], + }); + + let sky_pipeline = { + let layout = graphics.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Sky Pipeline Layout"), + bind_group_layouts: &[&graphics.layouts.camera_bind_group_layout, &graphics.layouts.environment_bind_group_layout], + push_constant_ranges: &[], + }); + let shader = wgpu::include_wgsl!("shaders/sky.wgsl"); + create_render_pipeline_ex( + Some("sky render pipeline"), + &graphics.device, + &layout, + graphics.hdr.read().format(), + Some(Texture::DEPTH_FORMAT), + &[], + wgpu::PrimitiveTopology::TriangleList, + shader, + false, + wgpu::CompareFunction::GreaterEqual, + ) + }; + + Self { + texture: sky_texture, + pipeline: sky_pipeline, + bind_group: environment_bind_group, + } + } +} @@ -17,7 +17,7 @@ use dropbear_engine::buffer::ResizableBuffer; use dropbear_engine::entity::EntityTransform; use dropbear_engine::graphics::InstanceRaw; use dropbear_engine::pipelines::light_cube::LightCubePipeline; -use dropbear_engine::texture::TextureWrapMode; +use dropbear_engine::texture::{Texture, TextureWrapMode}; use dropbear_engine::{camera::Camera, entity::{MeshRenderer, Transform}, future::FutureHandle, graphics::{SharedGraphicsContext}, model::{ModelId, MODEL_CACHE}, scene::SceneCommand, DropbearWindowBuilder, WindowData}; use egui::{self, Context}; use egui_dock::{DockArea, DockState, NodeIndex, Style}; @@ -51,7 +51,7 @@ use std::{ time::Instant, }; use std::rc::Rc; -use log::debug; +use log::{debug, error}; use tokio::sync::oneshot; use transform_gizmo_egui::{EnumSet, Gizmo, GizmoMode, GizmoOrientation}; use wgpu::{Color, Extent3d}; @@ -59,9 +59,10 @@ use winit::window::{CursorGrabMode, WindowAttributes}; use winit::{keyboard::KeyCode, window::Window}; use winit::dpi::PhysicalSize; use dropbear_engine::mipmap::MipMapper; -use dropbear_engine::pipelines::DropbearShaderPipeline; +use dropbear_engine::pipelines::{create_render_pipeline, DropbearShaderPipeline}; use dropbear_engine::pipelines::shader::MainRenderPipeline; use dropbear_engine::pipelines::GlobalsUniform; +use dropbear_engine::sky::{HdrLoader, SkyPipeline}; use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry}; use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline; @@ -88,6 +89,7 @@ pub struct Editor { pub shader_globals: Option<GlobalsUniform>, pub collider_wireframe_pipeline: Option<ColliderWireframePipeline>, pub mipmapper: Option<MipMapper>, + pub sky_pipeline: Option<SkyPipeline>, pub active_camera: Arc<Mutex<Option<Entity>>>, @@ -271,6 +273,7 @@ impl Editor { collider_wireframe_geometry_cache: HashMap::new(), collider_instance_buffer: None, mipmapper: None, + sky_pipeline: None, }) } @@ -1356,6 +1359,24 @@ impl Editor { self.texture_id = Some((*graphics.texture_id).clone()); self.window = Some(graphics.window.clone()); self.is_world_loaded.mark_rendering_loaded(); + + let sky_bytes = include_bytes!("../../../../resources/textures/kloofendal_48d_partly_cloudy_puresky_4k.hdr"); + let sky_texture = HdrLoader::from_equirectangular_bytes( + &graphics.device, + &graphics.queue, + sky_bytes, + 1080, + Some("sky texture") + ); + + match sky_texture { + Ok(sky_texture) => { + self.sky_pipeline = Some(SkyPipeline::new(graphics.clone(), sky_texture)); + } + Err(e) => { + error!("Failed to load sky texture: {}", e); + } + } } /// Initialises another eucalyptus-editor play mode app as a separate process and monitors it in a separate thread. @@ -389,35 +389,34 @@ impl Scene for Editor { }; log_once::debug_once!("Pipeline ready"); - { // ensures clearing of the encoder is done correctly. - let mut encoder = CommandEncoder::new(graphics.clone(), Some("viewport clear render encoder")); - - { - let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("viewport clear pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: hdr.view(), - depth_slice: None, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 100.0 / 255.0, - g: 149.0 / 255.0, - b: 237.0 / 255.0, - a: 1.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - }); - } - - if let Err(e) = encoder.submit() { - log_once::error_once!("{}", e); - } + { + let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("viewport clear pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: hdr.view(), + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 100.0 / 255.0, + g: 149.0 / 255.0, + b: 237.0 / 255.0, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(0.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + }); } let lights = { @@ -509,14 +508,14 @@ impl Scene for Editor { depth_slice: None, resolve_target: None, ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(clear_color), + load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { view: &graphics.depth_texture.view, depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(0.0), + load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }), stencil_ops: None, @@ -539,8 +538,6 @@ impl Scene for Editor { } } - - if let Some(lcp) = &self.light_cube_pipeline { for (model, instance_buffer, instance_count) in prepared_models { let globals_bind_group = &self @@ -717,8 +714,41 @@ impl Scene for Editor { ); } } - } +} } + + if let Some(sky) = &self.sky_pipeline { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("sky render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: hdr.view(), + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &graphics.depth_texture.view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + + render_pass.set_pipeline(&sky.pipeline); + render_pass.set_bind_group(0, &camera.bind_group, &[]); + render_pass.set_bind_group(1, &sky.bind_group, &[]); + render_pass.draw(0..3, 0..1); + } + + hdr.process(&mut encoder, &graphics.viewport_texture.view); + if let Err(e) = encoder.submit() { log_once::error_once!("{}", e); } @@ -667,6 +667,8 @@ impl Scene for PlayMode { }); } + hdr.process(&mut encoder, &graphics.viewport_texture.view); + if let Err(e) = encoder.submit() { log_once::error_once!("{}", e); } @@ -972,6 +974,8 @@ impl Scene for PlayMode { } } + hdr.process(&mut encoder, &graphics.viewport_texture.view); + if let Err(e) = encoder.submit() { log_once::error_once!("{}", e); } @@ -997,6 +1001,8 @@ impl Scene for PlayMode { if let Some(kino) = &mut self.kino { let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino encoder")); kino.render(&graphics.device, &graphics.queue, &mut encoder, hdr.view()); + + hdr.process(&mut encoder, &graphics.viewport_texture.view); if let Err(e) = encoder.submit() { log_once::error_once!("Unable to submit kino: {}", e); } @@ -0,0 +1,40 @@ +{ + "rdocCaptureSettings": 1, + "settings": { + "autoStart": false, + "commandLine": "", + "environment": [ + { + "separator": "Platform style", + "type": "Set", + "value": "x11", + "variable": "WINIT_UNIX_BACKEND" + }, + { + "separator": "Platform style", + "type": "Set", + "value": "", + "variable": "WAYLAND_DISPLAY" + } + ], + "executable": "/home/tirbofish/dropbear/target/debug/eucalyptus-editor", + "inject": false, + "numQueuedFrames": 0, + "options": { + "allowFullscreen": true, + "allowVSync": true, + "apiValidation": false, + "captureAllCmdLists": false, + "captureCallstacks": false, + "captureCallstacksOnlyDraws": false, + "debugOutputMute": true, + "delayForDebugger": 0, + "hookIntoChildren": false, + "refAllResources": false, + "softMemoryLimit": 0, + "verifyBufferAccess": false + }, + "queuedFrameCap": 0, + "workingDir": "" + } +} Binary files /dev/null and b/resources/textures/kloofendal_48d_partly_cloudy_puresky_4k.hdr differ