From 631d65b3dfca985ca6bc7d4bf3e6eeb3d25b3de9 Mon Sep 17 00:00:00 2001 From: James Gayfer <10660608+jgayfer@users.noreply.github.com> Date: Wed, 31 Jul 2024 09:37:49 -0700 Subject: [PATCH] Add isolated light map pass The resulting texture isn't used yet, but it's being written to. --- src/plugin.rs | 14 ++- src/render/lighting/light_map.wgsl | 152 +++++++++++++++++++++++++++++ src/render/lighting/mod.rs | 2 + src/render/lighting/node.rs | 55 ++++++++++- src/render/lighting/pipeline.rs | 85 ++++++++++------ src/render/lighting/prepare.rs | 49 ++++++---- 6 files changed, 308 insertions(+), 49 deletions(-) create mode 100644 src/render/lighting/light_map.wgsl diff --git a/src/plugin.rs b/src/plugin.rs index 4d09ec1..0a1748c 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -22,8 +22,9 @@ use crate::{ ExtractedAmbientLight2d, ExtractedLightOccluder2d, ExtractedPointLight2d, }, lighting::{ - prepare_lighting_auxiliary_textures, prepare_lighting_pipelines, LightingNode, - LightingPass, LightingPipeline, SdfPipeline, LIGHTING_SHADER, SDF_SHADER, TYPES_SHADER, + prepare_lighting_auxiliary_textures, prepare_lighting_pipelines, LightMapPipeline, + LightingNode, LightingPass, LightingPipeline, SdfPipeline, LIGHTING_SHADER, + LIGHT_MAP_SHADER, SDF_SHADER, TYPES_SHADER, }, }, }; @@ -51,6 +52,12 @@ impl Plugin for Light2dPlugin { "render/lighting/lighting.wgsl", Shader::from_wgsl ); + load_internal_asset!( + app, + LIGHT_MAP_SHADER, + "render/lighting/light_map.wgsl", + Shader::from_wgsl + ); app.add_plugins(( UniformComponentPlugin::::default(), @@ -98,6 +105,7 @@ impl Plugin for Light2dPlugin { render_app .init_resource::() - .init_resource::(); + .init_resource::() + .init_resource::(); } } diff --git a/src/render/lighting/light_map.wgsl b/src/render/lighting/light_map.wgsl new file mode 100644 index 0000000..1d49eb8 --- /dev/null +++ b/src/render/lighting/light_map.wgsl @@ -0,0 +1,152 @@ +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_render::view::View + +fn world_to_ndc(world_position: vec2) -> vec2 { + return (view.clip_from_world * vec4(world_position, 0.0, 1.0)).xy; +} + +fn ndc_to_world(ndc_position: vec2) -> vec2 { + return (view.world_from_clip * vec4(ndc_position, 0.0, 1.0)).xy; +} + +fn frag_coord_to_uv(frag_coord: vec2) -> vec2 { + return (frag_coord - view.viewport.xy) / view.viewport.zw; +} + +fn frag_coord_to_ndc(frag_coord: vec2) -> vec2 { + return uv_to_ndc(frag_coord_to_uv(frag_coord.xy)); +} + +fn uv_to_ndc(uv: vec2) -> vec2 { + return uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0); +} + +fn ndc_to_uv(ndc: vec2) -> vec2 { + return ndc * vec2(0.5, -0.5) + vec2(0.5); +} + +// We're currently only using a single uniform binding for point lights in +// WebGL2, which is limited to 4kb in BatchedUniformBuffer, so we need to +// ensure our point lights can fit in 4kb. +const MAX_POINT_LIGHTS: u32 = 82u; + +struct PointLight2d { + center: vec2f, + radius: f32, + color: vec4, + intensity: f32, + falloff: f32 +} + +struct AmbientLight2d { + color: vec4 +} + +@group(0) @binding(0) +var view: View; + +@group(0) @binding(1) +var ambient_light: AmbientLight2d; + +// WebGL2 does not support storage buffers, so we fall back to a fixed length +// array in a uniform buffer. +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + @group(0) @binding(2) + var point_lights: array; +#else + @group(0) @binding(2) + var point_lights: array; +#endif + +@group(0) @binding(3) +var sdf: texture_2d; + +@group(0) @binding(4) +var sdf_sampler: sampler; + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + let pos = ndc_to_world(frag_coord_to_ndc(in.position.xy)); + + var lighting_color = ambient_light.color; + + if get_distance(pos) <= 0.0 { + return lighting_color; + } + + // WebGL2 does not support storage buffers (or runtime sized arrays), so we + // need to use a fixed number of point lights. +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + let point_light_count = arrayLength(&point_lights); +#else + let point_light_count = MAX_POINT_LIGHTS; +#endif + + for (var i = 0u; i < point_light_count; i++) { + let light = point_lights[i]; + let dist = distance(light.center, pos); + + if dist < light.radius { + let raymarch = raymarch(pos, light.center); + + if raymarch > 0.0 { + lighting_color += light.color * attenuation(light, dist); + } + } + } + + return lighting_color; +} + +fn square(x: f32) -> f32 { + return x * x; +} + +fn attenuation(light: PointLight2d, dist: f32) -> f32 { + let s = dist / light.radius; + if s > 1.0 { + return 0.0; + } + let s2 = square(s); + return light.intensity * square(1 - s2) / (1 + light.falloff * s2); +} + +fn get_distance(pos: vec2) -> f32 { + let uv = ndc_to_uv(world_to_ndc(pos)); + let dist = textureSample(sdf, sdf_sampler, uv).r; + return dist; +} + +fn distance_squared(a: vec2, b: vec2) -> f32 { + let c = a - b; + return dot(c, c); +} + +fn raymarch(ray_origin: vec2, ray_target: vec2) -> f32 { + let ray_direction = normalize(ray_target - ray_origin); + let stop_at = distance_squared(ray_origin, ray_target); + + var ray_progress: f32 = 0.0; + var pos = vec2(0.0); + + for (var i = 0; i < 32; i++) { + pos = ray_origin + ray_progress * ray_direction; + + if (ray_progress * ray_progress >= stop_at) { + // ray found target + return 1.0; + } + + let dist = get_distance(pos); + + if dist <= 0.0 { + break; + } + + ray_progress += dist; + } + + // ray found occluder + return 0.0; +} + diff --git a/src/render/lighting/mod.rs b/src/render/lighting/mod.rs index 36369aa..baed3b6 100644 --- a/src/render/lighting/mod.rs +++ b/src/render/lighting/mod.rs @@ -21,6 +21,8 @@ pub const SDF_SHADER: Handle = Handle::weak_from_u128(231804371047309214783091483091843019281); pub const LIGHTING_SHADER: Handle = Handle::weak_from_u128(111120241052143214281687226997564407636); +pub const LIGHT_MAP_SHADER: Handle = + Handle::weak_from_u128(320609826414128764415270070474935914193); #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] pub struct LightingPass; diff --git a/src/render/lighting/node.rs b/src/render/lighting/node.rs index c5d982f..6e20f98 100644 --- a/src/render/lighting/node.rs +++ b/src/render/lighting/node.rs @@ -15,10 +15,15 @@ use crate::render::extract::{ ExtractedAmbientLight2d, ExtractedLightOccluder2d, ExtractedPointLight2d, }; -use super::{Lighting2dAuxiliaryTextures, LightingPipeline, LightingPipelineId, SdfPipeline}; +use super::{ + LightMapPipeline, Lighting2dAuxiliaryTextures, LightingPipeline, LightingPipelineId, + SdfPipeline, +}; const SDF_PASS: &str = "sdf_pass"; const SDF_BIND_GROUP: &str = "sdf_bind_group"; +const LIGHT_MAP_PASS: &str = "light_map_pass"; +const LIGHT_MAP_BIND_GROUP: &str = "light_map_bind_group"; const LIGHTING_PASS: &str = "lighting_pass"; const LIGHTING_BIND_GROUP: &str = "lighting_bind_group"; @@ -45,12 +50,14 @@ impl ViewNode for LightingNode { world: &'w World, ) -> Result<(), bevy::render::render_graph::NodeRunError> { let sdf_pipeline_resource = world.resource::(); + let light_map_pipeline_resource = world.resource::(); let pipeline = world.resource::(); let pipeline_cache = world.resource::(); let ( Some(sdf_pipeline), Some(lighting_pipeline), + Some(light_map_pipeline), Some(view_uniform_binding), Some(ambient_light_uniform), Some(point_light_binding), @@ -58,6 +65,7 @@ impl ViewNode for LightingNode { ) = ( pipeline_cache.get_render_pipeline(sdf_pipeline_resource.pipeline_id), pipeline_cache.get_render_pipeline(pipeline_id.0), + pipeline_cache.get_render_pipeline(light_map_pipeline_resource.pipeline_id), world.resource::().uniforms.binding(), world .resource::>() @@ -112,6 +120,51 @@ impl ViewNode for LightingNode { drop(sdf_pass); + // Light map + let light_map_bind_group = render_context.render_device().create_bind_group( + LIGHT_MAP_BIND_GROUP, + &light_map_pipeline_resource.layout, + &BindGroupEntries::sequential(( + view_uniform_binding.clone(), + ambient_light_uniform.clone(), + point_light_binding.clone(), + &aux_textures.sdf.default_view, + &light_map_pipeline_resource.sdf_sampler, + )), + ); + + let mut light_map_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some(LIGHT_MAP_PASS), + color_attachments: &[Some(RenderPassColorAttachment { + view: &aux_textures.light_map.default_view, + resolve_target: None, + ops: Operations::default(), + })], + ..default() + }); + + let mut light_map_offsets: SmallVec<[u32; 3]> = + smallvec![view_offset.offset, ambient_index.index()]; + + // Storage buffers aren't available in WebGL2. We fall back to a + // dynamic uniform buffer, and therefore need to provide the offset. + // We're providing a value of 0 here as we're limiting the number of + // point lights to only those that can reasonably fit in a single binding. + if world + .resource::() + .limits() + .max_storage_buffers_per_shader_stage + == 0 + { + light_map_offsets.push(0); + } + + light_map_pass.set_render_pipeline(light_map_pipeline); + light_map_pass.set_bind_group(0, &light_map_bind_group, &light_map_offsets); + light_map_pass.draw(0..3, 0..1); + + drop(light_map_pass); + // Main pass (should be replaced by lighting, blur and post process) let post_process = view_target.post_process_write(); diff --git a/src/render/lighting/pipeline.rs b/src/render/lighting/pipeline.rs index f7d9a72..7387b6b 100644 --- a/src/render/lighting/pipeline.rs +++ b/src/render/lighting/pipeline.rs @@ -15,12 +15,14 @@ use crate::render::extract::{ ExtractedAmbientLight2d, ExtractedLightOccluder2d, ExtractedPointLight2d, }; -use super::{LightingPipelineKey, LIGHTING_SHADER, SDF_SHADER}; +use super::{LightingPipelineKey, LIGHTING_SHADER, LIGHT_MAP_SHADER, SDF_SHADER}; const SDF_PIPELINE: &str = "sdf_pipeline"; const SDF_BIND_GROUP_LAYOUT: &str = "sdf_bind_group_layout"; const LIGHTING_PIPELINE: &str = "lighting_pipeline"; const LIGHTING_BIND_GROUP_LAYOUT: &str = "lighting_bind_group_layout"; +const LIGHT_MAP_BIND_GROUP_LAYOUT: &str = "light_map_group_layout"; +const LIGHT_MAP_PIPELINE: &str = "light_map_pipeline"; #[derive(Resource)] pub struct SdfPipeline { @@ -53,8 +55,7 @@ impl FromWorld for SdfPipeline { shader_defs: vec![], entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { - // No need to use a hdr format here - format: TextureFormat::bevy_default(), + format: TextureFormat::Rgba16Float, blend: None, write_mask: ColorWrites::ALL, })], @@ -72,32 +73,60 @@ impl FromWorld for SdfPipeline { } } -impl SpecializedRenderPipeline for SdfPipeline { - type Key = LightingPipelineKey; +#[derive(Resource)] +pub struct LightMapPipeline { + pub layout: BindGroupLayout, + pub sdf_sampler: Sampler, + pub pipeline_id: CachedRenderPipelineId, +} - fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { - RenderPipelineDescriptor { - label: Some(SDF_PIPELINE.into()), - layout: vec![self.layout.clone()], - vertex: fullscreen_shader_vertex_state(), - fragment: Some(FragmentState { - shader: LIGHTING_SHADER, - shader_defs: vec![], - entry_point: "fragment".into(), - targets: vec![Some(ColorTargetState { - format: if key.hdr { - ViewTarget::TEXTURE_FORMAT_HDR - } else { - TextureFormat::bevy_default() - }, - blend: None, - write_mask: ColorWrites::ALL, - })], - }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - push_constant_ranges: vec![], +impl FromWorld for LightMapPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + let layout = render_device.create_bind_group_layout( + LIGHT_MAP_BIND_GROUP_LAYOUT, + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + uniform_buffer::(true), + uniform_buffer::(true), + GpuArrayBuffer::::binding_layout(render_device), + texture_2d(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), + ), + ), + ); + + let sdf_sampler = render_device.create_sampler(&SamplerDescriptor::default()); + + let pipeline_id = + world + .resource_mut::() + .queue_render_pipeline(RenderPipelineDescriptor { + label: Some(LIGHT_MAP_PIPELINE.into()), + layout: vec![layout.clone()], + vertex: fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader: LIGHT_MAP_SHADER, + shader_defs: vec![], + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: TextureFormat::Rgba16Float, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + push_constant_ranges: vec![], + }); + + Self { + layout, + sdf_sampler, + pipeline_id, } } } diff --git a/src/render/lighting/prepare.rs b/src/render/lighting/prepare.rs index 1293fb2..18fd329 100644 --- a/src/render/lighting/prepare.rs +++ b/src/render/lighting/prepare.rs @@ -3,7 +3,7 @@ use bevy::{ render::{ render_resource::{ PipelineCache, SpecializedRenderPipelines, TextureDescriptor, TextureDimension, - TextureUsages, + TextureFormat, TextureUsages, }, renderer::RenderDevice, texture::{CachedTexture, TextureCache}, @@ -35,9 +35,31 @@ pub fn prepare_lighting_pipelines( } } +fn create_aux_texture( + view_target: &ViewTarget, + texture_cache: &mut TextureCache, + render_device: &RenderDevice, + label: &'static str, +) -> CachedTexture { + texture_cache.get( + render_device, + TextureDescriptor { + label: Some(label), + size: view_target.main_texture().size(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ) +} + #[derive(Component)] pub struct Lighting2dAuxiliaryTextures { pub sdf: CachedTexture, + pub light_map: CachedTexture, } pub fn prepare_lighting_auxiliary_textures( @@ -47,21 +69,14 @@ pub fn prepare_lighting_auxiliary_textures( view_targets: Query<(Entity, &ViewTarget)>, ) { for (entity, view_target) in &view_targets { - let texture_descriptor = TextureDescriptor { - label: Some("auxiliary texture"), - size: view_target.main_texture().size(), - mip_level_count: 1, - sample_count: view_target.main_texture().sample_count(), - dimension: TextureDimension::D2, - format: view_target.main_texture_format(), - usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }; - - let texture = texture_cache.get(&render_device, texture_descriptor); - - commands - .entity(entity) - .insert(Lighting2dAuxiliaryTextures { sdf: texture }); + commands.entity(entity).insert(Lighting2dAuxiliaryTextures { + sdf: create_aux_texture(view_target, &mut texture_cache, &render_device, "sdf"), + light_map: create_aux_texture( + view_target, + &mut texture_cache, + &render_device, + "light_map", + ), + }); } }