Path: blob/main/crates/bevy_pbr/src/light_probe/generate.rs
9423 views
//! Like [`EnvironmentMapLight`], but filtered in realtime from a cubemap.1//!2//! An environment map needs to be processed to be able to support uses beyond a simple skybox,3//! such as reflections, and ambient light contribution.4//! This process is called filtering, and can either be done ahead of time (prefiltering), or5//! in realtime, although at a reduced quality. Prefiltering is preferred, but not always possible:6//! sometimes you only gain access to an environment map at runtime, for whatever reason.7//! Typically this is from realtime reflection probes, but can also be from other sources.8//!9//! In any case, Bevy supports both modes of filtering.10//! This module provides realtime filtering via [`bevy_light::GeneratedEnvironmentMapLight`].11//! For prefiltered environment maps, see [`bevy_light::EnvironmentMapLight`].12//! These components are intended to be added to a camera.13use bevy_app::{App, Plugin, Update};14use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Assets, RenderAssetUsages};15use bevy_core_pipeline::mip_generation::{self, DownsampleShaders, DownsamplingConstants};16use bevy_ecs::{17component::Component,18entity::Entity,19query::Without,20resource::Resource,21schedule::IntoScheduleConfigs,22system::{Commands, Query, Res, ResMut},23};24use bevy_image::Image;25use bevy_math::{Quat, UVec2, Vec2};26use bevy_render::{27diagnostic::RecordDiagnostics,28render_asset::RenderAssets,29render_resource::{30binding_types::*, AddressMode, BindGroup, BindGroupEntries, BindGroupLayoutDescriptor,31BindGroupLayoutEntries, CachedComputePipelineId, ComputePassDescriptor,32ComputePipelineDescriptor, DownlevelFlags, Extent3d, FilterMode, MipmapFilterMode,33PipelineCache, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType,34StorageTextureAccess, Texture, TextureAspect, TextureDescriptor, TextureDimension,35TextureFormat, TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor,36TextureViewDimension, UniformBuffer,37},38renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue},39settings::WgpuFeatures,40sync_component::{SyncComponent, SyncComponentPlugin},41sync_world::RenderEntity,42texture::{CachedTexture, GpuImage, TextureCache},43Extract, ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems,44};4546// Implementation: generate diffuse and specular cubemaps required by PBR47// from a given high-res cubemap by48//49// 1. Copying the base mip (level 0) of the source cubemap into an intermediate50// storage texture.51// 2. Generating mipmaps using [single-pass down-sampling] (SPD).52// 3. Convolving the mip chain twice:53// * a [Lambertian convolution] for the 32 × 32 diffuse cubemap54// * a [GGX convolution], once per mip level, for the specular cubemap.55//56// [single-pass down-sampling]: https://gpuopen.com/fidelityfx-spd/57// [Lambertian convolution]: https://bruop.github.io/ibl/#:~:text=Lambertian%20Diffuse%20Component58// [GGX convolution]: https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf5960use bevy_light::{EnvironmentMapLight, GeneratedEnvironmentMapLight};61use bevy_shader::ShaderDefVal;62use core::cmp::min;63use tracing::info;6465use crate::Bluenoise;6667/// Stores the bind group layouts for the environment map generation pipelines68#[derive(Resource)]69pub struct GeneratorBindGroupLayouts {70pub downsampling_first: BindGroupLayoutDescriptor,71pub downsampling_second: BindGroupLayoutDescriptor,72pub radiance: BindGroupLayoutDescriptor,73pub irradiance: BindGroupLayoutDescriptor,74pub copy: BindGroupLayoutDescriptor,75}7677/// Samplers for the environment map generation pipelines78#[derive(Resource)]79pub struct GeneratorSamplers {80pub linear: Sampler,81}8283/// Pipelines for the environment map generation pipelines84#[derive(Resource)]85pub struct GeneratorPipelines {86pub downsample_first: CachedComputePipelineId,87pub downsample_second: CachedComputePipelineId,88pub copy: CachedComputePipelineId,89pub radiance: CachedComputePipelineId,90pub irradiance: CachedComputePipelineId,91}9293/// Configuration for downsampling strategy based on device limits94#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq)]95pub struct DownsamplingConfig {96// can bind ≥12 storage textures and use read-write storage textures97pub combine_bind_group: bool,98}99100pub struct EnvironmentMapGenerationPlugin;101102impl Plugin for EnvironmentMapGenerationPlugin {103fn build(&self, _: &mut App) {}104fn finish(&self, app: &mut App) {105if let Some(render_app) = app.get_sub_app_mut(RenderApp) {106let adapter = render_app.world().resource::<RenderAdapter>();107let device = render_app.world().resource::<RenderDevice>();108109// Cubemap SPD requires at least 6 storage textures110let limit_support = device.limits().max_storage_textures_per_shader_stage >= 6111&& device.limits().max_compute_workgroup_storage_size != 0112&& device.limits().max_compute_workgroup_size_x != 0;113114let downlevel_support = adapter115.get_downlevel_capabilities()116.flags117.contains(DownlevelFlags::COMPUTE_SHADERS);118119if !limit_support || !downlevel_support {120info!("Disabling EnvironmentMapGenerationPlugin because compute is not supported on this platform. This is safe to ignore if you are not using EnvironmentMapGenerationPlugin.");121return;122}123} else {124return;125}126127embedded_asset!(app, "environment_filter.wgsl");128embedded_asset!(app, "copy.wgsl");129130app.add_plugins(SyncComponentPlugin::<GeneratedEnvironmentMapLight, Self>::default())131.add_systems(Update, generate_environment_map_light);132133let Some(render_app) = app.get_sub_app_mut(RenderApp) else {134return;135};136137render_app138.add_systems(139ExtractSchedule,140extract_generated_environment_map_entities.after(generate_environment_map_light),141)142.add_systems(143Render,144(145prepare_generated_environment_map_bind_groups146.in_set(RenderSystems::PrepareBindGroups),147prepare_generated_environment_map_intermediate_textures148.in_set(RenderSystems::PrepareResources),149(downsampling_system, filtering_system)150.chain()151.after(RenderSystems::PrepareBindGroups)152.before(RenderSystems::Render),153),154)155.add_systems(156RenderStartup,157initialize_generated_environment_map_resources,158);159}160}161162/// Initializes all render-world resources used by the environment-map generator once on163/// [`bevy_render::RenderStartup`].164pub fn initialize_generated_environment_map_resources(165mut commands: Commands,166render_device: Res<RenderDevice>,167render_adapter: Res<RenderAdapter>,168pipeline_cache: Res<PipelineCache>,169asset_server: Res<AssetServer>,170downsample_shaders: Res<DownsampleShaders>,171) {172// Combine the bind group and use read-write storage if it is supported173let combine_bind_group =174mip_generation::can_combine_downsampling_bind_groups(&render_adapter, &render_device);175176// Output mips are write-only177let mips =178texture_storage_2d_array(TextureFormat::Rgba16Float, StorageTextureAccess::WriteOnly);179180// Bind group layouts181let (downsampling_first, downsampling_second) = if combine_bind_group {182// One big bind group layout containing all outputs 1–12183let downsampling = BindGroupLayoutDescriptor::new(184"downsampling_bind_group_layout_combined",185&BindGroupLayoutEntries::sequential(186ShaderStages::COMPUTE,187(188sampler(SamplerBindingType::Filtering),189uniform_buffer::<DownsamplingConstants>(false),190texture_2d_array(TextureSampleType::Float { filterable: true }),191mips, // 1192mips, // 2193mips, // 3194mips, // 4195mips, // 5196texture_storage_2d_array(197TextureFormat::Rgba16Float,198StorageTextureAccess::ReadWrite,199), // 6200mips, // 7201mips, // 8202mips, // 9203mips, // 10204mips, // 11205mips, // 12206),207),208);209210(downsampling.clone(), downsampling)211} else {212// Split layout: first pass outputs 1–6, second pass outputs 7–12 (input mip6 read-only)213214let downsampling_first = BindGroupLayoutDescriptor::new(215"downsampling_first_bind_group_layout",216&BindGroupLayoutEntries::sequential(217ShaderStages::COMPUTE,218(219sampler(SamplerBindingType::Filtering),220uniform_buffer::<DownsamplingConstants>(false),221// Input mip 0222texture_2d_array(TextureSampleType::Float { filterable: true }),223mips, // 1224mips, // 2225mips, // 3226mips, // 4227mips, // 5228mips, // 6229),230),231);232233let downsampling_second = BindGroupLayoutDescriptor::new(234"downsampling_second_bind_group_layout",235&BindGroupLayoutEntries::sequential(236ShaderStages::COMPUTE,237(238sampler(SamplerBindingType::Filtering),239uniform_buffer::<DownsamplingConstants>(false),240// Input mip 6241texture_2d_array(TextureSampleType::Float { filterable: true }),242mips, // 7243mips, // 8244mips, // 9245mips, // 10246mips, // 11247mips, // 12248),249),250);251252(downsampling_first, downsampling_second)253};254let radiance = BindGroupLayoutDescriptor::new(255"radiance_bind_group_layout",256&BindGroupLayoutEntries::sequential(257ShaderStages::COMPUTE,258(259// Source environment cubemap260texture_2d_array(TextureSampleType::Float { filterable: true }),261sampler(SamplerBindingType::Filtering), // Source sampler262// Output specular map263texture_storage_2d_array(264TextureFormat::Rgba16Float,265StorageTextureAccess::WriteOnly,266),267uniform_buffer::<FilteringConstants>(false), // Uniforms268texture_2d_array(TextureSampleType::Float { filterable: true }), // Blue noise texture269),270),271);272273let irradiance = BindGroupLayoutDescriptor::new(274"irradiance_bind_group_layout",275&BindGroupLayoutEntries::sequential(276ShaderStages::COMPUTE,277(278// Source environment cubemap279texture_2d_array(TextureSampleType::Float { filterable: true }),280sampler(SamplerBindingType::Filtering), // Source sampler281// Output irradiance map282texture_storage_2d_array(283TextureFormat::Rgba16Float,284StorageTextureAccess::WriteOnly,285),286uniform_buffer::<FilteringConstants>(false), // Uniforms287texture_2d_array(TextureSampleType::Float { filterable: true }), // Blue noise texture288),289),290);291292let copy = BindGroupLayoutDescriptor::new(293"copy_bind_group_layout",294&BindGroupLayoutEntries::sequential(295ShaderStages::COMPUTE,296(297// Source cubemap298texture_2d_array(TextureSampleType::Float { filterable: true }),299// Destination mip0300texture_storage_2d_array(301TextureFormat::Rgba16Float,302StorageTextureAccess::WriteOnly,303),304),305),306);307308let layouts = GeneratorBindGroupLayouts {309downsampling_first,310downsampling_second,311radiance,312irradiance,313copy,314};315316// Samplers317let linear = render_device.create_sampler(&SamplerDescriptor {318label: Some("generator_linear_sampler"),319address_mode_u: AddressMode::ClampToEdge,320address_mode_v: AddressMode::ClampToEdge,321address_mode_w: AddressMode::ClampToEdge,322mag_filter: FilterMode::Linear,323min_filter: FilterMode::Linear,324mipmap_filter: MipmapFilterMode::Linear,325..Default::default()326});327328let samplers = GeneratorSamplers { linear };329330// Pipelines331let features = render_device.features();332let mut shader_defs = vec![];333if features.contains(WgpuFeatures::SUBGROUP) {334shader_defs.push(ShaderDefVal::Int("SUBGROUP_SUPPORT".into(), 1));335}336if combine_bind_group {337shader_defs.push(ShaderDefVal::Int("COMBINE_BIND_GROUP".into(), 1));338}339shader_defs.push(ShaderDefVal::Bool("ARRAY_TEXTURE".into(), true));340#[cfg(feature = "bluenoise_texture")]341{342shader_defs.push(ShaderDefVal::Int("HAS_BLUE_NOISE".into(), 1));343}344345let env_filter_shader = load_embedded_asset!(asset_server.as_ref(), "environment_filter.wgsl");346let copy_shader = load_embedded_asset!(asset_server.as_ref(), "copy.wgsl");347348let downsampling_shader = downsample_shaders349.general350.get(&TextureFormat::Rgba16Float)351.expect("Mip generation shader should exist in the general downsampling shader table");352353// First pass for base mip Levels (0-5)354let downsample_first = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {355label: Some("downsampling_first_pipeline".into()),356layout: vec![layouts.downsampling_first.clone()],357immediate_size: 0,358shader: downsampling_shader.clone(),359shader_defs: {360let mut defs = shader_defs.clone();361if !combine_bind_group {362defs.push(ShaderDefVal::Int("FIRST_PASS".into(), 1));363}364defs365},366entry_point: Some("downsample_first".into()),367zero_initialize_workgroup_memory: false,368});369370let downsample_second = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {371label: Some("downsampling_second_pipeline".into()),372layout: vec![layouts.downsampling_second.clone()],373immediate_size: 0,374shader: downsampling_shader.clone(),375shader_defs: {376let mut defs = shader_defs.clone();377if !combine_bind_group {378defs.push(ShaderDefVal::Int("SECOND_PASS".into(), 1));379}380defs381},382entry_point: Some("downsample_second".into()),383zero_initialize_workgroup_memory: false,384});385386// Radiance map for specular environment maps387let radiance = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {388label: Some("radiance_pipeline".into()),389layout: vec![layouts.radiance.clone()],390immediate_size: 0,391shader: env_filter_shader.clone(),392shader_defs: shader_defs.clone(),393entry_point: Some("generate_radiance_map".into()),394zero_initialize_workgroup_memory: false,395});396397// Irradiance map for diffuse environment maps398let irradiance = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {399label: Some("irradiance_pipeline".into()),400layout: vec![layouts.irradiance.clone()],401immediate_size: 0,402shader: env_filter_shader,403shader_defs: shader_defs.clone(),404entry_point: Some("generate_irradiance_map".into()),405zero_initialize_workgroup_memory: false,406});407408// Copy pipeline handles format conversion and populates mip0 when formats differ409let copy_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {410label: Some("copy_pipeline".into()),411layout: vec![layouts.copy.clone()],412immediate_size: 0,413shader: copy_shader,414shader_defs: vec![],415entry_point: Some("copy".into()),416zero_initialize_workgroup_memory: false,417});418419let pipelines = GeneratorPipelines {420downsample_first,421downsample_second,422radiance,423irradiance,424copy: copy_pipeline,425};426427// Insert all resources into the render world428commands.insert_resource(layouts);429commands.insert_resource(samplers);430commands.insert_resource(pipelines);431commands.insert_resource(DownsamplingConfig { combine_bind_group });432}433434pub fn extract_generated_environment_map_entities(435query: Extract<436Query<(437RenderEntity,438&GeneratedEnvironmentMapLight,439&EnvironmentMapLight,440)>,441>,442mut commands: Commands,443render_images: Res<RenderAssets<GpuImage>>,444) {445for (entity, filtered_env_map, env_map_light) in query.iter() {446let Some(env_map) = render_images.get(&filtered_env_map.environment_map) else {447continue;448};449450let diffuse_map = render_images.get(&env_map_light.diffuse_map);451let specular_map = render_images.get(&env_map_light.specular_map);452453// continue if the diffuse map is not found454if diffuse_map.is_none() || specular_map.is_none() {455continue;456}457458let diffuse_map = diffuse_map.unwrap();459let specular_map = specular_map.unwrap();460461let render_filtered_env_map = RenderEnvironmentMap {462environment_map: env_map.clone(),463diffuse_map: diffuse_map.clone(),464specular_map: specular_map.clone(),465intensity: filtered_env_map.intensity,466rotation: filtered_env_map.rotation,467affects_lightmapped_mesh_diffuse: filtered_env_map.affects_lightmapped_mesh_diffuse,468};469commands470.get_entity(entity)471.expect("Entity not synced to render world")472.insert(render_filtered_env_map);473}474}475476// A render-world specific version of FilteredEnvironmentMapLight that uses CachedTexture477#[derive(Component, Clone)]478pub struct RenderEnvironmentMap {479pub environment_map: GpuImage,480pub diffuse_map: GpuImage,481pub specular_map: GpuImage,482pub intensity: f32,483pub rotation: Quat,484pub affects_lightmapped_mesh_diffuse: bool,485}486487#[derive(Component)]488pub struct IntermediateTextures {489pub environment_map: CachedTexture,490}491492/// Returns the total number of mip levels for the provided square texture size.493/// `size` must be a power of two greater than zero. For example, `size = 512` → `9`.494#[inline]495fn compute_mip_count(size: u32) -> u32 {496debug_assert!(size.is_power_of_two());49732 - size.leading_zeros()498}499500/// Prepares textures needed for single pass downsampling501pub fn prepare_generated_environment_map_intermediate_textures(502light_probes: Query<(Entity, &RenderEnvironmentMap)>,503render_device: Res<RenderDevice>,504mut texture_cache: ResMut<TextureCache>,505mut commands: Commands,506) {507for (entity, env_map_light) in &light_probes {508let base_size = env_map_light.environment_map.texture_descriptor.size.width;509let mip_level_count = compute_mip_count(base_size);510511let environment_map = texture_cache.get(512&render_device,513TextureDescriptor {514label: Some("intermediate_environment_map"),515size: Extent3d {516width: base_size,517height: base_size,518depth_or_array_layers: 6, // Cubemap faces519},520mip_level_count,521sample_count: 1,522dimension: TextureDimension::D2,523format: TextureFormat::Rgba16Float,524usage: TextureUsages::TEXTURE_BINDING525| TextureUsages::STORAGE_BINDING526| TextureUsages::COPY_DST,527view_formats: &[],528},529);530531commands532.entity(entity)533.insert(IntermediateTextures { environment_map });534}535}536537/// Constants for filtering538#[derive(Clone, Copy, ShaderType)]539#[repr(C)]540pub struct FilteringConstants {541mip_level: f32,542sample_count: u32,543roughness: f32,544noise_size_bits: UVec2,545}546547/// Stores bind groups for the environment map generation pipelines548#[derive(Component)]549pub struct GeneratorBindGroups {550pub downsampling_first: BindGroup,551pub downsampling_second: BindGroup,552pub radiance: Vec<BindGroup>, // One per mip level553pub irradiance: BindGroup,554pub copy: BindGroup,555}556557/// Prepares bind groups for environment map generation pipelines558pub fn prepare_generated_environment_map_bind_groups(559light_probes: Query<(Entity, &IntermediateTextures, &RenderEnvironmentMap)>,560render_device: Res<RenderDevice>,561pipeline_cache: Res<PipelineCache>,562queue: Res<RenderQueue>,563layouts: Res<GeneratorBindGroupLayouts>,564samplers: Res<GeneratorSamplers>,565render_images: Res<RenderAssets<GpuImage>>,566bluenoise: Res<Bluenoise>,567config: Res<DownsamplingConfig>,568mut commands: Commands,569) {570// Skip until the blue-noise texture is available to avoid panicking.571// The system will retry next frame once the asset has loaded.572let Some(stbn_texture) = render_images.get(&bluenoise.texture) else {573return;574};575576assert!(stbn_texture.texture_descriptor.size.width.is_power_of_two());577assert!(stbn_texture578.texture_descriptor579.size580.height581.is_power_of_two());582let noise_size_bits = UVec2::new(583stbn_texture.texture_descriptor.size.width.trailing_zeros(),584stbn_texture.texture_descriptor.size.height.trailing_zeros(),585);586587for (entity, textures, env_map_light) in &light_probes {588// Determine mip chain based on input size589let base_size = env_map_light.environment_map.texture_descriptor.size.width;590let mip_count = compute_mip_count(base_size);591let last_mip = mip_count - 1;592let env_map_texture = env_map_light.environment_map.texture.clone();593594// Create downsampling constants595let downsampling_constants = DownsamplingConstants {596mips: mip_count - 1, // Number of mips we are generating (excluding mip 0)597inverse_input_size: Vec2::new(1.0 / base_size as f32, 1.0 / base_size as f32),598_padding: 0,599};600601let mut downsampling_constants_buffer = UniformBuffer::from(downsampling_constants);602downsampling_constants_buffer.write_buffer(&render_device, &queue);603604let input_env_map_first = env_map_texture.clone().create_view(&TextureViewDescriptor {605dimension: Some(TextureViewDimension::D2Array),606..Default::default()607});608609// Utility closure to get a unique storage view for a given mip level.610let mip_storage = |level: u32| {611if level <= last_mip {612create_storage_view(&textures.environment_map.texture, level, &render_device)613} else {614// Return a fresh 1×1 placeholder view so each binding has its own sub-resource and cannot alias.615create_placeholder_storage_view(&render_device)616}617};618619// Depending on device limits, build either a combined or split bind group layout620let (downsampling_first_bind_group, downsampling_second_bind_group) =621if config.combine_bind_group {622// Combined layout expects destinations 1–12 in both bind groups623let bind_group = render_device.create_bind_group(624"downsampling_bind_group_combined_first",625&pipeline_cache.get_bind_group_layout(&layouts.downsampling_first),626&BindGroupEntries::sequential((627&samplers.linear,628&downsampling_constants_buffer,629&input_env_map_first,630&mip_storage(1),631&mip_storage(2),632&mip_storage(3),633&mip_storage(4),634&mip_storage(5),635&mip_storage(6),636&mip_storage(7),637&mip_storage(8),638&mip_storage(9),639&mip_storage(10),640&mip_storage(11),641&mip_storage(12),642)),643);644645(bind_group.clone(), bind_group)646} else {647// Split path requires a separate view for mip6 input648let input_env_map_second =649textures650.environment_map651.texture652.create_view(&TextureViewDescriptor {653dimension: Some(TextureViewDimension::D2Array),654base_mip_level: min(6, last_mip),655mip_level_count: Some(1),656..Default::default()657});658659// Split layout (current behavior)660let first = render_device.create_bind_group(661"downsampling_first_bind_group",662&pipeline_cache.get_bind_group_layout(&layouts.downsampling_first),663&BindGroupEntries::sequential((664&samplers.linear,665&downsampling_constants_buffer,666&input_env_map_first,667&mip_storage(1),668&mip_storage(2),669&mip_storage(3),670&mip_storage(4),671&mip_storage(5),672&mip_storage(6),673)),674);675676let second = render_device.create_bind_group(677"downsampling_second_bind_group",678&pipeline_cache.get_bind_group_layout(&layouts.downsampling_second),679&BindGroupEntries::sequential((680&samplers.linear,681&downsampling_constants_buffer,682&input_env_map_second,683&mip_storage(7),684&mip_storage(8),685&mip_storage(9),686&mip_storage(10),687&mip_storage(11),688&mip_storage(12),689)),690);691692(first, second)693};694695// create a 2d array view of the bluenoise texture696let stbn_texture_view = stbn_texture697.texture698.clone()699.create_view(&TextureViewDescriptor {700dimension: Some(TextureViewDimension::D2Array),701..Default::default()702});703704// Create radiance map bind groups for each mip level705let num_mips = mip_count as usize;706let mut radiance_bind_groups = Vec::with_capacity(num_mips);707708for mip in 0..num_mips {709// Calculate roughness from 0.0 (mip 0) to 0.889 (mip 8)710// We don't need roughness=1.0 as a mip level because it's handled by the separate diffuse irradiance map711let roughness = mip as f32 / (num_mips - 1) as f32;712let sample_count = 32u32 * 2u32.pow((roughness * 4.0) as u32);713714let radiance_constants = FilteringConstants {715mip_level: mip as f32,716sample_count,717roughness,718noise_size_bits,719};720721let mut radiance_constants_buffer = UniformBuffer::from(radiance_constants);722radiance_constants_buffer.write_buffer(&render_device, &queue);723724let mip_storage_view = create_storage_view(725&env_map_light.specular_map.texture,726mip as u32,727&render_device,728);729let bind_group = render_device.create_bind_group(730Some(format!("radiance_bind_group_mip_{mip}").as_str()),731&pipeline_cache.get_bind_group_layout(&layouts.radiance),732&BindGroupEntries::sequential((733&textures.environment_map.default_view,734&samplers.linear,735&mip_storage_view,736&radiance_constants_buffer,737&stbn_texture_view,738)),739);740741radiance_bind_groups.push(bind_group);742}743744// Create irradiance bind group745let irradiance_constants = FilteringConstants {746mip_level: 0.0,747// 32 phi, 32 theta = 1024 samples total748sample_count: 1024,749roughness: 1.0,750noise_size_bits,751};752753let mut irradiance_constants_buffer = UniformBuffer::from(irradiance_constants);754irradiance_constants_buffer.write_buffer(&render_device, &queue);755756// create a 2d array view757let irradiance_map =758env_map_light759.diffuse_map760.texture761.create_view(&TextureViewDescriptor {762dimension: Some(TextureViewDimension::D2Array),763..Default::default()764});765766let irradiance_bind_group = render_device.create_bind_group(767"irradiance_bind_group",768&pipeline_cache.get_bind_group_layout(&layouts.irradiance),769&BindGroupEntries::sequential((770&textures.environment_map.default_view,771&samplers.linear,772&irradiance_map,773&irradiance_constants_buffer,774&stbn_texture_view,775)),776);777778// Create copy bind group (source env map → destination mip0)779let src_view = env_map_light780.environment_map781.texture782.create_view(&TextureViewDescriptor {783dimension: Some(TextureViewDimension::D2Array),784..Default::default()785});786787let dst_view = create_storage_view(&textures.environment_map.texture, 0, &render_device);788789let copy_bind_group = render_device.create_bind_group(790"copy_bind_group",791&pipeline_cache.get_bind_group_layout(&layouts.copy),792&BindGroupEntries::with_indices(((0, &src_view), (1, &dst_view))),793);794795commands.entity(entity).insert(GeneratorBindGroups {796downsampling_first: downsampling_first_bind_group,797downsampling_second: downsampling_second_bind_group,798radiance: radiance_bind_groups,799irradiance: irradiance_bind_group,800copy: copy_bind_group,801});802}803}804805/// Helper function to create a storage texture view for a specific mip level806fn create_storage_view(texture: &Texture, mip: u32, _render_device: &RenderDevice) -> TextureView {807texture.create_view(&TextureViewDescriptor {808label: Some(format!("storage_view_mip_{mip}").as_str()),809format: Some(texture.format()),810dimension: Some(TextureViewDimension::D2Array),811aspect: TextureAspect::All,812base_mip_level: mip,813mip_level_count: Some(1),814base_array_layer: 0,815array_layer_count: Some(texture.depth_or_array_layers()),816usage: Some(TextureUsages::STORAGE_BINDING),817})818}819820/// To ensure compatibility in web browsers, each call returns a unique resource so that multiple missing mip821/// bindings in the same bind-group never alias.822fn create_placeholder_storage_view(render_device: &RenderDevice) -> TextureView {823let tex = render_device.create_texture(&TextureDescriptor {824label: Some("lightprobe_placeholder"),825size: Extent3d {826width: 1,827height: 1,828depth_or_array_layers: 6,829},830mip_level_count: 1,831sample_count: 1,832dimension: TextureDimension::D2,833format: TextureFormat::Rgba16Float,834usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,835view_formats: &[],836});837838tex.create_view(&TextureViewDescriptor::default())839}840841pub fn downsampling_system(842query: Query<(&GeneratorBindGroups, &RenderEnvironmentMap)>,843pipeline_cache: Res<PipelineCache>,844pipelines: Option<Res<GeneratorPipelines>>,845mut ctx: RenderContext,846) {847let Some(pipelines) = pipelines else {848return;849};850851let Some(downsample_first_pipeline) =852pipeline_cache.get_compute_pipeline(pipelines.downsample_first)853else {854return;855};856857let Some(downsample_second_pipeline) =858pipeline_cache.get_compute_pipeline(pipelines.downsample_second)859else {860return;861};862863let Some(copy_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.copy) else {864return;865};866867let diagnostics = ctx.diagnostic_recorder();868let diagnostics = diagnostics.as_deref();869870for (bind_groups, env_map_light) in &query {871// Copy base mip using compute shader with pre-built bind group872{873let mut compute_pass =874ctx.command_encoder()875.begin_compute_pass(&ComputePassDescriptor {876label: Some("lightprobe_copy"),877timestamp_writes: None,878});879880let pass_span = diagnostics.pass_span(&mut compute_pass, "lightprobe_copy");881882compute_pass.set_pipeline(copy_pipeline);883compute_pass.set_bind_group(0, &bind_groups.copy, &[]);884885let tex_size = env_map_light.environment_map.texture_descriptor.size;886let wg_x = tex_size.width.div_ceil(8);887let wg_y = tex_size.height.div_ceil(8);888compute_pass.dispatch_workgroups(wg_x, wg_y, 6);889890pass_span.end(&mut compute_pass);891}892893// First pass - process mips 0-5894{895let mut compute_pass =896ctx.command_encoder()897.begin_compute_pass(&ComputePassDescriptor {898label: Some("lightprobe_downsampling_first_pass"),899timestamp_writes: None,900});901902let pass_span =903diagnostics.pass_span(&mut compute_pass, "lightprobe_downsampling_first_pass");904905compute_pass.set_pipeline(downsample_first_pipeline);906compute_pass.set_bind_group(0, &bind_groups.downsampling_first, &[]);907908let tex_size = env_map_light.environment_map.texture_descriptor.size;909let wg_x = tex_size.width.div_ceil(64);910let wg_y = tex_size.height.div_ceil(64);911compute_pass.dispatch_workgroups(wg_x, wg_y, 6); // 6 faces912913pass_span.end(&mut compute_pass);914}915916// Second pass - process mips 6-12917{918let mut compute_pass =919ctx.command_encoder()920.begin_compute_pass(&ComputePassDescriptor {921label: Some("lightprobe_downsampling_second_pass"),922timestamp_writes: None,923});924925let pass_span =926diagnostics.pass_span(&mut compute_pass, "lightprobe_downsampling_second_pass");927928compute_pass.set_pipeline(downsample_second_pipeline);929compute_pass.set_bind_group(0, &bind_groups.downsampling_second, &[]);930931let tex_size = env_map_light.environment_map.texture_descriptor.size;932let wg_x = tex_size.width.div_ceil(256);933let wg_y = tex_size.height.div_ceil(256);934compute_pass.dispatch_workgroups(wg_x, wg_y, 6);935936pass_span.end(&mut compute_pass);937}938}939}940941pub fn filtering_system(942query: Query<(&GeneratorBindGroups, &RenderEnvironmentMap)>,943pipeline_cache: Res<PipelineCache>,944pipelines: Option<Res<GeneratorPipelines>>,945mut ctx: RenderContext,946) {947let Some(pipelines) = pipelines else {948return;949};950951let Some(radiance_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.radiance) else {952return;953};954let Some(irradiance_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.irradiance)955else {956return;957};958959let diagnostics = ctx.diagnostic_recorder();960let diagnostics = diagnostics.as_deref();961962for (bind_groups, env_map_light) in &query {963// Radiance convolution pass964{965let mut compute_pass =966ctx.command_encoder()967.begin_compute_pass(&ComputePassDescriptor {968label: Some("lightprobe_radiance_map"),969timestamp_writes: None,970});971972let pass_span = diagnostics.pass_span(&mut compute_pass, "lightprobe_radiance_map");973974compute_pass.set_pipeline(radiance_pipeline);975976let base_size = env_map_light.specular_map.texture_descriptor.size.width;977978// Process each mip at different roughness levels979for (mip, bind_group) in bind_groups.radiance.iter().enumerate() {980compute_pass.set_bind_group(0, bind_group, &[]);981982// Calculate dispatch size based on mip level983let mip_size = base_size >> mip;984let workgroup_count = mip_size.div_ceil(8);985986// Dispatch for all 6 faces987compute_pass.dispatch_workgroups(workgroup_count, workgroup_count, 6);988}989990pass_span.end(&mut compute_pass);991}992993// Irradiance convolution pass994// Generate the diffuse environment map995{996let mut compute_pass =997ctx.command_encoder()998.begin_compute_pass(&ComputePassDescriptor {999label: Some("lightprobe_irradiance_map"),1000timestamp_writes: None,1001});10021003let irr_span = diagnostics.pass_span(&mut compute_pass, "lightprobe_irradiance_map");10041005compute_pass.set_pipeline(irradiance_pipeline);1006compute_pass.set_bind_group(0, &bind_groups.irradiance, &[]);10071008// 32×32 texture processed with 8×8 workgroups for all 6 faces1009compute_pass.dispatch_workgroups(4, 4, 6);10101011irr_span.end(&mut compute_pass);1012}1013}1014}10151016/// System that generates an `EnvironmentMapLight` component based on the `GeneratedEnvironmentMapLight` component1017pub fn generate_environment_map_light(1018mut commands: Commands,1019mut images: ResMut<Assets<Image>>,1020query: Query<(Entity, &GeneratedEnvironmentMapLight), Without<EnvironmentMapLight>>,1021) {1022for (entity, filtered_env_map) in &query {1023// Validate and fetch the source cubemap so we can size our targets correctly1024let Some(src_image) = images.get(&filtered_env_map.environment_map) else {1025// Texture not ready yet – try again next frame1026continue;1027};10281029let base_size = src_image.texture_descriptor.size.width;10301031// Sanity checks – square, power-of-two, ≤ 81921032if src_image.texture_descriptor.size.height != base_size1033|| !base_size.is_power_of_two()1034|| base_size > 81921035{1036panic!(1037"GeneratedEnvironmentMapLight source cubemap must be square power-of-two ≤ 8192, got {}×{}",1038base_size, src_image.texture_descriptor.size.height1039);1040}10411042let mip_count = compute_mip_count(base_size);10431044// Create a placeholder for the irradiance map1045let mut diffuse = Image::new_fill(1046Extent3d {1047width: 32,1048height: 32,1049depth_or_array_layers: 6,1050},1051TextureDimension::D2,1052&[0; 8],1053TextureFormat::Rgba16Float,1054RenderAssetUsages::all(),1055);10561057diffuse.texture_descriptor.usage =1058TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING;10591060diffuse.texture_view_descriptor = Some(TextureViewDescriptor {1061dimension: Some(TextureViewDimension::Cube),1062..Default::default()1063});10641065let diffuse_handle = images.add(diffuse);10661067// Create a placeholder for the specular map. It matches the input cubemap resolution.1068let mut specular = Image::new_fill(1069Extent3d {1070width: base_size,1071height: base_size,1072depth_or_array_layers: 6,1073},1074TextureDimension::D2,1075&[0; 8],1076TextureFormat::Rgba16Float,1077RenderAssetUsages::all(),1078);10791080// Set up for mipmaps1081specular.texture_descriptor.usage =1082TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING;1083specular.texture_descriptor.mip_level_count = mip_count;10841085// When setting mip_level_count, we need to allocate appropriate data size1086// For GPU-generated mipmaps, we can set data to None since the GPU will generate the data1087specular.data = None;10881089specular.texture_view_descriptor = Some(TextureViewDescriptor {1090dimension: Some(TextureViewDimension::Cube),1091mip_level_count: Some(mip_count),1092..Default::default()1093});10941095let specular_handle = images.add(specular);10961097// Add the EnvironmentMapLight component with the placeholder handles1098commands.entity(entity).insert(EnvironmentMapLight {1099diffuse_map: diffuse_handle,1100specular_map: specular_handle,1101intensity: filtered_env_map.intensity,1102rotation: filtered_env_map.rotation,1103affects_lightmapped_mesh_diffuse: filtered_env_map.affects_lightmapped_mesh_diffuse,1104});1105}1106}11071108impl SyncComponent<EnvironmentMapGenerationPlugin> for GeneratedEnvironmentMapLight {1109type Out = RenderEnvironmentMap;1110}111111121113