diff --git a/Cargo.toml b/Cargo.toml index 5918458d2e445..858b58ac9e7b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,9 @@ bevy_ci_testing = ["bevy_internal/bevy_ci_testing"] bevy_dylib = { path = "crates/bevy_dylib", version = "0.5.0", default-features = false, optional = true } bevy_internal = { path = "crates/bevy_internal", version = "0.5.0", default-features = false } +[target.'cfg(target_arch = "wasm32")'.dependencies] +bevy_internal = { path = "crates/bevy_internal", version = "0.5.0", default-features = false, features = ["webgl"] } + [dev-dependencies] anyhow = "1.0.4" rand = "0.8.0" diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 51a402c10fbf9..064e14b874da1 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -41,6 +41,9 @@ x11 = ["bevy_winit/x11"] # enable rendering of font glyphs using subpixel accuracy subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"] +# Optimise for WebGL2 +webgl = ["bevy_pbr/webgl", "bevy_render/webgl"] + # enable systems that allow for automated testing on CI bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_render/ci_limits"] diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 6d8c854ed38cf..cee9a7db2426f 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -8,6 +8,9 @@ repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] +[features] +webgl = [] + [dependencies] # bevy bevy_app = { path = "../bevy_app", version = "0.5.0" } diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index a44ca6841a620..f77501bb93cfb 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -151,7 +151,10 @@ pub struct DirectionalLightShadowMap { impl Default for DirectionalLightShadowMap { fn default() -> Self { - Self { size: 4096 } + #[cfg(feature = "webgl")] + return Self { size: 2048 }; + #[cfg(not(feature = "webgl"))] + return Self { size: 4096 }; } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 8442ed16a9c36..d6545a43cd95f 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -142,6 +142,9 @@ pub struct GpuLights { pub const MAX_POINT_LIGHTS: usize = 256; // FIXME: How should we handle shadows for clustered forward? Limiting to maximum 10 // point light shadow maps for now +#[cfg(feature = "webgl")] +pub const MAX_POINT_LIGHT_SHADOW_MAPS: usize = 1; +#[cfg(not(feature = "webgl"))] pub const MAX_POINT_LIGHT_SHADOW_MAPS: usize = 10; pub const MAX_DIRECTIONAL_LIGHTS: usize = 1; pub const POINT_SHADOW_LAYERS: u32 = (6 * MAX_POINT_LIGHT_SHADOW_MAPS) as u32; @@ -587,14 +590,30 @@ pub fn prepare_lights( global_light_meta.gpu_point_lights.clear(); global_light_meta.entity_to_index.clear(); - let n_point_lights = point_lights.iter().count(); - if global_light_meta.entity_to_index.capacity() < n_point_lights { - global_light_meta.entity_to_index.reserve(n_point_lights); + + let mut point_lights: Vec<_> = point_lights.iter().collect::>(); + + // Sort point lights with shadows enabled first, then by a stable key so that the index can be used + // to render at most `MAX_POINT_LIGHT_SHADOW_MAPS` point light shadows. + point_lights.sort_by(|(entity_1, light_1), (entity_2, light_2)| { + light_1 + .shadows_enabled + .cmp(&light_2.shadows_enabled) + .reverse() + .then_with(|| entity_1.cmp(entity_2)) + }); + + if global_light_meta.entity_to_index.capacity() < point_lights.len() { + global_light_meta + .entity_to_index + .reserve(point_lights.len()); } + let mut gpu_point_lights = [GpuPointLight::default(); MAX_POINT_LIGHTS]; - for (index, (entity, light)) in point_lights.iter().enumerate() { + for (index, &(entity, light)) in point_lights.iter().enumerate() { let mut flags = PointLightFlags::NONE; - if light.shadows_enabled { + // Lights are sorted, shadow enabled lights are first + if light.shadows_enabled && index < MAX_POINT_LIGHT_SHADOW_MAPS { flags |= PointLightFlags::SHADOWS_ENABLED; } gpu_point_lights[index] = GpuPointLight { @@ -683,63 +702,63 @@ pub fn prepare_lights( }; // TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query - let mut point_light_count = 0; - for (light_entity, light) in point_lights.iter() { - if point_light_count < MAX_POINT_LIGHT_SHADOW_MAPS && light.shadows_enabled { - point_light_count += 1; - let light_index = *global_light_meta - .entity_to_index - .get(&light_entity) - .unwrap(); - // ignore scale because we don't want to effectively scale light radius and range - // by applying those as a view transform to shadow map rendering of objects - // and ignore rotation because we want the shadow map projections to align with the axes - let view_translation = - GlobalTransform::from_translation(light.transform.translation); - - for (face_index, view_rotation) in cube_face_rotations.iter().enumerate() { - let depth_texture_view = - point_light_depth_texture - .texture - .create_view(&TextureViewDescriptor { - label: Some("point_light_shadow_map_texture_view"), - format: None, - dimension: Some(TextureViewDimension::D2), - aspect: TextureAspect::All, - base_mip_level: 0, - mip_level_count: None, - base_array_layer: (light_index * 6 + face_index) as u32, - array_layer_count: NonZeroU32::new(1), - }); - - let view_light_entity = commands - .spawn() - .insert_bundle(( - ShadowView { - depth_texture_view, - pass_name: format!( - "shadow pass point light {} {}", - light_index, - face_index_to_name(face_index) - ), - }, - ExtractedView { - width: point_light_shadow_map.size as u32, - height: point_light_shadow_map.size as u32, - transform: view_translation * *view_rotation, - projection: cube_face_projection, - near: POINT_LIGHT_NEAR_Z, - far: light.range, - }, - RenderPhase::::default(), - LightEntity::Point { - light_entity, - face_index, - }, - )) - .id(); - view_lights.push(view_light_entity); - } + for &(light_entity, light) in point_lights + .iter() + // Lights are sorted, shadow enabled lights are first + .take(MAX_POINT_LIGHT_SHADOW_MAPS) + .filter(|(_, light)| light.shadows_enabled) + { + let light_index = *global_light_meta + .entity_to_index + .get(&light_entity) + .unwrap(); + // ignore scale because we don't want to effectively scale light radius and range + // by applying those as a view transform to shadow map rendering of objects + // and ignore rotation because we want the shadow map projections to align with the axes + let view_translation = GlobalTransform::from_translation(light.transform.translation); + + for (face_index, view_rotation) in cube_face_rotations.iter().enumerate() { + let depth_texture_view = + point_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("point_light_shadow_map_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: (light_index * 6 + face_index) as u32, + array_layer_count: NonZeroU32::new(1), + }); + + let view_light_entity = commands + .spawn() + .insert_bundle(( + ShadowView { + depth_texture_view, + pass_name: format!( + "shadow pass point light {} {}", + light_index, + face_index_to_name(face_index) + ), + }, + ExtractedView { + width: point_light_shadow_map.size as u32, + height: point_light_shadow_map.size as u32, + transform: view_translation * *view_rotation, + projection: cube_face_projection, + near: POINT_LIGHT_NEAR_Z, + far: light.range, + }, + RenderPhase::::default(), + LightEntity::Point { + light_entity, + face_index, + }, + )) + .id(); + view_lights.push(view_light_entity); } } @@ -830,7 +849,10 @@ pub fn prepare_lights( .create_view(&TextureViewDescriptor { label: Some("point_light_shadow_map_array_texture_view"), format: None, + #[cfg(not(feature = "webgl"))] dimension: Some(TextureViewDimension::CubeArray), + #[cfg(feature = "webgl")] + dimension: Some(TextureViewDimension::Cube), aspect: TextureAspect::All, base_mip_level: 0, mip_level_count: None, @@ -842,7 +864,10 @@ pub fn prepare_lights( .create_view(&TextureViewDescriptor { label: Some("directional_light_shadow_map_array_texture_view"), format: None, + #[cfg(not(feature = "webgl"))] dimension: Some(TextureViewDimension::D2Array), + #[cfg(feature = "webgl")] + dimension: Some(TextureViewDimension::D2), aspect: TextureAspect::All, base_mip_level: 0, mip_level_count: None, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 42e4bb344dd57..f4a5fcd4d69cf 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -202,7 +202,10 @@ impl FromWorld for MeshPipeline { ty: BindingType::Texture { multisampled: false, sample_type: TextureSampleType::Depth, + #[cfg(not(feature = "webgl"))] view_dimension: TextureViewDimension::CubeArray, + #[cfg(feature = "webgl")] + view_dimension: TextureViewDimension::Cube, }, count: None, }, @@ -220,7 +223,10 @@ impl FromWorld for MeshPipeline { ty: BindingType::Texture { multisampled: false, sample_type: TextureSampleType::Depth, + #[cfg(not(feature = "webgl"))] view_dimension: TextureViewDimension::D2Array, + #[cfg(feature = "webgl")] + view_dimension: TextureViewDimension::D2, }, count: None, }, @@ -485,6 +491,9 @@ impl SpecializedPipeline for MeshPipeline { depth_write_enabled = true; } + #[cfg(feature = "webgl")] + shader_defs.push(String::from("NO_ARRAY_TEXTURES_SUPPORT")); + RenderPipelineDescriptor { vertex: VertexState { shader: MESH_SHADER_HANDLE.typed::(), diff --git a/crates/bevy_pbr/src/render/mesh_view_bind_group.wgsl b/crates/bevy_pbr/src/render/mesh_view_bind_group.wgsl index e1e1b87dd86cd..0d7a0a7f50859 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bind_group.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bind_group.wgsl @@ -73,12 +73,22 @@ struct ClusterOffsetsAndCounts { var view: View; [[group(0), binding(1)]] var lights: Lights; +#ifdef NO_ARRAY_TEXTURES_SUPPORT +[[group(0), binding(2)]] +var point_shadow_textures: texture_depth_cube; +#else [[group(0), binding(2)]] var point_shadow_textures: texture_depth_cube_array; +#endif [[group(0), binding(3)]] var point_shadow_textures_sampler: sampler_comparison; +#ifdef NO_ARRAY_TEXTURES_SUPPORT +[[group(0), binding(4)]] +var directional_shadow_textures: texture_depth_2d; +#else [[group(0), binding(4)]] var directional_shadow_textures: texture_depth_2d_array; +#endif [[group(0), binding(5)]] var directional_shadow_textures_sampler: sampler_comparison; [[group(0), binding(6)]] diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index da7d135adf13e..1ce1e363132d8 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -381,7 +381,11 @@ fn fetch_point_shadow(light_id: u32, frag_position: vec4, surface_normal: v // a quad (2x2 fragments) being processed not being sampled, and this messing with // mip-mapping functionality. The shadow maps have no mipmaps so Level just samples // from LOD 0. +#ifdef NO_ARRAY_TEXTURES_SUPPORT + return textureSampleCompare(point_shadow_textures, point_shadow_textures_sampler, frag_ls, depth); +#else return textureSampleCompareLevel(point_shadow_textures, point_shadow_textures_sampler, frag_ls, i32(light_id), depth); +#endif } fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { @@ -412,7 +416,11 @@ fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_nor // do the lookup, using HW PCF and comparison // NOTE: Due to non-uniform control flow above, we must use the level variant of the texture // sampler to avoid use of implicit derivatives causing possible undefined behavior. +#ifdef NO_ARRAY_TEXTURES_SUPPORT + return textureSampleCompareLevel(directional_shadow_textures, directional_shadow_textures_sampler, light_local, depth); +#else return textureSampleCompareLevel(directional_shadow_textures, directional_shadow_textures_sampler, light_local, i32(light_id), depth); +#endif } fn hsv2rgb(hue: f32, saturation: f32, value: f32) -> vec3 { diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 9b30e7c1abcdb..4b7afb46ac75a 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -18,6 +18,7 @@ bmp = ["image/bmp"] trace = [] wgpu_trace = ["wgpu/trace"] ci_limits = [] +webgl = ["wgpu/webgl"] [dependencies] # bevy @@ -51,6 +52,3 @@ hexasphere = "6.0.0" parking_lot = "0.11.0" regex = "1.5" crevice = { path = "../crevice", version = "0.8.0", features = ["glam"] } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wgpu = { version = "0.12.0", features = ["spirv", "webgl"] } diff --git a/crates/bevy_render/src/mesh/mesh/mod.rs b/crates/bevy_render/src/mesh/mesh/mod.rs index c12d1a688dd91..dd65353489809 100644 --- a/crates/bevy_render/src/mesh/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mesh/mod.rs @@ -623,7 +623,7 @@ impl RenderAsset for Mesh { let vertex_buffer_data = mesh.get_vertex_buffer_data(); let vertex_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { usage: BufferUsages::VERTEX, - label: None, + label: Some("Mesh Vertex Buffer"), contents: &vertex_buffer_data, }); @@ -631,7 +631,7 @@ impl RenderAsset for Mesh { buffer: render_device.create_buffer_with_data(&BufferInitDescriptor { usage: BufferUsages::INDEX, contents: data, - label: None, + label: Some("Mesh Index Buffer"), }), count: mesh.indices().unwrap().len() as u32, index_format: mesh.indices().unwrap().into(), diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 6538b1f604817..1f05d04e66a66 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -72,7 +72,6 @@ pub async fn initialize_renderer( .await .expect("Unable to find a GPU! Make sure you have installed required drivers!"); - #[cfg(not(target_arch = "wasm32"))] info!("{:?}", adapter.get_info()); #[cfg(feature = "wgpu_trace")] diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 7be6163f79efc..2bb395b368fa0 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -44,12 +44,18 @@ pub struct Msaa { /// smoother edges. Note that WGPU currently only supports 1 or 4 samples. /// Ultimately we plan on supporting whatever is natively supported on a given device. /// Check out this issue for more info: + /// It defaults to 1 in wasm - pub samples: u32, } impl Default for Msaa { fn default() -> Self { - Self { samples: 4 } + Self { + #[cfg(feature = "webgl")] + samples: 1, + #[cfg(not(feature = "webgl"))] + samples: 4, + } } }