Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
#import bevy_pbr::{
mesh_view_types::{Lights, DirectionalLight},
atmosphere::{
types::{Atmosphere, AtmosphereSettings},
bindings::{atmosphere, settings, view, lights, aerial_view_lut_out},
functions::{
sample_transmittance_lut, sample_atmosphere, rayleigh, henyey_greenstein,
sample_multiscattering_lut, AtmosphereSample, sample_local_inscattering,
get_local_r, get_local_up, view_radius, uv_to_ndc, max_atmosphere_distance,
uv_to_ray_direction, MIDPOINT_RATIO
},
}
}
@group(0) @binding(13) var aerial_view_lut_out: texture_storage_3d<rgba16float, write>;
@compute
@workgroup_size(16, 16, 1)
fn main(@builtin(global_invocation_id) idx: vec3<u32>) {
if any(idx.xy > settings.aerial_view_lut_size.xy) { return; }
let uv = (vec2<f32>(idx.xy) + 0.5) / vec2<f32>(settings.aerial_view_lut_size.xy);
let ray_dir = uv_to_ray_direction(uv);
let r = view_radius();
let mu = ray_dir.y;
let t_max = settings.aerial_view_lut_max_distance;
var prev_t = 0.0;
var total_inscattering = vec3(0.0);
var throughput = vec3(1.0);
for (var slice_i: u32 = 0; slice_i < settings.aerial_view_lut_size.z; slice_i++) {
for (var step_i: u32 = 0; step_i < settings.aerial_view_lut_samples; step_i++) {
let t_i = t_max * (f32(slice_i) + ((f32(step_i) + MIDPOINT_RATIO) / f32(settings.aerial_view_lut_samples))) / f32(settings.aerial_view_lut_size.z);
let dt = (t_i - prev_t);
prev_t = t_i;
let local_r = get_local_r(r, mu, t_i);
let local_up = get_local_up(r, t_i, ray_dir.xyz);
let local_atmosphere = sample_atmosphere(local_r);
let sample_optical_depth = local_atmosphere.extinction * dt;
let sample_transmittance = exp(-sample_optical_depth);
// evaluate one segment of the integral
var inscattering = sample_local_inscattering(local_atmosphere, ray_dir.xyz, local_r, local_up);
// Analytical integration of the single scattering term in the radiance transfer equation
let s_int = (inscattering - inscattering * sample_transmittance) / local_atmosphere.extinction;
total_inscattering += throughput * s_int;
throughput *= sample_transmittance;
if all(throughput < vec3(0.001)) {
break;
}
}
// Store in log space to allow linear interpolation of exponential values between slices
let log_inscattering = log(max(total_inscattering, vec3(1e-6)));
textureStore(aerial_view_lut_out, vec3(vec2<u32>(idx.xy), slice_i), vec4(log_inscattering, 0.0));
}
}

View File

@@ -0,0 +1,22 @@
#define_import_path bevy_pbr::atmosphere::bindings
#import bevy_render::view::View;
#import bevy_pbr::{
mesh_view_types::Lights,
atmosphere::types::{Atmosphere, AtmosphereSettings, AtmosphereTransforms}
}
@group(0) @binding(0) var<uniform> atmosphere: Atmosphere;
@group(0) @binding(1) var<uniform> settings: AtmosphereSettings;
@group(0) @binding(2) var<uniform> atmosphere_transforms: AtmosphereTransforms;
@group(0) @binding(3) var<uniform> view: View;
@group(0) @binding(4) var<uniform> lights: Lights;
@group(0) @binding(5) var transmittance_lut: texture_2d<f32>;
@group(0) @binding(6) var transmittance_lut_sampler: sampler;
@group(0) @binding(7) var multiscattering_lut: texture_2d<f32>;
@group(0) @binding(8) var multiscattering_lut_sampler: sampler;
@group(0) @binding(9) var sky_view_lut: texture_2d<f32>;
@group(0) @binding(10) var sky_view_lut_sampler: sampler;
@group(0) @binding(11) var aerial_view_lut: texture_3d<f32>;
@group(0) @binding(12) var aerial_view_lut_sampler: sampler;

View File

@@ -0,0 +1,139 @@
// Copyright (c) 2017 Eric Bruneton
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// 3. Neither the name of the copyright holders nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
//
// Precomputed Atmospheric Scattering
// Copyright (c) 2008 INRIA
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// 3. Neither the name of the copyright holders nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
#define_import_path bevy_pbr::atmosphere::bruneton_functions
#import bevy_pbr::atmosphere::{
types::Atmosphere,
bindings::atmosphere,
}
// Mapping from view height (r) and zenith cos angle (mu) to UV coordinates in the transmittance LUT
// Assuming r between ground and top atmosphere boundary, and mu= cos(zenith_angle)
// Chosen to increase precision near the ground and to work around a discontinuity at the horizon
// See Bruneton and Neyret 2008, "Precomputed Atmospheric Scattering" section 4
fn transmittance_lut_r_mu_to_uv(r: f32, mu: f32) -> vec2<f32> {
// Distance along a horizontal ray from the ground to the top atmosphere boundary
let H = sqrt(atmosphere.top_radius * atmosphere.top_radius - atmosphere.bottom_radius * atmosphere.bottom_radius);
// Distance from a point at height r to the horizon
// ignore the case where r <= atmosphere.bottom_radius
let rho = sqrt(max(r * r - atmosphere.bottom_radius * atmosphere.bottom_radius, 0.0));
// Distance from a point at height r to the top atmosphere boundary at zenith angle mu
let d = distance_to_top_atmosphere_boundary(r, mu);
// Minimum and maximum distance to the top atmosphere boundary from a point at height r
let d_min = atmosphere.top_radius - r; // length of the ray straight up to the top atmosphere boundary
let d_max = rho + H; // length of the ray to the top atmosphere boundary and grazing the horizon
let u = (d - d_min) / (d_max - d_min);
let v = rho / H;
return vec2<f32>(u, v);
}
// Inverse of the mapping above, mapping from UV coordinates in the transmittance LUT to view height (r) and zenith cos angle (mu)
fn transmittance_lut_uv_to_r_mu(uv: vec2<f32>) -> vec2<f32> {
// Distance to top atmosphere boundary for a horizontal ray at ground level
let H = sqrt(atmosphere.top_radius * atmosphere.top_radius - atmosphere.bottom_radius * atmosphere.bottom_radius);
// Distance to the horizon, from which we can compute r:
let rho = H * uv.y;
let r = sqrt(rho * rho + atmosphere.bottom_radius * atmosphere.bottom_radius);
// Distance to the top atmosphere boundary for the ray (r,mu), and its minimum
// and maximum values over all mu- obtained for (r,1) and (r,mu_horizon) -
// from which we can recover mu:
let d_min = atmosphere.top_radius - r;
let d_max = rho + H;
let d = d_min + uv.x * (d_max - d_min);
var mu: f32;
if d == 0.0 {
mu = 1.0;
} else {
mu = (H * H - rho * rho - d * d) / (2.0 * r * d);
}
mu = clamp(mu, -1.0, 1.0);
return vec2<f32>(r, mu);
}
/// Simplified ray-sphere intersection
/// where:
/// Ray origin, o = [0,0,r] with r <= atmosphere.top_radius
/// mu is the cosine of spherical coordinate theta (-1.0 <= mu <= 1.0)
/// so ray direction in spherical coordinates is [1,acos(mu),0] which needs to be converted to cartesian
/// Direction of ray, u = [0,sqrt(1-mu*mu),mu]
/// Center of sphere, c = [0,0,0]
/// Radius of sphere, r = atmosphere.top_radius
/// This function solves the quadratic equation for line-sphere intersection simplified under these assumptions
fn distance_to_top_atmosphere_boundary(r: f32, mu: f32) -> f32 {
// ignore the case where r > atmosphere.top_radius
let positive_discriminant = max(r * r * (mu * mu - 1.0) + atmosphere.top_radius * atmosphere.top_radius, 0.0);
return max(-r * mu + sqrt(positive_discriminant), 0.0);
}
/// Simplified ray-sphere intersection
/// as above for intersections with the ground
fn distance_to_bottom_atmosphere_boundary(r: f32, mu: f32) -> f32 {
let positive_discriminant = max(r * r * (mu * mu - 1.0) + atmosphere.bottom_radius * atmosphere.bottom_radius, 0.0);
return max(-r * mu - sqrt(positive_discriminant), 0.0);
}
fn ray_intersects_ground(r: f32, mu: f32) -> bool {
return mu < 0.0 && r * r * (mu * mu - 1.0) + atmosphere.bottom_radius * atmosphere.bottom_radius >= 0.0;
}

View File

@@ -0,0 +1,379 @@
#define_import_path bevy_pbr::atmosphere::functions
#import bevy_render::maths::{PI, HALF_PI, PI_2, fast_acos, fast_acos_4, fast_atan2}
#import bevy_pbr::atmosphere::{
types::Atmosphere,
bindings::{
atmosphere, settings, view, lights, transmittance_lut, transmittance_lut_sampler,
multiscattering_lut, multiscattering_lut_sampler, sky_view_lut, sky_view_lut_sampler,
aerial_view_lut, aerial_view_lut_sampler, atmosphere_transforms
},
bruneton_functions::{
transmittance_lut_r_mu_to_uv, transmittance_lut_uv_to_r_mu,
ray_intersects_ground, distance_to_top_atmosphere_boundary,
distance_to_bottom_atmosphere_boundary
},
}
// NOTE FOR CONVENTIONS:
// r:
// radius, or distance from planet center
//
// altitude:
// distance from planet **surface**
//
// mu:
// cosine of the zenith angle of a ray with
// respect to the planet normal
//
// atmosphere space:
// abbreviated as "as" (contrast with vs, cs, ws), this space is similar
// to view space, but with the camera positioned horizontally on the planet
// surface, so the horizon is a horizontal line centered vertically in the
// frame. This enables the non-linear latitude parametrization the paper uses
// to concentrate detail near the horizon
// CONSTANTS
const FRAC_PI: f32 = 0.3183098862; // 1 / π
const FRAC_2_PI: f32 = 0.15915494309; // 1 / (2π)
const FRAC_3_16_PI: f32 = 0.0596831036594607509; // 3 / (16π)
const FRAC_4_PI: f32 = 0.07957747154594767; // 1 / (4π)
const ROOT_2: f32 = 1.41421356; // √2
// During raymarching, each segment is sampled at a single point. This constant determines
// where in the segment that sample is taken (0.0 = start, 0.5 = middle, 1.0 = end).
// We use 0.3 to sample closer to the start of each segment, which better approximates
// the exponential falloff of atmospheric density.
const MIDPOINT_RATIO: f32 = 0.3;
// LUT UV PARAMATERIZATIONS
fn unit_to_sub_uvs(val: vec2<f32>, resolution: vec2<f32>) -> vec2<f32> {
return (val + 0.5f / resolution) * (resolution / (resolution + 1.0f));
}
fn sub_uvs_to_unit(val: vec2<f32>, resolution: vec2<f32>) -> vec2<f32> {
return (val - 0.5f / resolution) * (resolution / (resolution - 1.0f));
}
fn multiscattering_lut_r_mu_to_uv(r: f32, mu: f32) -> vec2<f32> {
let u = 0.5 + 0.5 * mu;
let v = saturate((r - atmosphere.bottom_radius) / (atmosphere.top_radius - atmosphere.bottom_radius)); //TODO
return unit_to_sub_uvs(vec2(u, v), vec2<f32>(settings.multiscattering_lut_size));
}
fn multiscattering_lut_uv_to_r_mu(uv: vec2<f32>) -> vec2<f32> {
let adj_uv = sub_uvs_to_unit(uv, vec2<f32>(settings.multiscattering_lut_size));
let r = mix(atmosphere.bottom_radius, atmosphere.top_radius, adj_uv.y);
let mu = adj_uv.x * 2 - 1;
return vec2(r, mu);
}
fn sky_view_lut_r_mu_azimuth_to_uv(r: f32, mu: f32, azimuth: f32) -> vec2<f32> {
let u = (azimuth * FRAC_2_PI) + 0.5;
let v_horizon = sqrt(r * r - atmosphere.bottom_radius * atmosphere.bottom_radius);
let cos_beta = v_horizon / r;
// Using fast_acos_4 for better precision at small angles
// to avoid artifacts at the horizon
let beta = fast_acos_4(cos_beta);
let horizon_zenith = PI - beta;
let view_zenith = fast_acos_4(mu);
// Apply non-linear transformation to compress more texels
// near the horizon where high-frequency details matter most
// l is latitude in [-π/2, π/2] and v is texture coordinate in [0,1]
let l = view_zenith - horizon_zenith;
let abs_l = abs(l);
let v = 0.5 + 0.5 * sign(l) * sqrt(abs_l / HALF_PI);
return unit_to_sub_uvs(vec2(u, v), vec2<f32>(settings.sky_view_lut_size));
}
fn sky_view_lut_uv_to_zenith_azimuth(r: f32, uv: vec2<f32>) -> vec2<f32> {
let adj_uv = sub_uvs_to_unit(vec2(uv.x, 1.0 - uv.y), vec2<f32>(settings.sky_view_lut_size));
let azimuth = (adj_uv.x - 0.5) * PI_2;
// Horizon parameters
let v_horizon = sqrt(r * r - atmosphere.bottom_radius * atmosphere.bottom_radius);
let cos_beta = v_horizon / r;
let beta = fast_acos_4(cos_beta);
let horizon_zenith = PI - beta;
// Inverse of horizon-detail mapping to recover original latitude from texture coordinate
let t = abs(2.0 * (adj_uv.y - 0.5));
let l = sign(adj_uv.y - 0.5) * HALF_PI * t * t;
return vec2(horizon_zenith - l, azimuth);
}
// LUT SAMPLING
fn sample_transmittance_lut(r: f32, mu: f32) -> vec3<f32> {
let uv = transmittance_lut_r_mu_to_uv(r, mu);
return textureSampleLevel(transmittance_lut, transmittance_lut_sampler, uv, 0.0).rgb;
}
// NOTICE: This function is copyrighted by Eric Bruneton and INRIA, and falls
// under the license reproduced in bruneton_functions.wgsl (variant of MIT license)
//
// FIXME: this function should be in bruneton_functions.wgsl, but because naga_oil doesn't
// support cyclic imports it's stuck here
fn sample_transmittance_lut_segment(r: f32, mu: f32, t: f32) -> vec3<f32> {
let r_t = get_local_r(r, mu, t);
let mu_t = clamp((r * mu + t) / r_t, -1.0, 1.0);
if ray_intersects_ground(r, mu) {
return min(
sample_transmittance_lut(r_t, -mu_t) / sample_transmittance_lut(r, -mu),
vec3(1.0)
);
} else {
return min(
sample_transmittance_lut(r, mu) / sample_transmittance_lut(r_t, mu_t), vec3(1.0)
);
}
}
fn sample_multiscattering_lut(r: f32, mu: f32) -> vec3<f32> {
let uv = multiscattering_lut_r_mu_to_uv(r, mu);
return textureSampleLevel(multiscattering_lut, multiscattering_lut_sampler, uv, 0.0).rgb;
}
fn sample_sky_view_lut(r: f32, ray_dir_as: vec3<f32>) -> vec3<f32> {
let mu = ray_dir_as.y;
let azimuth = fast_atan2(ray_dir_as.x, -ray_dir_as.z);
let uv = sky_view_lut_r_mu_azimuth_to_uv(r, mu, azimuth);
return textureSampleLevel(sky_view_lut, sky_view_lut_sampler, uv, 0.0).rgb;
}
fn ndc_to_camera_dist(ndc: vec3<f32>) -> f32 {
let view_pos = view.view_from_clip * vec4(ndc, 1.0);
let t = length(view_pos.xyz / view_pos.w) * settings.scene_units_to_m;
return t;
}
// RGB channels: total inscattered light along the camera ray to the current sample.
// A channel: average transmittance across all wavelengths to the current sample.
fn sample_aerial_view_lut(uv: vec2<f32>, t: f32) -> vec3<f32> {
let t_max = settings.aerial_view_lut_max_distance;
let num_slices = f32(settings.aerial_view_lut_size.z);
// Each texel stores the value of the scattering integral over the whole slice,
// which requires us to offset the w coordinate by half a slice. For
// example, if we wanted the value of the integral at the boundary between slices,
// we'd need to sample at the center of the previous slice, and vice-versa for
// sampling in the center of a slice.
let uvw = vec3(uv, saturate(t / t_max - 0.5 / num_slices));
let sample = textureSampleLevel(aerial_view_lut, aerial_view_lut_sampler, uvw, 0.0);
// Since sampling anywhere between w=0 and w=t_slice will clamp to the first slice,
// we need to do a linear step over the first slice towards zero at the camera's
// position to recover the correct integral value.
let t_slice = t_max / num_slices;
let fade = saturate(t / t_slice);
// Recover the values from log space
return exp(sample.rgb) * fade;
}
// PHASE FUNCTIONS
// -(L . V) == (L . -V). -V here is our ray direction, which points away from the view
// instead of towards it (which would be the *view direction*, V)
// evaluates the rayleigh phase function, which describes the likelihood
// of a rayleigh scattering event scattering light from the light direction towards the view
fn rayleigh(neg_LdotV: f32) -> f32 {
return FRAC_3_16_PI * (1 + (neg_LdotV * neg_LdotV));
}
// evaluates the henyey-greenstein phase function, which describes the likelihood
// of a mie scattering event scattering light from the light direction towards the view
fn henyey_greenstein(neg_LdotV: f32) -> f32 {
let g = atmosphere.mie_asymmetry;
let denom = 1.0 + g * g - 2.0 * g * neg_LdotV;
return FRAC_4_PI * (1.0 - g * g) / (denom * sqrt(denom));
}
// ATMOSPHERE SAMPLING
struct AtmosphereSample {
/// units: m^-1
rayleigh_scattering: vec3<f32>,
/// units: m^-1
mie_scattering: f32,
/// the sum of scattering and absorption. Since the phase function doesn't
/// matter for this, we combine rayleigh and mie extinction to a single
// value.
//
/// units: m^-1
extinction: vec3<f32>
}
/// Samples atmosphere optical densities at a given radius
fn sample_atmosphere(r: f32) -> AtmosphereSample {
let altitude = clamp(r, atmosphere.bottom_radius, atmosphere.top_radius) - atmosphere.bottom_radius;
// atmosphere values at altitude
let mie_density = exp(-atmosphere.mie_density_exp_scale * altitude);
let rayleigh_density = exp(-atmosphere.rayleigh_density_exp_scale * altitude);
var ozone_density: f32 = max(0.0, 1.0 - (abs(altitude - atmosphere.ozone_layer_altitude) / (atmosphere.ozone_layer_width * 0.5)));
let mie_scattering = mie_density * atmosphere.mie_scattering;
let mie_absorption = mie_density * atmosphere.mie_absorption;
let mie_extinction = mie_scattering + mie_absorption;
let rayleigh_scattering = rayleigh_density * atmosphere.rayleigh_scattering;
// no rayleigh absorption
// rayleigh extinction is the sum of scattering and absorption
// ozone doesn't contribute to scattering
let ozone_absorption = ozone_density * atmosphere.ozone_absorption;
var sample: AtmosphereSample;
sample.rayleigh_scattering = rayleigh_scattering;
sample.mie_scattering = mie_scattering;
sample.extinction = rayleigh_scattering + mie_extinction + ozone_absorption;
return sample;
}
/// evaluates L_scat, equation 3 in the paper, which gives the total single-order scattering towards the view at a single point
fn sample_local_inscattering(local_atmosphere: AtmosphereSample, ray_dir: vec3<f32>, local_r: f32, local_up: vec3<f32>) -> vec3<f32> {
var inscattering = vec3(0.0);
for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) {
let light = &lights.directional_lights[light_i];
let mu_light = dot((*light).direction_to_light, local_up);
// -(L . V) == (L . -V). -V here is our ray direction, which points away from the view
// instead of towards it (as is the convention for V)
let neg_LdotV = dot((*light).direction_to_light, ray_dir);
// Phase functions give the proportion of light
// scattered towards the camera for each scattering type
let rayleigh_phase = rayleigh(neg_LdotV);
let mie_phase = henyey_greenstein(neg_LdotV);
let scattering_coeff = local_atmosphere.rayleigh_scattering * rayleigh_phase + local_atmosphere.mie_scattering * mie_phase;
let transmittance_to_light = sample_transmittance_lut(local_r, mu_light);
let shadow_factor = transmittance_to_light * f32(!ray_intersects_ground(local_r, mu_light));
// Transmittance from scattering event to light source
let scattering_factor = shadow_factor * scattering_coeff;
// Additive factor from the multiscattering LUT
let psi_ms = sample_multiscattering_lut(local_r, mu_light);
let multiscattering_factor = psi_ms * (local_atmosphere.rayleigh_scattering + local_atmosphere.mie_scattering);
inscattering += (*light).color.rgb * (scattering_factor + multiscattering_factor);
}
return inscattering * view.exposure;
}
const SUN_ANGULAR_SIZE: f32 = 0.0174533; // angular diameter of sun in radians
fn sample_sun_radiance(ray_dir_ws: vec3<f32>) -> vec3<f32> {
let r = view_radius();
let mu_view = ray_dir_ws.y;
let shadow_factor = f32(!ray_intersects_ground(r, mu_view));
var sun_radiance = vec3(0.0);
for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) {
let light = &lights.directional_lights[light_i];
let neg_LdotV = dot((*light).direction_to_light, ray_dir_ws);
let angle_to_sun = fast_acos(neg_LdotV);
let pixel_size = fwidth(angle_to_sun);
let factor = smoothstep(0.0, -pixel_size * ROOT_2, angle_to_sun - SUN_ANGULAR_SIZE * 0.5);
let sun_solid_angle = (SUN_ANGULAR_SIZE * SUN_ANGULAR_SIZE) * 4.0 * FRAC_PI;
sun_radiance += ((*light).color.rgb / sun_solid_angle) * factor * shadow_factor;
}
return sun_radiance;
}
// TRANSFORM UTILITIES
fn max_atmosphere_distance(r: f32, mu: f32) -> f32 {
let t_top = distance_to_top_atmosphere_boundary(r, mu);
let t_bottom = distance_to_bottom_atmosphere_boundary(r, mu);
let hits = ray_intersects_ground(r, mu);
return mix(t_top, t_bottom, f32(hits));
}
/// Assuming y=0 is the planet ground, returns the view radius in meters
fn view_radius() -> f32 {
return view.world_position.y * settings.scene_units_to_m + atmosphere.bottom_radius;
}
// We assume the `up` vector at the view position is the y axis, since the world is locally flat/level.
// t = distance along view ray in atmosphere space
// NOTE: this means that if your world is actually spherical, this will be wrong.
fn get_local_up(r: f32, t: f32, ray_dir: vec3<f32>) -> vec3<f32> {
return normalize(vec3(0.0, r, 0.0) + t * ray_dir);
}
// Given a ray starting at radius r, with mu = cos(zenith angle),
// and a t = distance along the ray, gives the new radius at point t
fn get_local_r(r: f32, mu: f32, t: f32) -> f32 {
return sqrt(t * t + 2.0 * r * mu * t + r * r);
}
// Convert uv [0.0 .. 1.0] coordinate to ndc space xy [-1.0 .. 1.0]
fn uv_to_ndc(uv: vec2<f32>) -> vec2<f32> {
return uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0);
}
/// Convert ndc space xy coordinate [-1.0 .. 1.0] to uv [0.0 .. 1.0]
fn ndc_to_uv(ndc: vec2<f32>) -> vec2<f32> {
return ndc * vec2(0.5, -0.5) + vec2(0.5);
}
/// Converts a direction in world space to atmosphere space
fn direction_world_to_atmosphere(dir_ws: vec3<f32>) -> vec3<f32> {
let dir_as = atmosphere_transforms.atmosphere_from_world * vec4(dir_ws, 0.0);
return dir_as.xyz;
}
/// Converts a direction in atmosphere space to world space
fn direction_atmosphere_to_world(dir_as: vec3<f32>) -> vec3<f32> {
let dir_ws = atmosphere_transforms.world_from_atmosphere * vec4(dir_as, 0.0);
return dir_ws.xyz;
}
// Modified from skybox.wgsl. For this pass we don't need to apply a separate sky transform or consider camera viewport.
// w component is the cosine of the view direction with the view forward vector, to correct step distance at the edges of the viewport
fn uv_to_ray_direction(uv: vec2<f32>) -> vec4<f32> {
// Using world positions of the fragment and camera to calculate a ray direction
// breaks down at large translations. This code only needs to know the ray direction.
// The ray direction is along the direction from the camera to the fragment position.
// In view space, the camera is at the origin, so the view space ray direction is
// along the direction of the fragment position - (0,0,0) which is just the
// fragment position.
// Use the position on the near clipping plane to avoid -inf world position
// because the far plane of an infinite reverse projection is at infinity.
let view_position_homogeneous = view.view_from_clip * vec4(
uv_to_ndc(uv),
1.0,
1.0,
);
let view_ray_direction = view_position_homogeneous.xyz / view_position_homogeneous.w;
// Transforming the view space ray direction by the inverse view matrix, transforms the
// direction to world space. Note that the w element is set to 0.0, as this is a
// vector direction, not a position, That causes the matrix multiplication to ignore
// the translations from the view matrix.
let ray_direction = (view.world_from_view * vec4(view_ray_direction, 0.0)).xyz;
return vec4(normalize(ray_direction), -view_ray_direction.z);
}
fn zenith_azimuth_to_ray_dir(zenith: f32, azimuth: f32) -> vec3<f32> {
let sin_zenith = sin(zenith);
let mu = cos(zenith);
let sin_azimuth = sin(azimuth);
let cos_azimuth = cos(azimuth);
return vec3(sin_azimuth * sin_zenith, mu, -cos_azimuth * sin_zenith);
}

472
vendor/bevy_pbr/src/atmosphere/mod.rs vendored Normal file
View File

@@ -0,0 +1,472 @@
//! Procedural Atmospheric Scattering.
//!
//! This plugin implements [Hillaire's 2020 paper](https://sebh.github.io/publications/egsr2020.pdf)
//! on real-time atmospheric scattering. While it *will* work simply as a
//! procedural skybox, it also does much more. It supports dynamic time-of-
//! -day, multiple directional lights, and since it's applied as a post-processing
//! effect *on top* of the existing skybox, a starry skybox would automatically
//! show based on the time of day. Scattering in front of terrain (similar
//! to distance fog, but more complex) is handled as well, and takes into
//! account the directional light color and direction.
//!
//! Adding the [`Atmosphere`] component to a 3d camera will enable the effect,
//! which by default is set to look similar to Earth's atmosphere. See the
//! documentation on the component itself for information regarding its fields.
//!
//! Performance-wise, the effect should be fairly cheap since the LUTs (Look
//! Up Tables) that encode most of the data are small, and take advantage of the
//! fact that the atmosphere is symmetric. Performance is also proportional to
//! the number of directional lights in the scene. In order to tune
//! performance more finely, the [`AtmosphereSettings`] camera component
//! manages the size of each LUT and the sample count for each ray.
//!
//! Given how similar it is to [`crate::volumetric_fog`], it might be expected
//! that these two modules would work together well. However for now using both
//! at once is untested, and might not be physically accurate. These may be
//! integrated into a single module in the future.
//!
//! On web platforms, atmosphere rendering will look slightly different. Specifically, when calculating how light travels
//! through the atmosphere, we use a simpler averaging technique instead of the more
//! complex blending operations. This difference will be resolved for WebGPU in a future release.
//!
//! [Shadertoy]: https://www.shadertoy.com/view/slSXRW
//!
//! [Unreal Engine Implementation]: https://github.com/sebh/UnrealEngineSkyAtmosphere
mod node;
pub mod resources;
use bevy_app::{App, Plugin};
use bevy_asset::load_internal_asset;
use bevy_core_pipeline::core_3d::graph::Node3d;
use bevy_ecs::{
component::Component,
query::{Changed, QueryItem, With},
schedule::IntoScheduleConfigs,
system::{lifetimeless::Read, Query},
};
use bevy_math::{UVec2, UVec3, Vec3};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
extract_component::UniformComponentPlugin,
render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines},
};
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_graph::{RenderGraphApp, ViewNodeRunner},
render_resource::{Shader, TextureFormat, TextureUsages},
renderer::RenderAdapter,
Render, RenderApp, RenderSet,
};
use bevy_core_pipeline::core_3d::{graph::Core3d, Camera3d};
use resources::{
prepare_atmosphere_transforms, queue_render_sky_pipelines, AtmosphereTransforms,
RenderSkyBindGroupLayouts,
};
use tracing::warn;
use self::{
node::{AtmosphereLutsNode, AtmosphereNode, RenderSkyNode},
resources::{
prepare_atmosphere_bind_groups, prepare_atmosphere_textures, AtmosphereBindGroupLayouts,
AtmosphereLutPipelines, AtmosphereSamplers,
},
};
mod shaders {
use bevy_asset::{weak_handle, Handle};
use bevy_render::render_resource::Shader;
pub const TYPES: Handle<Shader> = weak_handle!("ef7e147e-30a0-4513-bae3-ddde2a6c20c5");
pub const FUNCTIONS: Handle<Shader> = weak_handle!("7ff93872-2ee9-4598-9f88-68b02fef605f");
pub const BRUNETON_FUNCTIONS: Handle<Shader> =
weak_handle!("e2dccbb0-7322-444a-983b-e74d0a08bcda");
pub const BINDINGS: Handle<Shader> = weak_handle!("bcc55ce5-0fc4-451e-8393-1b9efd2612c4");
pub const TRANSMITTANCE_LUT: Handle<Shader> =
weak_handle!("a4187282-8cb1-42d3-889c-cbbfb6044183");
pub const MULTISCATTERING_LUT: Handle<Shader> =
weak_handle!("bde3a71a-73e9-49fe-a379-a81940c67a1e");
pub const SKY_VIEW_LUT: Handle<Shader> = weak_handle!("f87e007a-bf4b-4f99-9ef0-ac21d369f0e5");
pub const AERIAL_VIEW_LUT: Handle<Shader> =
weak_handle!("a3daf030-4b64-49ae-a6a7-354489597cbe");
pub const RENDER_SKY: Handle<Shader> = weak_handle!("09422f46-d0f7-41c1-be24-121c17d6e834");
}
#[doc(hidden)]
pub struct AtmospherePlugin;
impl Plugin for AtmospherePlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, shaders::TYPES, "types.wgsl", Shader::from_wgsl);
load_internal_asset!(app, shaders::FUNCTIONS, "functions.wgsl", Shader::from_wgsl);
load_internal_asset!(
app,
shaders::BRUNETON_FUNCTIONS,
"bruneton_functions.wgsl",
Shader::from_wgsl
);
load_internal_asset!(app, shaders::BINDINGS, "bindings.wgsl", Shader::from_wgsl);
load_internal_asset!(
app,
shaders::TRANSMITTANCE_LUT,
"transmittance_lut.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
shaders::MULTISCATTERING_LUT,
"multiscattering_lut.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
shaders::SKY_VIEW_LUT,
"sky_view_lut.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
shaders::AERIAL_VIEW_LUT,
"aerial_view_lut.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
shaders::RENDER_SKY,
"render_sky.wgsl",
Shader::from_wgsl
);
app.register_type::<Atmosphere>()
.register_type::<AtmosphereSettings>()
.add_plugins((
ExtractComponentPlugin::<Atmosphere>::default(),
ExtractComponentPlugin::<AtmosphereSettings>::default(),
UniformComponentPlugin::<Atmosphere>::default(),
UniformComponentPlugin::<AtmosphereSettings>::default(),
));
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
let render_adapter = render_app.world().resource::<RenderAdapter>();
if !render_adapter
.get_downlevel_capabilities()
.flags
.contains(DownlevelFlags::COMPUTE_SHADERS)
{
warn!("AtmospherePlugin not loaded. GPU lacks support for compute shaders.");
return;
}
if !render_adapter
.get_texture_format_features(TextureFormat::Rgba16Float)
.allowed_usages
.contains(TextureUsages::STORAGE_BINDING)
{
warn!("AtmospherePlugin not loaded. GPU lacks support: TextureFormat::Rgba16Float does not support TextureUsages::STORAGE_BINDING.");
return;
}
render_app
.init_resource::<AtmosphereBindGroupLayouts>()
.init_resource::<RenderSkyBindGroupLayouts>()
.init_resource::<AtmosphereSamplers>()
.init_resource::<AtmosphereLutPipelines>()
.init_resource::<AtmosphereTransforms>()
.init_resource::<SpecializedRenderPipelines<RenderSkyBindGroupLayouts>>()
.add_systems(
Render,
(
configure_camera_depth_usages.in_set(RenderSet::ManageViews),
queue_render_sky_pipelines.in_set(RenderSet::Queue),
prepare_atmosphere_textures.in_set(RenderSet::PrepareResources),
prepare_atmosphere_transforms.in_set(RenderSet::PrepareResources),
prepare_atmosphere_bind_groups.in_set(RenderSet::PrepareBindGroups),
),
)
.add_render_graph_node::<ViewNodeRunner<AtmosphereLutsNode>>(
Core3d,
AtmosphereNode::RenderLuts,
)
.add_render_graph_edges(
Core3d,
(
// END_PRE_PASSES -> RENDER_LUTS -> MAIN_PASS
Node3d::EndPrepasses,
AtmosphereNode::RenderLuts,
Node3d::StartMainPass,
),
)
.add_render_graph_node::<ViewNodeRunner<RenderSkyNode>>(
Core3d,
AtmosphereNode::RenderSky,
)
.add_render_graph_edges(
Core3d,
(
Node3d::MainOpaquePass,
AtmosphereNode::RenderSky,
Node3d::MainTransparentPass,
),
);
}
}
/// This component describes the atmosphere of a planet, and when added to a camera
/// will enable atmospheric scattering for that camera. This is only compatible with
/// HDR cameras.
///
/// Most atmospheric particles scatter and absorb light in two main ways:
///
/// Rayleigh scattering occurs among very small particles, like individual gas
/// molecules. It's wavelength dependent, and causes colors to separate out as
/// light travels through the atmosphere. These particles *don't* absorb light.
///
/// Mie scattering occurs among slightly larger particles, like dust and sea spray.
/// These particles *do* absorb light, but Mie scattering and absorption is
/// *wavelength independent*.
///
/// Ozone acts differently from the other two, and is special-cased because
/// it's very important to the look of Earth's atmosphere. It's wavelength
/// dependent, but only *absorbs* light. Also, while the density of particles
/// participating in Rayleigh and Mie scattering falls off roughly exponentially
/// from the planet's surface, ozone only exists in a band centered at a fairly
/// high altitude.
#[derive(Clone, Component, Reflect, ShaderType)]
#[require(AtmosphereSettings)]
#[reflect(Clone, Default)]
pub struct Atmosphere {
/// Radius of the planet
///
/// units: m
pub bottom_radius: f32,
/// Radius at which we consider the atmosphere to 'end' for our
/// calculations (from center of planet)
///
/// units: m
pub top_radius: f32,
/// An approximation of the average albedo (or color, roughly) of the
/// planet's surface. This is used when calculating multiscattering.
///
/// units: N/A
pub ground_albedo: Vec3,
/// The rate of falloff of rayleigh particulate with respect to altitude:
/// optical density = exp(-rayleigh_density_exp_scale * altitude in meters).
///
/// THIS VALUE MUST BE POSITIVE
///
/// units: N/A
pub rayleigh_density_exp_scale: f32,
/// The scattering optical density of rayleigh particulate, or how
/// much light it scatters per meter
///
/// units: m^-1
pub rayleigh_scattering: Vec3,
/// The rate of falloff of mie particulate with respect to altitude:
/// optical density = exp(-mie_density_exp_scale * altitude in meters)
///
/// THIS VALUE MUST BE POSITIVE
///
/// units: N/A
pub mie_density_exp_scale: f32,
/// The scattering optical density of mie particulate, or how much light
/// it scatters per meter.
///
/// units: m^-1
pub mie_scattering: f32,
/// The absorbing optical density of mie particulate, or how much light
/// it absorbs per meter.
///
/// units: m^-1
pub mie_absorption: f32,
/// The "asymmetry" of mie scattering, or how much light tends to scatter
/// forwards, rather than backwards or to the side.
///
/// domain: (-1, 1)
/// units: N/A
pub mie_asymmetry: f32, //the "asymmetry" value of the phase function, unitless. Domain: (-1, 1)
/// The altitude at which the ozone layer is centered.
///
/// units: m
pub ozone_layer_altitude: f32,
/// The width of the ozone layer
///
/// units: m
pub ozone_layer_width: f32,
/// The optical density of ozone, or how much of each wavelength of
/// light it absorbs per meter.
///
/// units: m^-1
pub ozone_absorption: Vec3,
}
impl Atmosphere {
pub const EARTH: Atmosphere = Atmosphere {
bottom_radius: 6_360_000.0,
top_radius: 6_460_000.0,
ground_albedo: Vec3::splat(0.3),
rayleigh_density_exp_scale: 1.0 / 8_000.0,
rayleigh_scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6),
mie_density_exp_scale: 1.0 / 1_200.0,
mie_scattering: 3.996e-6,
mie_absorption: 0.444e-6,
mie_asymmetry: 0.8,
ozone_layer_altitude: 25_000.0,
ozone_layer_width: 30_000.0,
ozone_absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6),
};
pub fn with_density_multiplier(mut self, mult: f32) -> Self {
self.rayleigh_scattering *= mult;
self.mie_scattering *= mult;
self.mie_absorption *= mult;
self.ozone_absorption *= mult;
self
}
}
impl Default for Atmosphere {
fn default() -> Self {
Self::EARTH
}
}
impl ExtractComponent for Atmosphere {
type QueryData = Read<Atmosphere>;
type QueryFilter = With<Camera3d>;
type Out = Atmosphere;
fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option<Self::Out> {
Some(item.clone())
}
}
/// This component controls the resolution of the atmosphere LUTs, and
/// how many samples are used when computing them.
///
/// The transmittance LUT stores the transmittance from a point in the
/// atmosphere to the outer edge of the atmosphere in any direction,
/// parametrized by the point's radius and the cosine of the zenith angle
/// of the ray.
///
/// The multiscattering LUT stores the factor representing luminance scattered
/// towards the camera with scattering order >2, parametrized by the point's radius
/// and the cosine of the zenith angle of the sun.
///
/// The sky-view lut is essentially the actual skybox, storing the light scattered
/// towards the camera in every direction with a cubemap.
///
/// The aerial-view lut is a 3d LUT fit to the view frustum, which stores the luminance
/// scattered towards the camera at each point (RGB channels), alongside the average
/// transmittance to that point (A channel).
#[derive(Clone, Component, Reflect, ShaderType)]
#[reflect(Clone, Default)]
pub struct AtmosphereSettings {
/// The size of the transmittance LUT
pub transmittance_lut_size: UVec2,
/// The size of the multiscattering LUT
pub multiscattering_lut_size: UVec2,
/// The size of the sky-view LUT.
pub sky_view_lut_size: UVec2,
/// The size of the aerial-view LUT.
pub aerial_view_lut_size: UVec3,
/// The number of points to sample along each ray when
/// computing the transmittance LUT
pub transmittance_lut_samples: u32,
/// The number of rays to sample when computing each
/// pixel of the multiscattering LUT
pub multiscattering_lut_dirs: u32,
/// The number of points to sample when integrating along each
/// multiscattering ray
pub multiscattering_lut_samples: u32,
/// The number of points to sample along each ray when
/// computing the sky-view LUT.
pub sky_view_lut_samples: u32,
/// The number of points to sample for each slice along the z-axis
/// of the aerial-view LUT.
pub aerial_view_lut_samples: u32,
/// The maximum distance from the camera to evaluate the
/// aerial view LUT. The slices along the z-axis of the
/// texture will be distributed linearly from the camera
/// to this value.
///
/// units: m
pub aerial_view_lut_max_distance: f32,
/// A conversion factor between scene units and meters, used to
/// ensure correctness at different length scales.
pub scene_units_to_m: f32,
}
impl Default for AtmosphereSettings {
fn default() -> Self {
Self {
transmittance_lut_size: UVec2::new(256, 128),
transmittance_lut_samples: 40,
multiscattering_lut_size: UVec2::new(32, 32),
multiscattering_lut_dirs: 64,
multiscattering_lut_samples: 20,
sky_view_lut_size: UVec2::new(400, 200),
sky_view_lut_samples: 16,
aerial_view_lut_size: UVec3::new(32, 32, 32),
aerial_view_lut_samples: 10,
aerial_view_lut_max_distance: 3.2e4,
scene_units_to_m: 1.0,
}
}
}
impl ExtractComponent for AtmosphereSettings {
type QueryData = Read<AtmosphereSettings>;
type QueryFilter = (With<Camera3d>, With<Atmosphere>);
type Out = AtmosphereSettings;
fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option<Self::Out> {
Some(item.clone())
}
}
fn configure_camera_depth_usages(
mut cameras: Query<&mut Camera3d, (Changed<Camera3d>, With<Atmosphere>)>,
) {
for mut camera in &mut cameras {
camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();
}
}

View File

@@ -0,0 +1,139 @@
#import bevy_pbr::{
mesh_view_types::{Lights, DirectionalLight},
atmosphere::{
types::{Atmosphere, AtmosphereSettings},
bindings::{atmosphere, settings},
functions::{
multiscattering_lut_uv_to_r_mu, sample_transmittance_lut,
get_local_r, get_local_up, sample_atmosphere, FRAC_4_PI,
max_atmosphere_distance, rayleigh, henyey_greenstein,
zenith_azimuth_to_ray_dir,
},
bruneton_functions::{
distance_to_top_atmosphere_boundary, distance_to_bottom_atmosphere_boundary, ray_intersects_ground
}
}
}
#import bevy_render::maths::{PI,PI_2}
const PHI_2: vec2<f32> = vec2(1.3247179572447460259609088, 1.7548776662466927600495087);
@group(0) @binding(13) var multiscattering_lut_out: texture_storage_2d<rgba16float, write>;
fn s2_sequence(n: u32) -> vec2<f32> {
return fract(0.5 + f32(n) * PHI_2);
}
// Lambert equal-area projection.
fn uv_to_sphere(uv: vec2<f32>) -> vec3<f32> {
let phi = PI_2 * uv.y;
let sin_lambda = 2 * uv.x - 1;
let cos_lambda = sqrt(1 - sin_lambda * sin_lambda);
return vec3(cos_lambda * cos(phi), cos_lambda * sin(phi), sin_lambda);
}
// Shared memory arrays for workgroup communication
var<workgroup> multi_scat_shared_mem: array<vec3<f32>, 64>;
var<workgroup> l_shared_mem: array<vec3<f32>, 64>;
@compute
@workgroup_size(1, 1, 64)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
var uv = (vec2<f32>(global_id.xy) + 0.5) / vec2<f32>(settings.multiscattering_lut_size);
let r_mu = multiscattering_lut_uv_to_r_mu(uv);
let light_dir = normalize(vec3(0.0, r_mu.y, -1.0));
let ray_dir = uv_to_sphere(s2_sequence(global_id.z));
let ms_sample = sample_multiscattering_dir(r_mu.x, ray_dir, light_dir);
// Calculate the contribution for this sample
let sphere_solid_angle = 4.0 * PI;
let sample_weight = sphere_solid_angle / 64.0;
multi_scat_shared_mem[global_id.z] = ms_sample.f_ms * sample_weight;
l_shared_mem[global_id.z] = ms_sample.l_2 * sample_weight;
workgroupBarrier();
// Parallel reduction bitshift to the right to divide by 2 each step
for (var step = 32u; step > 0u; step >>= 1u) {
if global_id.z < step {
multi_scat_shared_mem[global_id.z] += multi_scat_shared_mem[global_id.z + step];
l_shared_mem[global_id.z] += l_shared_mem[global_id.z + step];
}
workgroupBarrier();
}
if global_id.z > 0u {
return;
}
// Apply isotropic phase function
let f_ms = multi_scat_shared_mem[0] * FRAC_4_PI;
let l_2 = l_shared_mem[0] * FRAC_4_PI;
// Equation 10 from the paper: Geometric series for infinite scattering
let psi_ms = l_2 / (1.0 - f_ms);
textureStore(multiscattering_lut_out, global_id.xy, vec4<f32>(psi_ms, 1.0));
}
struct MultiscatteringSample {
l_2: vec3<f32>,
f_ms: vec3<f32>,
};
fn sample_multiscattering_dir(r: f32, ray_dir: vec3<f32>, light_dir: vec3<f32>) -> MultiscatteringSample {
// get the cosine of the zenith angle of the view direction with respect to the light direction
let mu_view = ray_dir.y;
let t_max = max_atmosphere_distance(r, mu_view);
let dt = t_max / f32(settings.multiscattering_lut_samples);
var optical_depth = vec3<f32>(0.0);
var l_2 = vec3(0.0);
var f_ms = vec3(0.0);
var throughput = vec3(1.0);
for (var i: u32 = 0u; i < settings.multiscattering_lut_samples; i++) {
let t_i = dt * (f32(i) + 0.5);
let local_r = get_local_r(r, mu_view, t_i);
let local_up = get_local_up(r, t_i, ray_dir);
let local_atmosphere = sample_atmosphere(local_r);
let sample_optical_depth = local_atmosphere.extinction * dt;
let sample_transmittance = exp(-sample_optical_depth);
optical_depth += sample_optical_depth;
let mu_light = dot(light_dir, local_up);
let scattering_no_phase = local_atmosphere.rayleigh_scattering + local_atmosphere.mie_scattering;
let ms = scattering_no_phase;
let ms_int = (ms - ms * sample_transmittance) / local_atmosphere.extinction;
f_ms += throughput * ms_int;
let transmittance_to_light = sample_transmittance_lut(local_r, mu_light);
let shadow_factor = transmittance_to_light * f32(!ray_intersects_ground(local_r, mu_light));
let s = scattering_no_phase * shadow_factor * FRAC_4_PI;
let s_int = (s - s * sample_transmittance) / local_atmosphere.extinction;
l_2 += throughput * s_int;
throughput *= sample_transmittance;
if all(throughput < vec3(0.001)) {
break;
}
}
//include reflected luminance from planet ground
if ray_intersects_ground(r, mu_view) {
let transmittance_to_ground = exp(-optical_depth);
let local_up = get_local_up(r, t_max, ray_dir);
let mu_light = dot(light_dir, local_up);
let transmittance_to_light = sample_transmittance_lut(0.0, mu_light);
let ground_luminance = transmittance_to_light * transmittance_to_ground * max(mu_light, 0.0) * atmosphere.ground_albedo;
l_2 += ground_luminance;
}
return MultiscatteringSample(l_2, f_ms);
}

221
vendor/bevy_pbr/src/atmosphere/node.rs vendored Normal file
View File

@@ -0,0 +1,221 @@
use bevy_ecs::{query::QueryItem, system::lifetimeless::Read, world::World};
use bevy_math::{UVec2, Vec3Swizzles};
use bevy_render::{
extract_component::DynamicUniformIndex,
render_graph::{NodeRunError, RenderGraphContext, RenderLabel, ViewNode},
render_resource::{ComputePass, ComputePassDescriptor, PipelineCache, RenderPassDescriptor},
renderer::RenderContext,
view::{ViewTarget, ViewUniformOffset},
};
use crate::ViewLightsUniformOffset;
use super::{
resources::{
AtmosphereBindGroups, AtmosphereLutPipelines, AtmosphereTransformsOffset,
RenderSkyPipelineId,
},
Atmosphere, AtmosphereSettings,
};
#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, RenderLabel)]
pub enum AtmosphereNode {
RenderLuts,
RenderSky,
}
#[derive(Default)]
pub(super) struct AtmosphereLutsNode {}
impl ViewNode for AtmosphereLutsNode {
type ViewQuery = (
Read<AtmosphereSettings>,
Read<AtmosphereBindGroups>,
Read<DynamicUniformIndex<Atmosphere>>,
Read<DynamicUniformIndex<AtmosphereSettings>>,
Read<AtmosphereTransformsOffset>,
Read<ViewUniformOffset>,
Read<ViewLightsUniformOffset>,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(
settings,
bind_groups,
atmosphere_uniforms_offset,
settings_uniforms_offset,
atmosphere_transforms_offset,
view_uniforms_offset,
lights_uniforms_offset,
): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let pipelines = world.resource::<AtmosphereLutPipelines>();
let pipeline_cache = world.resource::<PipelineCache>();
let (
Some(transmittance_lut_pipeline),
Some(multiscattering_lut_pipeline),
Some(sky_view_lut_pipeline),
Some(aerial_view_lut_pipeline),
) = (
pipeline_cache.get_compute_pipeline(pipelines.transmittance_lut),
pipeline_cache.get_compute_pipeline(pipelines.multiscattering_lut),
pipeline_cache.get_compute_pipeline(pipelines.sky_view_lut),
pipeline_cache.get_compute_pipeline(pipelines.aerial_view_lut),
)
else {
return Ok(());
};
let command_encoder = render_context.command_encoder();
let mut luts_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor {
label: Some("atmosphere_luts_pass"),
timestamp_writes: None,
});
fn dispatch_2d(compute_pass: &mut ComputePass, size: UVec2) {
const WORKGROUP_SIZE: u32 = 16;
let workgroups_x = size.x.div_ceil(WORKGROUP_SIZE);
let workgroups_y = size.y.div_ceil(WORKGROUP_SIZE);
compute_pass.dispatch_workgroups(workgroups_x, workgroups_y, 1);
}
// Transmittance LUT
luts_pass.set_pipeline(transmittance_lut_pipeline);
luts_pass.set_bind_group(
0,
&bind_groups.transmittance_lut,
&[
atmosphere_uniforms_offset.index(),
settings_uniforms_offset.index(),
],
);
dispatch_2d(&mut luts_pass, settings.transmittance_lut_size);
// Multiscattering LUT
luts_pass.set_pipeline(multiscattering_lut_pipeline);
luts_pass.set_bind_group(
0,
&bind_groups.multiscattering_lut,
&[
atmosphere_uniforms_offset.index(),
settings_uniforms_offset.index(),
],
);
luts_pass.dispatch_workgroups(
settings.multiscattering_lut_size.x,
settings.multiscattering_lut_size.y,
1,
);
// Sky View LUT
luts_pass.set_pipeline(sky_view_lut_pipeline);
luts_pass.set_bind_group(
0,
&bind_groups.sky_view_lut,
&[
atmosphere_uniforms_offset.index(),
settings_uniforms_offset.index(),
atmosphere_transforms_offset.index(),
view_uniforms_offset.offset,
lights_uniforms_offset.offset,
],
);
dispatch_2d(&mut luts_pass, settings.sky_view_lut_size);
// Aerial View LUT
luts_pass.set_pipeline(aerial_view_lut_pipeline);
luts_pass.set_bind_group(
0,
&bind_groups.aerial_view_lut,
&[
atmosphere_uniforms_offset.index(),
settings_uniforms_offset.index(),
view_uniforms_offset.offset,
lights_uniforms_offset.offset,
],
);
dispatch_2d(&mut luts_pass, settings.aerial_view_lut_size.xy());
Ok(())
}
}
#[derive(Default)]
pub(super) struct RenderSkyNode;
impl ViewNode for RenderSkyNode {
type ViewQuery = (
Read<AtmosphereBindGroups>,
Read<ViewTarget>,
Read<DynamicUniformIndex<Atmosphere>>,
Read<DynamicUniformIndex<AtmosphereSettings>>,
Read<AtmosphereTransformsOffset>,
Read<ViewUniformOffset>,
Read<ViewLightsUniformOffset>,
Read<RenderSkyPipelineId>,
);
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(
atmosphere_bind_groups,
view_target,
atmosphere_uniforms_offset,
settings_uniforms_offset,
atmosphere_transforms_offset,
view_uniforms_offset,
lights_uniforms_offset,
render_sky_pipeline_id,
): QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>();
let Some(render_sky_pipeline) =
pipeline_cache.get_render_pipeline(render_sky_pipeline_id.0)
else {
return Ok(());
}; //TODO: warning
let mut render_sky_pass =
render_context
.command_encoder()
.begin_render_pass(&RenderPassDescriptor {
label: Some("render_sky_pass"),
color_attachments: &[Some(view_target.get_color_attachment())],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
render_sky_pass.set_pipeline(render_sky_pipeline);
render_sky_pass.set_bind_group(
0,
&atmosphere_bind_groups.render_sky,
&[
atmosphere_uniforms_offset.index(),
settings_uniforms_offset.index(),
atmosphere_transforms_offset.index(),
view_uniforms_offset.offset,
lights_uniforms_offset.offset,
],
);
render_sky_pass.draw(0..3, 0..1);
Ok(())
}
}

View File

@@ -0,0 +1,58 @@
#import bevy_pbr::atmosphere::{
types::{Atmosphere, AtmosphereSettings},
bindings::{atmosphere, view, atmosphere_transforms},
functions::{
sample_transmittance_lut, sample_transmittance_lut_segment,
sample_sky_view_lut, direction_world_to_atmosphere,
uv_to_ray_direction, uv_to_ndc, sample_aerial_view_lut,
view_radius, sample_sun_radiance, ndc_to_camera_dist
},
};
#import bevy_render::view::View;
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#ifdef MULTISAMPLED
@group(0) @binding(13) var depth_texture: texture_depth_multisampled_2d;
#else
@group(0) @binding(13) var depth_texture: texture_depth_2d;
#endif
struct RenderSkyOutput {
@location(0) inscattering: vec4<f32>,
#ifdef DUAL_SOURCE_BLENDING
@location(0) @second_blend_source transmittance: vec4<f32>,
#endif
}
@fragment
fn main(in: FullscreenVertexOutput) -> RenderSkyOutput {
let depth = textureLoad(depth_texture, vec2<i32>(in.position.xy), 0);
let ray_dir_ws = uv_to_ray_direction(in.uv);
let r = view_radius();
let mu = ray_dir_ws.y;
var transmittance: vec3<f32>;
var inscattering: vec3<f32>;
let sun_radiance = sample_sun_radiance(ray_dir_ws.xyz);
if depth == 0.0 {
let ray_dir_as = direction_world_to_atmosphere(ray_dir_ws.xyz);
transmittance = sample_transmittance_lut(r, mu);
inscattering += sample_sky_view_lut(r, ray_dir_as);
inscattering += sun_radiance * transmittance * view.exposure;
} else {
let t = ndc_to_camera_dist(vec3(uv_to_ndc(in.uv), depth));
inscattering = sample_aerial_view_lut(in.uv, t);
transmittance = sample_transmittance_lut_segment(r, mu, t);
}
#ifdef DUAL_SOURCE_BLENDING
return RenderSkyOutput(vec4(inscattering, 0.0), vec4(transmittance, 1.0));
#else
let mean_transmittance = (transmittance.r + transmittance.g + transmittance.b) / 3.0;
return RenderSkyOutput(vec4(inscattering, mean_transmittance));
#endif
}

View File

@@ -0,0 +1,732 @@
use bevy_core_pipeline::{
core_3d::Camera3d, fullscreen_vertex_shader::fullscreen_shader_vertex_state,
};
use bevy_ecs::{
component::Component,
entity::Entity,
query::With,
resource::Resource,
system::{Commands, Query, Res, ResMut},
world::{FromWorld, World},
};
use bevy_math::{Mat4, Vec3};
use bevy_render::{
camera::Camera,
extract_component::ComponentUniforms,
render_resource::{binding_types::*, *},
renderer::{RenderDevice, RenderQueue},
texture::{CachedTexture, TextureCache},
view::{ExtractedView, Msaa, ViewDepthTexture, ViewUniform, ViewUniforms},
};
use crate::{GpuLights, LightMeta};
use super::{shaders, Atmosphere, AtmosphereSettings};
#[derive(Resource)]
pub(crate) struct AtmosphereBindGroupLayouts {
pub transmittance_lut: BindGroupLayout,
pub multiscattering_lut: BindGroupLayout,
pub sky_view_lut: BindGroupLayout,
pub aerial_view_lut: BindGroupLayout,
}
#[derive(Resource)]
pub(crate) struct RenderSkyBindGroupLayouts {
pub render_sky: BindGroupLayout,
pub render_sky_msaa: BindGroupLayout,
}
impl FromWorld for AtmosphereBindGroupLayouts {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let transmittance_lut = render_device.create_bind_group_layout(
"transmittance_lut_bind_group_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::COMPUTE,
(
(0, uniform_buffer::<Atmosphere>(true)),
(1, uniform_buffer::<AtmosphereSettings>(true)),
(
// transmittance lut storage texture
13,
texture_storage_2d(
TextureFormat::Rgba16Float,
StorageTextureAccess::WriteOnly,
),
),
),
),
);
let multiscattering_lut = render_device.create_bind_group_layout(
"multiscattering_lut_bind_group_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::COMPUTE,
(
(0, uniform_buffer::<Atmosphere>(true)),
(1, uniform_buffer::<AtmosphereSettings>(true)),
(5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler
(6, sampler(SamplerBindingType::Filtering)),
(
//multiscattering lut storage texture
13,
texture_storage_2d(
TextureFormat::Rgba16Float,
StorageTextureAccess::WriteOnly,
),
),
),
),
);
let sky_view_lut = render_device.create_bind_group_layout(
"sky_view_lut_bind_group_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::COMPUTE,
(
(0, uniform_buffer::<Atmosphere>(true)),
(1, uniform_buffer::<AtmosphereSettings>(true)),
(2, uniform_buffer::<AtmosphereTransform>(true)),
(3, uniform_buffer::<ViewUniform>(true)),
(4, uniform_buffer::<GpuLights>(true)),
(5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler
(6, sampler(SamplerBindingType::Filtering)),
(7, texture_2d(TextureSampleType::Float { filterable: true })), //multiscattering lut and sampler
(8, sampler(SamplerBindingType::Filtering)),
(
13,
texture_storage_2d(
TextureFormat::Rgba16Float,
StorageTextureAccess::WriteOnly,
),
),
),
),
);
let aerial_view_lut = render_device.create_bind_group_layout(
"aerial_view_lut_bind_group_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::COMPUTE,
(
(0, uniform_buffer::<Atmosphere>(true)),
(1, uniform_buffer::<AtmosphereSettings>(true)),
(3, uniform_buffer::<ViewUniform>(true)),
(4, uniform_buffer::<GpuLights>(true)),
(5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler
(6, sampler(SamplerBindingType::Filtering)),
(7, texture_2d(TextureSampleType::Float { filterable: true })), //multiscattering lut and sampler
(8, sampler(SamplerBindingType::Filtering)),
(
//Aerial view lut storage texture
13,
texture_storage_3d(
TextureFormat::Rgba16Float,
StorageTextureAccess::WriteOnly,
),
),
),
),
);
Self {
transmittance_lut,
multiscattering_lut,
sky_view_lut,
aerial_view_lut,
}
}
}
impl FromWorld for RenderSkyBindGroupLayouts {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let render_sky = render_device.create_bind_group_layout(
"render_sky_bind_group_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::FRAGMENT,
(
(0, uniform_buffer::<Atmosphere>(true)),
(1, uniform_buffer::<AtmosphereSettings>(true)),
(2, uniform_buffer::<AtmosphereTransform>(true)),
(3, uniform_buffer::<ViewUniform>(true)),
(4, uniform_buffer::<GpuLights>(true)),
(5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler
(6, sampler(SamplerBindingType::Filtering)),
(9, texture_2d(TextureSampleType::Float { filterable: true })), //sky view lut and sampler
(10, sampler(SamplerBindingType::Filtering)),
(
// aerial view lut and sampler
11,
texture_3d(TextureSampleType::Float { filterable: true }),
),
(12, sampler(SamplerBindingType::Filtering)),
(
//view depth texture
13,
texture_2d(TextureSampleType::Depth),
),
),
),
);
let render_sky_msaa = render_device.create_bind_group_layout(
"render_sky_msaa_bind_group_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::FRAGMENT,
(
(0, uniform_buffer::<Atmosphere>(true)),
(1, uniform_buffer::<AtmosphereSettings>(true)),
(2, uniform_buffer::<AtmosphereTransform>(true)),
(3, uniform_buffer::<ViewUniform>(true)),
(4, uniform_buffer::<GpuLights>(true)),
(5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler
(6, sampler(SamplerBindingType::Filtering)),
(9, texture_2d(TextureSampleType::Float { filterable: true })), //sky view lut and sampler
(10, sampler(SamplerBindingType::Filtering)),
(
// aerial view lut and sampler
11,
texture_3d(TextureSampleType::Float { filterable: true }),
),
(12, sampler(SamplerBindingType::Filtering)),
(
//view depth texture
13,
texture_2d_multisampled(TextureSampleType::Depth),
),
),
),
);
Self {
render_sky,
render_sky_msaa,
}
}
}
#[derive(Resource)]
pub struct AtmosphereSamplers {
pub transmittance_lut: Sampler,
pub multiscattering_lut: Sampler,
pub sky_view_lut: Sampler,
pub aerial_view_lut: Sampler,
}
impl FromWorld for AtmosphereSamplers {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let base_sampler = SamplerDescriptor {
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: FilterMode::Nearest,
..Default::default()
};
let transmittance_lut = render_device.create_sampler(&SamplerDescriptor {
label: Some("transmittance_lut_sampler"),
..base_sampler
});
let multiscattering_lut = render_device.create_sampler(&SamplerDescriptor {
label: Some("multiscattering_lut_sampler"),
..base_sampler
});
let sky_view_lut = render_device.create_sampler(&SamplerDescriptor {
label: Some("sky_view_lut_sampler"),
address_mode_u: AddressMode::Repeat,
..base_sampler
});
let aerial_view_lut = render_device.create_sampler(&SamplerDescriptor {
label: Some("aerial_view_lut_sampler"),
..base_sampler
});
Self {
transmittance_lut,
multiscattering_lut,
sky_view_lut,
aerial_view_lut,
}
}
}
#[derive(Resource)]
pub(crate) struct AtmosphereLutPipelines {
pub transmittance_lut: CachedComputePipelineId,
pub multiscattering_lut: CachedComputePipelineId,
pub sky_view_lut: CachedComputePipelineId,
pub aerial_view_lut: CachedComputePipelineId,
}
impl FromWorld for AtmosphereLutPipelines {
fn from_world(world: &mut World) -> Self {
let pipeline_cache = world.resource::<PipelineCache>();
let layouts = world.resource::<AtmosphereBindGroupLayouts>();
let transmittance_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
label: Some("transmittance_lut_pipeline".into()),
layout: vec![layouts.transmittance_lut.clone()],
push_constant_ranges: vec![],
shader: shaders::TRANSMITTANCE_LUT,
shader_defs: vec![],
entry_point: "main".into(),
zero_initialize_workgroup_memory: false,
});
let multiscattering_lut =
pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
label: Some("multi_scattering_lut_pipeline".into()),
layout: vec![layouts.multiscattering_lut.clone()],
push_constant_ranges: vec![],
shader: shaders::MULTISCATTERING_LUT,
shader_defs: vec![],
entry_point: "main".into(),
zero_initialize_workgroup_memory: false,
});
let sky_view_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
label: Some("sky_view_lut_pipeline".into()),
layout: vec![layouts.sky_view_lut.clone()],
push_constant_ranges: vec![],
shader: shaders::SKY_VIEW_LUT,
shader_defs: vec![],
entry_point: "main".into(),
zero_initialize_workgroup_memory: false,
});
let aerial_view_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
label: Some("aerial_view_lut_pipeline".into()),
layout: vec![layouts.aerial_view_lut.clone()],
push_constant_ranges: vec![],
shader: shaders::AERIAL_VIEW_LUT,
shader_defs: vec![],
entry_point: "main".into(),
zero_initialize_workgroup_memory: false,
});
Self {
transmittance_lut,
multiscattering_lut,
sky_view_lut,
aerial_view_lut,
}
}
}
#[derive(Component)]
pub(crate) struct RenderSkyPipelineId(pub CachedRenderPipelineId);
#[derive(Copy, Clone, Hash, PartialEq, Eq)]
pub(crate) struct RenderSkyPipelineKey {
pub msaa_samples: u32,
pub hdr: bool,
pub dual_source_blending: bool,
}
impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts {
type Key = RenderSkyPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let mut shader_defs = Vec::new();
if key.msaa_samples > 1 {
shader_defs.push("MULTISAMPLED".into());
}
if key.hdr {
shader_defs.push("TONEMAP_IN_SHADER".into());
}
if key.dual_source_blending {
shader_defs.push("DUAL_SOURCE_BLENDING".into());
}
let dst_factor = if key.dual_source_blending {
BlendFactor::Src1
} else {
BlendFactor::SrcAlpha
};
RenderPipelineDescriptor {
label: Some(format!("render_sky_pipeline_{}", key.msaa_samples).into()),
layout: vec![if key.msaa_samples == 1 {
self.render_sky.clone()
} else {
self.render_sky_msaa.clone()
}],
push_constant_ranges: vec![],
vertex: fullscreen_shader_vertex_state(),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState {
count: key.msaa_samples,
mask: !0,
alpha_to_coverage_enabled: false,
},
zero_initialize_workgroup_memory: false,
fragment: Some(FragmentState {
shader: shaders::RENDER_SKY.clone(),
shader_defs,
entry_point: "main".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::Rgba16Float,
blend: Some(BlendState {
color: BlendComponent {
src_factor: BlendFactor::One,
dst_factor,
operation: BlendOperation::Add,
},
alpha: BlendComponent {
src_factor: BlendFactor::Zero,
dst_factor: BlendFactor::One,
operation: BlendOperation::Add,
},
}),
write_mask: ColorWrites::ALL,
})],
}),
}
}
}
pub(super) fn queue_render_sky_pipelines(
views: Query<(Entity, &Camera, &Msaa), With<Atmosphere>>,
pipeline_cache: Res<PipelineCache>,
layouts: Res<RenderSkyBindGroupLayouts>,
mut specializer: ResMut<SpecializedRenderPipelines<RenderSkyBindGroupLayouts>>,
render_device: Res<RenderDevice>,
mut commands: Commands,
) {
for (entity, camera, msaa) in &views {
let id = specializer.specialize(
&pipeline_cache,
&layouts,
RenderSkyPipelineKey {
msaa_samples: msaa.samples(),
hdr: camera.hdr,
dual_source_blending: render_device
.features()
.contains(WgpuFeatures::DUAL_SOURCE_BLENDING),
},
);
commands.entity(entity).insert(RenderSkyPipelineId(id));
}
}
#[derive(Component)]
pub struct AtmosphereTextures {
pub transmittance_lut: CachedTexture,
pub multiscattering_lut: CachedTexture,
pub sky_view_lut: CachedTexture,
pub aerial_view_lut: CachedTexture,
}
pub(super) fn prepare_atmosphere_textures(
views: Query<(Entity, &AtmosphereSettings), With<Atmosphere>>,
render_device: Res<RenderDevice>,
mut texture_cache: ResMut<TextureCache>,
mut commands: Commands,
) {
for (entity, lut_settings) in &views {
let transmittance_lut = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("transmittance_lut"),
size: Extent3d {
width: lut_settings.transmittance_lut_size.x,
height: lut_settings.transmittance_lut_size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba16Float,
usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
);
let multiscattering_lut = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("multiscattering_lut"),
size: Extent3d {
width: lut_settings.multiscattering_lut_size.x,
height: lut_settings.multiscattering_lut_size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba16Float,
usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
);
let sky_view_lut = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("sky_view_lut"),
size: Extent3d {
width: lut_settings.sky_view_lut_size.x,
height: lut_settings.sky_view_lut_size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba16Float,
usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
);
let aerial_view_lut = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("aerial_view_lut"),
size: Extent3d {
width: lut_settings.aerial_view_lut_size.x,
height: lut_settings.aerial_view_lut_size.y,
depth_or_array_layers: lut_settings.aerial_view_lut_size.z,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D3,
format: TextureFormat::Rgba16Float,
usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
);
commands.entity(entity).insert({
AtmosphereTextures {
transmittance_lut,
multiscattering_lut,
sky_view_lut,
aerial_view_lut,
}
});
}
}
#[derive(Resource, Default)]
pub struct AtmosphereTransforms {
uniforms: DynamicUniformBuffer<AtmosphereTransform>,
}
impl AtmosphereTransforms {
#[inline]
pub fn uniforms(&self) -> &DynamicUniformBuffer<AtmosphereTransform> {
&self.uniforms
}
}
#[derive(ShaderType)]
pub struct AtmosphereTransform {
world_from_atmosphere: Mat4,
atmosphere_from_world: Mat4,
}
#[derive(Component)]
pub struct AtmosphereTransformsOffset {
index: u32,
}
impl AtmosphereTransformsOffset {
#[inline]
pub fn index(&self) -> u32 {
self.index
}
}
pub(super) fn prepare_atmosphere_transforms(
views: Query<(Entity, &ExtractedView), (With<Atmosphere>, With<Camera3d>)>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut atmo_uniforms: ResMut<AtmosphereTransforms>,
mut commands: Commands,
) {
let atmo_count = views.iter().len();
let Some(mut writer) =
atmo_uniforms
.uniforms
.get_writer(atmo_count, &render_device, &render_queue)
else {
return;
};
for (entity, view) in &views {
let world_from_view = view.world_from_view.compute_matrix();
let camera_z = world_from_view.z_axis.truncate();
let camera_y = world_from_view.y_axis.truncate();
let atmo_z = camera_z
.with_y(0.0)
.try_normalize()
.unwrap_or_else(|| camera_y.with_y(0.0).normalize());
let atmo_y = Vec3::Y;
let atmo_x = atmo_y.cross(atmo_z).normalize();
let world_from_atmosphere = Mat4::from_cols(
atmo_x.extend(0.0),
atmo_y.extend(0.0),
atmo_z.extend(0.0),
world_from_view.w_axis,
);
let atmosphere_from_world = world_from_atmosphere.inverse();
commands.entity(entity).insert(AtmosphereTransformsOffset {
index: writer.write(&AtmosphereTransform {
world_from_atmosphere,
atmosphere_from_world,
}),
});
}
}
#[derive(Component)]
pub(crate) struct AtmosphereBindGroups {
pub transmittance_lut: BindGroup,
pub multiscattering_lut: BindGroup,
pub sky_view_lut: BindGroup,
pub aerial_view_lut: BindGroup,
pub render_sky: BindGroup,
}
pub(super) fn prepare_atmosphere_bind_groups(
views: Query<
(Entity, &AtmosphereTextures, &ViewDepthTexture, &Msaa),
(With<Camera3d>, With<Atmosphere>),
>,
render_device: Res<RenderDevice>,
layouts: Res<AtmosphereBindGroupLayouts>,
render_sky_layouts: Res<RenderSkyBindGroupLayouts>,
samplers: Res<AtmosphereSamplers>,
view_uniforms: Res<ViewUniforms>,
lights_uniforms: Res<LightMeta>,
atmosphere_transforms: Res<AtmosphereTransforms>,
atmosphere_uniforms: Res<ComponentUniforms<Atmosphere>>,
settings_uniforms: Res<ComponentUniforms<AtmosphereSettings>>,
mut commands: Commands,
) {
if views.iter().len() == 0 {
return;
}
let atmosphere_binding = atmosphere_uniforms
.binding()
.expect("Failed to prepare atmosphere bind groups. Atmosphere uniform buffer missing");
let transforms_binding = atmosphere_transforms
.uniforms()
.binding()
.expect("Failed to prepare atmosphere bind groups. Atmosphere transforms buffer missing");
let settings_binding = settings_uniforms.binding().expect(
"Failed to prepare atmosphere bind groups. AtmosphereSettings uniform buffer missing",
);
let view_binding = view_uniforms
.uniforms
.binding()
.expect("Failed to prepare atmosphere bind groups. View uniform buffer missing");
let lights_binding = lights_uniforms
.view_gpu_lights
.binding()
.expect("Failed to prepare atmosphere bind groups. Lights uniform buffer missing");
for (entity, textures, view_depth_texture, msaa) in &views {
let transmittance_lut = render_device.create_bind_group(
"transmittance_lut_bind_group",
&layouts.transmittance_lut,
&BindGroupEntries::with_indices((
(0, atmosphere_binding.clone()),
(1, settings_binding.clone()),
(13, &textures.transmittance_lut.default_view),
)),
);
let multiscattering_lut = render_device.create_bind_group(
"multiscattering_lut_bind_group",
&layouts.multiscattering_lut,
&BindGroupEntries::with_indices((
(0, atmosphere_binding.clone()),
(1, settings_binding.clone()),
(5, &textures.transmittance_lut.default_view),
(6, &samplers.transmittance_lut),
(13, &textures.multiscattering_lut.default_view),
)),
);
let sky_view_lut = render_device.create_bind_group(
"sky_view_lut_bind_group",
&layouts.sky_view_lut,
&BindGroupEntries::with_indices((
(0, atmosphere_binding.clone()),
(1, settings_binding.clone()),
(2, transforms_binding.clone()),
(3, view_binding.clone()),
(4, lights_binding.clone()),
(5, &textures.transmittance_lut.default_view),
(6, &samplers.transmittance_lut),
(7, &textures.multiscattering_lut.default_view),
(8, &samplers.multiscattering_lut),
(13, &textures.sky_view_lut.default_view),
)),
);
let aerial_view_lut = render_device.create_bind_group(
"sky_view_lut_bind_group",
&layouts.aerial_view_lut,
&BindGroupEntries::with_indices((
(0, atmosphere_binding.clone()),
(1, settings_binding.clone()),
(3, view_binding.clone()),
(4, lights_binding.clone()),
(5, &textures.transmittance_lut.default_view),
(6, &samplers.transmittance_lut),
(7, &textures.multiscattering_lut.default_view),
(8, &samplers.multiscattering_lut),
(13, &textures.aerial_view_lut.default_view),
)),
);
let render_sky = render_device.create_bind_group(
"render_sky_bind_group",
if *msaa == Msaa::Off {
&render_sky_layouts.render_sky
} else {
&render_sky_layouts.render_sky_msaa
},
&BindGroupEntries::with_indices((
(0, atmosphere_binding.clone()),
(1, settings_binding.clone()),
(2, transforms_binding.clone()),
(3, view_binding.clone()),
(4, lights_binding.clone()),
(5, &textures.transmittance_lut.default_view),
(6, &samplers.transmittance_lut),
(9, &textures.sky_view_lut.default_view),
(10, &samplers.sky_view_lut),
(11, &textures.aerial_view_lut.default_view),
(12, &samplers.aerial_view_lut),
(13, view_depth_texture.view()),
)),
);
commands.entity(entity).insert(AtmosphereBindGroups {
transmittance_lut,
multiscattering_lut,
sky_view_lut,
aerial_view_lut,
render_sky,
});
}
}

View File

@@ -0,0 +1,72 @@
#import bevy_pbr::{
mesh_view_types::Lights,
atmosphere::{
types::{Atmosphere, AtmosphereSettings},
bindings::{atmosphere, view, settings},
functions::{
sample_atmosphere, get_local_up, AtmosphereSample,
sample_local_inscattering, get_local_r, view_radius,
max_atmosphere_distance, direction_atmosphere_to_world,
sky_view_lut_uv_to_zenith_azimuth, zenith_azimuth_to_ray_dir,
MIDPOINT_RATIO
},
}
}
#import bevy_render::{
view::View,
maths::HALF_PI,
}
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
@group(0) @binding(13) var sky_view_lut_out: texture_storage_2d<rgba16float, write>;
@compute
@workgroup_size(16, 16, 1)
fn main(@builtin(global_invocation_id) idx: vec3<u32>) {
let uv = vec2<f32>(idx.xy) / vec2<f32>(settings.sky_view_lut_size);
let r = view_radius();
var zenith_azimuth = sky_view_lut_uv_to_zenith_azimuth(r, uv);
let ray_dir_as = zenith_azimuth_to_ray_dir(zenith_azimuth.x, zenith_azimuth.y);
let ray_dir_ws = direction_atmosphere_to_world(ray_dir_as);
let mu = ray_dir_ws.y;
let t_max = max_atmosphere_distance(r, mu);
let sample_count = mix(1.0, f32(settings.sky_view_lut_samples), clamp(t_max * 0.01, 0.0, 1.0));
var total_inscattering = vec3(0.0);
var throughput = vec3(1.0);
var prev_t = 0.0;
for (var s = 0.0; s < sample_count; s += 1.0) {
let t_i = t_max * (s + MIDPOINT_RATIO) / sample_count;
let dt_i = (t_i - prev_t);
prev_t = t_i;
let local_r = get_local_r(r, mu, t_i);
let local_up = get_local_up(r, t_i, ray_dir_ws);
let local_atmosphere = sample_atmosphere(local_r);
let sample_optical_depth = local_atmosphere.extinction * dt_i;
let sample_transmittance = exp(-sample_optical_depth);
let inscattering = sample_local_inscattering(
local_atmosphere,
ray_dir_ws,
local_r,
local_up
);
// Analytical integration of the single scattering term in the radiance transfer equation
let s_int = (inscattering - inscattering * sample_transmittance) / local_atmosphere.extinction;
total_inscattering += throughput * s_int;
throughput *= sample_transmittance;
if all(throughput < vec3(0.001)) {
break;
}
}
textureStore(sky_view_lut_out, idx.xy, vec4(total_inscattering, 1.0));
}

View File

@@ -0,0 +1,48 @@
#import bevy_pbr::atmosphere::{
types::{Atmosphere, AtmosphereSettings},
bindings::{settings, atmosphere},
functions::{AtmosphereSample, sample_atmosphere, get_local_r, max_atmosphere_distance, MIDPOINT_RATIO},
bruneton_functions::{transmittance_lut_uv_to_r_mu, distance_to_bottom_atmosphere_boundary, distance_to_top_atmosphere_boundary},
}
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
@group(0) @binding(13) var transmittance_lut_out: texture_storage_2d<rgba16float, write>;
@compute
@workgroup_size(16, 16, 1)
fn main(@builtin(global_invocation_id) idx: vec3<u32>) {
let uv: vec2<f32> = (vec2<f32>(idx.xy) + 0.5) / vec2<f32>(settings.transmittance_lut_size);
// map UV coordinates to view height (r) and zenith cos angle (mu)
let r_mu = transmittance_lut_uv_to_r_mu(uv);
// compute the optical depth from view height r to the top atmosphere boundary
let optical_depth = ray_optical_depth(r_mu.x, r_mu.y, settings.transmittance_lut_samples);
let transmittance = exp(-optical_depth);
textureStore(transmittance_lut_out, idx.xy, vec4(transmittance, 1.0));
}
/// Compute the optical depth of the atmosphere from the ground to the top atmosphere boundary
/// at a given view height (r) and zenith cos angle (mu)
fn ray_optical_depth(r: f32, mu: f32, sample_count: u32) -> vec3<f32> {
let t_max = max_atmosphere_distance(r, mu);
var optical_depth = vec3<f32>(0.0f);
var prev_t = 0.0f;
for (var i = 0u; i < sample_count; i++) {
let t_i = t_max * (f32(i) + MIDPOINT_RATIO) / f32(sample_count);
let dt = t_i - prev_t;
prev_t = t_i;
let r_i = get_local_r(r, mu, t_i);
let atmosphere_sample = sample_atmosphere(r_i);
let sample_optical_depth = atmosphere_sample.extinction * dt;
optical_depth += sample_optical_depth;
}
return optical_depth;
}

View File

@@ -0,0 +1,45 @@
#define_import_path bevy_pbr::atmosphere::types
struct Atmosphere {
// Radius of the planet
bottom_radius: f32, // units: m
// Radius at which we consider the atmosphere to 'end' for out calculations (from center of planet)
top_radius: f32, // units: m
ground_albedo: vec3<f32>,
rayleigh_density_exp_scale: f32,
rayleigh_scattering: vec3<f32>,
mie_density_exp_scale: f32,
mie_scattering: f32, // units: m^-1
mie_absorption: f32, // units: m^-1
mie_asymmetry: f32, // the "asymmetry" value of the phase function, unitless. Domain: (-1, 1)
ozone_layer_altitude: f32, // units: m
ozone_layer_width: f32, // units: m
ozone_absorption: vec3<f32>, // ozone absorption. units: m^-1
}
struct AtmosphereSettings {
transmittance_lut_size: vec2<u32>,
multiscattering_lut_size: vec2<u32>,
sky_view_lut_size: vec2<u32>,
aerial_view_lut_size: vec3<u32>,
transmittance_lut_samples: u32,
multiscattering_lut_dirs: u32,
multiscattering_lut_samples: u32,
sky_view_lut_samples: u32,
aerial_view_lut_samples: u32,
aerial_view_lut_max_distance: f32,
scene_units_to_m: f32,
}
// "Atmosphere space" is just the view position with y=0 and oriented horizontally,
// so the horizon stays a horizontal line in our luts
struct AtmosphereTransforms {
world_from_atmosphere: mat4x4<f32>,
atmosphere_from_world: mat4x4<f32>,
}

1186
vendor/bevy_pbr/src/cluster/assign.rs vendored Normal file

File diff suppressed because it is too large Load Diff

859
vendor/bevy_pbr/src/cluster/mod.rs vendored Normal file
View File

@@ -0,0 +1,859 @@
//! Spatial clustering of objects, currently just point and spot lights.
use core::num::NonZero;
use bevy_core_pipeline::core_3d::Camera3d;
use bevy_ecs::{
component::Component,
entity::{Entity, EntityHashMap},
query::{With, Without},
reflect::ReflectComponent,
resource::Resource,
system::{Commands, Query, Res},
world::{FromWorld, World},
};
use bevy_math::{uvec4, AspectRatio, UVec2, UVec3, UVec4, Vec3Swizzles as _, Vec4};
use bevy_platform::collections::HashSet;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::Camera,
render_resource::{
BindingResource, BufferBindingType, ShaderSize as _, ShaderType, StorageBuffer,
UniformBuffer,
},
renderer::{RenderDevice, RenderQueue},
sync_world::RenderEntity,
Extract,
};
use tracing::warn;
pub(crate) use crate::cluster::assign::assign_objects_to_clusters;
use crate::MeshPipeline;
pub(crate) mod assign;
#[cfg(test)]
mod test;
// NOTE: this must be kept in sync with the same constants in
// `mesh_view_types.wgsl`.
pub const MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS: usize = 204;
// Make sure that the clusterable object buffer doesn't overflow the maximum
// size of a UBO on WebGL 2.
const _: () =
assert!(size_of::<GpuClusterableObject>() * MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS <= 16384);
// NOTE: Clustered-forward rendering requires 3 storage buffer bindings so check that
// at least that many are supported using this constant and SupportedBindingType::from_device()
pub const CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT: u32 = 3;
// this must match CLUSTER_COUNT_SIZE in pbr.wgsl
// and must be large enough to contain MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS
const CLUSTER_COUNT_SIZE: u32 = 9;
const CLUSTER_OFFSET_MASK: u32 = (1 << (32 - (CLUSTER_COUNT_SIZE * 2))) - 1;
const CLUSTER_COUNT_MASK: u32 = (1 << CLUSTER_COUNT_SIZE) - 1;
// Clustered-forward rendering notes
// The main initial reference material used was this rather accessible article:
// http://www.aortiz.me/2018/12/21/CG.html
// Some inspiration was taken from “Practical Clustered Shading” which is part 2 of:
// https://efficientshading.com/2015/01/01/real-time-many-light-management-and-shadows-with-clustered-shading/
// (Also note that Part 3 of the above shows how we could support the shadow mapping for many lights.)
// The z-slicing method mentioned in the aortiz article is originally from Tiago Sousa's Siggraph 2016 talk about Doom 2016:
// http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf
/// Configure the far z-plane mode used for the furthest depth slice for clustered forward
/// rendering
#[derive(Debug, Copy, Clone, Reflect)]
#[reflect(Clone)]
pub enum ClusterFarZMode {
/// Calculate the required maximum z-depth based on currently visible
/// clusterable objects. Makes better use of available clusters, speeding
/// up GPU lighting operations at the expense of some CPU time and using
/// more indices in the clusterable object index lists.
MaxClusterableObjectRange,
/// Constant max z-depth
Constant(f32),
}
/// Configure the depth-slicing strategy for clustered forward rendering
#[derive(Debug, Copy, Clone, Reflect)]
#[reflect(Default, Clone)]
pub struct ClusterZConfig {
/// Far `Z` plane of the first depth slice
pub first_slice_depth: f32,
/// Strategy for how to evaluate the far `Z` plane of the furthest depth slice
pub far_z_mode: ClusterFarZMode,
}
/// Configuration of the clustering strategy for clustered forward rendering
#[derive(Debug, Copy, Clone, Component, Reflect)]
#[reflect(Component, Debug, Default, Clone)]
pub enum ClusterConfig {
/// Disable cluster calculations for this view
None,
/// One single cluster. Optimal for low-light complexity scenes or scenes where
/// most lights affect the entire scene.
Single,
/// Explicit `X`, `Y` and `Z` counts (may yield non-square `X/Y` clusters depending on the aspect ratio)
XYZ {
dimensions: UVec3,
z_config: ClusterZConfig,
/// Specify if clusters should automatically resize in `X/Y` if there is a risk of exceeding
/// the available cluster-object index limit
dynamic_resizing: bool,
},
/// Fixed number of `Z` slices, `X` and `Y` calculated to give square clusters
/// with at most total clusters. For top-down games where lights will generally always be within a
/// short depth range, it may be useful to use this configuration with 1 or few `Z` slices. This
/// would reduce the number of lights per cluster by distributing more clusters in screen space
/// `X/Y` which matches how lights are distributed in the scene.
FixedZ {
total: u32,
z_slices: u32,
z_config: ClusterZConfig,
/// Specify if clusters should automatically resize in `X/Y` if there is a risk of exceeding
/// the available clusterable object index limit
dynamic_resizing: bool,
},
}
#[derive(Component, Debug, Default)]
pub struct Clusters {
/// Tile size
pub(crate) tile_size: UVec2,
/// Number of clusters in `X` / `Y` / `Z` in the view frustum
pub(crate) dimensions: UVec3,
/// Distance to the far plane of the first depth slice. The first depth slice is special
/// and explicitly-configured to avoid having unnecessarily many slices close to the camera.
pub(crate) near: f32,
pub(crate) far: f32,
pub(crate) clusterable_objects: Vec<VisibleClusterableObjects>,
}
#[derive(Clone, Component, Debug, Default)]
pub struct VisibleClusterableObjects {
pub(crate) entities: Vec<Entity>,
counts: ClusterableObjectCounts,
}
#[derive(Resource, Default)]
pub struct GlobalVisibleClusterableObjects {
pub(crate) entities: HashSet<Entity>,
}
#[derive(Resource)]
pub struct GlobalClusterableObjectMeta {
pub gpu_clusterable_objects: GpuClusterableObjects,
pub entity_to_index: EntityHashMap<usize>,
}
#[derive(Copy, Clone, ShaderType, Default, Debug)]
pub struct GpuClusterableObject {
// For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3]
// For spot lights: 2 components of the direction (x,z), spot_scale and spot_offset
pub(crate) light_custom_data: Vec4,
pub(crate) color_inverse_square_range: Vec4,
pub(crate) position_radius: Vec4,
pub(crate) flags: u32,
pub(crate) shadow_depth_bias: f32,
pub(crate) shadow_normal_bias: f32,
pub(crate) spot_light_tan_angle: f32,
pub(crate) soft_shadow_size: f32,
pub(crate) shadow_map_near_z: f32,
pub(crate) pad_a: f32,
pub(crate) pad_b: f32,
}
pub enum GpuClusterableObjects {
Uniform(UniformBuffer<GpuClusterableObjectsUniform>),
Storage(StorageBuffer<GpuClusterableObjectsStorage>),
}
#[derive(ShaderType)]
pub struct GpuClusterableObjectsUniform {
data: Box<[GpuClusterableObject; MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS]>,
}
#[derive(ShaderType, Default)]
pub struct GpuClusterableObjectsStorage {
#[size(runtime)]
data: Vec<GpuClusterableObject>,
}
#[derive(Component)]
pub struct ExtractedClusterConfig {
/// Special near value for cluster calculations
pub(crate) near: f32,
pub(crate) far: f32,
/// Number of clusters in `X` / `Y` / `Z` in the view frustum
pub(crate) dimensions: UVec3,
}
/// Stores the number of each type of clusterable object in a single cluster.
///
/// Note that `reflection_probes` and `irradiance_volumes` won't be clustered if
/// fewer than 3 SSBOs are available, which usually means on WebGL 2.
#[derive(Clone, Copy, Default, Debug)]
struct ClusterableObjectCounts {
/// The number of point lights in the cluster.
point_lights: u32,
/// The number of spot lights in the cluster.
spot_lights: u32,
/// The number of reflection probes in the cluster.
reflection_probes: u32,
/// The number of irradiance volumes in the cluster.
irradiance_volumes: u32,
/// The number of decals in the cluster.
decals: u32,
}
enum ExtractedClusterableObjectElement {
ClusterHeader(ClusterableObjectCounts),
ClusterableObjectEntity(Entity),
}
#[derive(Component)]
pub struct ExtractedClusterableObjects {
data: Vec<ExtractedClusterableObjectElement>,
}
#[derive(ShaderType)]
struct GpuClusterOffsetsAndCountsUniform {
data: Box<[UVec4; ViewClusterBindings::MAX_UNIFORM_ITEMS]>,
}
#[derive(ShaderType, Default)]
struct GpuClusterableObjectIndexListsStorage {
#[size(runtime)]
data: Vec<u32>,
}
#[derive(ShaderType, Default)]
struct GpuClusterOffsetsAndCountsStorage {
/// The starting offset, followed by the number of point lights, spot
/// lights, reflection probes, and irradiance volumes in each cluster, in
/// that order. The remaining fields are filled with zeroes.
#[size(runtime)]
data: Vec<[UVec4; 2]>,
}
enum ViewClusterBuffers {
Uniform {
// NOTE: UVec4 is because all arrays in Std140 layout have 16-byte alignment
clusterable_object_index_lists: UniformBuffer<GpuClusterableObjectIndexListsUniform>,
// NOTE: UVec4 is because all arrays in Std140 layout have 16-byte alignment
cluster_offsets_and_counts: UniformBuffer<GpuClusterOffsetsAndCountsUniform>,
},
Storage {
clusterable_object_index_lists: StorageBuffer<GpuClusterableObjectIndexListsStorage>,
cluster_offsets_and_counts: StorageBuffer<GpuClusterOffsetsAndCountsStorage>,
},
}
#[derive(Component)]
pub struct ViewClusterBindings {
n_indices: usize,
n_offsets: usize,
buffers: ViewClusterBuffers,
}
impl Default for ClusterZConfig {
fn default() -> Self {
Self {
first_slice_depth: 5.0,
far_z_mode: ClusterFarZMode::MaxClusterableObjectRange,
}
}
}
impl Default for ClusterConfig {
fn default() -> Self {
// 24 depth slices, square clusters with at most 4096 total clusters
// use max light distance as clusters max `Z`-depth, first slice extends to 5.0
Self::FixedZ {
total: 4096,
z_slices: 24,
z_config: ClusterZConfig::default(),
dynamic_resizing: true,
}
}
}
impl ClusterConfig {
fn dimensions_for_screen_size(&self, screen_size: UVec2) -> UVec3 {
match &self {
ClusterConfig::None => UVec3::ZERO,
ClusterConfig::Single => UVec3::ONE,
ClusterConfig::XYZ { dimensions, .. } => *dimensions,
ClusterConfig::FixedZ {
total, z_slices, ..
} => {
let aspect_ratio: f32 = AspectRatio::try_from_pixels(screen_size.x, screen_size.y)
.expect("Failed to calculate aspect ratio for Cluster: screen dimensions must be positive, non-zero values")
.ratio();
let mut z_slices = *z_slices;
if *total < z_slices {
warn!("ClusterConfig has more z-slices than total clusters!");
z_slices = *total;
}
let per_layer = *total as f32 / z_slices as f32;
let y = f32::sqrt(per_layer / aspect_ratio);
let mut x = (y * aspect_ratio) as u32;
let mut y = y as u32;
// check extremes
if x == 0 {
x = 1;
y = per_layer as u32;
}
if y == 0 {
x = per_layer as u32;
y = 1;
}
UVec3::new(x, y, z_slices)
}
}
}
fn first_slice_depth(&self) -> f32 {
match self {
ClusterConfig::None | ClusterConfig::Single => 0.0,
ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {
z_config.first_slice_depth
}
}
}
fn far_z_mode(&self) -> ClusterFarZMode {
match self {
ClusterConfig::None => ClusterFarZMode::Constant(0.0),
ClusterConfig::Single => ClusterFarZMode::MaxClusterableObjectRange,
ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {
z_config.far_z_mode
}
}
}
fn dynamic_resizing(&self) -> bool {
match self {
ClusterConfig::None | ClusterConfig::Single => false,
ClusterConfig::XYZ {
dynamic_resizing, ..
}
| ClusterConfig::FixedZ {
dynamic_resizing, ..
} => *dynamic_resizing,
}
}
}
impl Clusters {
fn update(&mut self, screen_size: UVec2, requested_dimensions: UVec3) {
debug_assert!(
requested_dimensions.x > 0 && requested_dimensions.y > 0 && requested_dimensions.z > 0
);
let tile_size = (screen_size.as_vec2() / requested_dimensions.xy().as_vec2())
.ceil()
.as_uvec2()
.max(UVec2::ONE);
self.tile_size = tile_size;
self.dimensions = (screen_size.as_vec2() / tile_size.as_vec2())
.ceil()
.as_uvec2()
.extend(requested_dimensions.z)
.max(UVec3::ONE);
// NOTE: Maximum 4096 clusters due to uniform buffer size constraints
debug_assert!(self.dimensions.x * self.dimensions.y * self.dimensions.z <= 4096);
}
fn clear(&mut self) {
self.tile_size = UVec2::ONE;
self.dimensions = UVec3::ZERO;
self.near = 0.0;
self.far = 0.0;
self.clusterable_objects.clear();
}
}
pub fn add_clusters(
mut commands: Commands,
cameras: Query<(Entity, Option<&ClusterConfig>, &Camera), (Without<Clusters>, With<Camera3d>)>,
) {
for (entity, config, camera) in &cameras {
if !camera.is_active {
continue;
}
let config = config.copied().unwrap_or_default();
// actual settings here don't matter - they will be overwritten in
// `assign_objects_to_clusters``
commands
.entity(entity)
.insert((Clusters::default(), config));
}
}
impl VisibleClusterableObjects {
#[inline]
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Entity> {
self.entities.iter()
}
#[inline]
pub fn len(&self) -> usize {
self.entities.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.entities.is_empty()
}
}
impl GlobalVisibleClusterableObjects {
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &Entity> {
self.entities.iter()
}
#[inline]
pub fn contains(&self, entity: Entity) -> bool {
self.entities.contains(&entity)
}
}
impl FromWorld for GlobalClusterableObjectMeta {
fn from_world(world: &mut World) -> Self {
Self::new(
world
.resource::<RenderDevice>()
.get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT),
)
}
}
impl GlobalClusterableObjectMeta {
pub fn new(buffer_binding_type: BufferBindingType) -> Self {
Self {
gpu_clusterable_objects: GpuClusterableObjects::new(buffer_binding_type),
entity_to_index: EntityHashMap::default(),
}
}
}
impl GpuClusterableObjects {
fn new(buffer_binding_type: BufferBindingType) -> Self {
match buffer_binding_type {
BufferBindingType::Storage { .. } => Self::storage(),
BufferBindingType::Uniform => Self::uniform(),
}
}
fn uniform() -> Self {
Self::Uniform(UniformBuffer::default())
}
fn storage() -> Self {
Self::Storage(StorageBuffer::default())
}
pub(crate) fn set(&mut self, mut clusterable_objects: Vec<GpuClusterableObject>) {
match self {
GpuClusterableObjects::Uniform(buffer) => {
let len = clusterable_objects
.len()
.min(MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS);
let src = &clusterable_objects[..len];
let dst = &mut buffer.get_mut().data[..len];
dst.copy_from_slice(src);
}
GpuClusterableObjects::Storage(buffer) => {
buffer.get_mut().data.clear();
buffer.get_mut().data.append(&mut clusterable_objects);
}
}
}
pub(crate) fn write_buffer(
&mut self,
render_device: &RenderDevice,
render_queue: &RenderQueue,
) {
match self {
GpuClusterableObjects::Uniform(buffer) => {
buffer.write_buffer(render_device, render_queue);
}
GpuClusterableObjects::Storage(buffer) => {
buffer.write_buffer(render_device, render_queue);
}
}
}
pub fn binding(&self) -> Option<BindingResource> {
match self {
GpuClusterableObjects::Uniform(buffer) => buffer.binding(),
GpuClusterableObjects::Storage(buffer) => buffer.binding(),
}
}
pub fn min_size(buffer_binding_type: BufferBindingType) -> NonZero<u64> {
match buffer_binding_type {
BufferBindingType::Storage { .. } => GpuClusterableObjectsStorage::min_size(),
BufferBindingType::Uniform => GpuClusterableObjectsUniform::min_size(),
}
}
}
impl Default for GpuClusterableObjectsUniform {
fn default() -> Self {
Self {
data: Box::new(
[GpuClusterableObject::default(); MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS],
),
}
}
}
/// Extracts clusters from the main world from the render world.
pub fn extract_clusters(
mut commands: Commands,
views: Extract<Query<(RenderEntity, &Clusters, &Camera)>>,
mapper: Extract<Query<RenderEntity>>,
) {
for (entity, clusters, camera) in &views {
let mut entity_commands = commands
.get_entity(entity)
.expect("Clusters entity wasn't synced.");
if !camera.is_active {
entity_commands.remove::<(ExtractedClusterableObjects, ExtractedClusterConfig)>();
continue;
}
let num_entities: usize = clusters
.clusterable_objects
.iter()
.map(|l| l.entities.len())
.sum();
let mut data = Vec::with_capacity(clusters.clusterable_objects.len() + num_entities);
for cluster_objects in &clusters.clusterable_objects {
data.push(ExtractedClusterableObjectElement::ClusterHeader(
cluster_objects.counts,
));
for clusterable_entity in &cluster_objects.entities {
if let Ok(entity) = mapper.get(*clusterable_entity) {
data.push(ExtractedClusterableObjectElement::ClusterableObjectEntity(
entity,
));
}
}
}
entity_commands.insert((
ExtractedClusterableObjects { data },
ExtractedClusterConfig {
near: clusters.near,
far: clusters.far,
dimensions: clusters.dimensions,
},
));
}
}
pub fn prepare_clusters(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mesh_pipeline: Res<MeshPipeline>,
global_clusterable_object_meta: Res<GlobalClusterableObjectMeta>,
views: Query<(Entity, &ExtractedClusterableObjects)>,
) {
let render_device = render_device.into_inner();
let supports_storage_buffers = matches!(
mesh_pipeline.clustered_forward_buffer_binding_type,
BufferBindingType::Storage { .. }
);
for (entity, extracted_clusters) in &views {
let mut view_clusters_bindings =
ViewClusterBindings::new(mesh_pipeline.clustered_forward_buffer_binding_type);
view_clusters_bindings.clear();
for record in &extracted_clusters.data {
match record {
ExtractedClusterableObjectElement::ClusterHeader(counts) => {
let offset = view_clusters_bindings.n_indices();
view_clusters_bindings.push_offset_and_counts(offset, counts);
}
ExtractedClusterableObjectElement::ClusterableObjectEntity(entity) => {
if let Some(clusterable_object_index) =
global_clusterable_object_meta.entity_to_index.get(entity)
{
if view_clusters_bindings.n_indices() >= ViewClusterBindings::MAX_INDICES
&& !supports_storage_buffers
{
warn!(
"Clusterable object index lists are full! The clusterable \
objects in the view are present in too many clusters."
);
break;
}
view_clusters_bindings.push_index(*clusterable_object_index);
}
}
}
}
view_clusters_bindings.write_buffers(render_device, &render_queue);
commands.entity(entity).insert(view_clusters_bindings);
}
}
impl ViewClusterBindings {
pub const MAX_OFFSETS: usize = 16384 / 4;
const MAX_UNIFORM_ITEMS: usize = Self::MAX_OFFSETS / 4;
pub const MAX_INDICES: usize = 16384;
pub fn new(buffer_binding_type: BufferBindingType) -> Self {
Self {
n_indices: 0,
n_offsets: 0,
buffers: ViewClusterBuffers::new(buffer_binding_type),
}
}
pub fn clear(&mut self) {
match &mut self.buffers {
ViewClusterBuffers::Uniform {
clusterable_object_index_lists,
cluster_offsets_and_counts,
} => {
*clusterable_object_index_lists.get_mut().data =
[UVec4::ZERO; Self::MAX_UNIFORM_ITEMS];
*cluster_offsets_and_counts.get_mut().data = [UVec4::ZERO; Self::MAX_UNIFORM_ITEMS];
}
ViewClusterBuffers::Storage {
clusterable_object_index_lists,
cluster_offsets_and_counts,
..
} => {
clusterable_object_index_lists.get_mut().data.clear();
cluster_offsets_and_counts.get_mut().data.clear();
}
}
}
fn push_offset_and_counts(&mut self, offset: usize, counts: &ClusterableObjectCounts) {
match &mut self.buffers {
ViewClusterBuffers::Uniform {
cluster_offsets_and_counts,
..
} => {
let array_index = self.n_offsets >> 2; // >> 2 is equivalent to / 4
if array_index >= Self::MAX_UNIFORM_ITEMS {
warn!("cluster offset and count out of bounds!");
return;
}
let component = self.n_offsets & ((1 << 2) - 1);
let packed =
pack_offset_and_counts(offset, counts.point_lights, counts.spot_lights);
cluster_offsets_and_counts.get_mut().data[array_index][component] = packed;
}
ViewClusterBuffers::Storage {
cluster_offsets_and_counts,
..
} => {
cluster_offsets_and_counts.get_mut().data.push([
uvec4(
offset as u32,
counts.point_lights,
counts.spot_lights,
counts.reflection_probes,
),
uvec4(counts.irradiance_volumes, counts.decals, 0, 0),
]);
}
}
self.n_offsets += 1;
}
pub fn n_indices(&self) -> usize {
self.n_indices
}
pub fn push_index(&mut self, index: usize) {
match &mut self.buffers {
ViewClusterBuffers::Uniform {
clusterable_object_index_lists,
..
} => {
let array_index = self.n_indices >> 4; // >> 4 is equivalent to / 16
let component = (self.n_indices >> 2) & ((1 << 2) - 1);
let sub_index = self.n_indices & ((1 << 2) - 1);
let index = index as u32;
clusterable_object_index_lists.get_mut().data[array_index][component] |=
index << (8 * sub_index);
}
ViewClusterBuffers::Storage {
clusterable_object_index_lists,
..
} => {
clusterable_object_index_lists
.get_mut()
.data
.push(index as u32);
}
}
self.n_indices += 1;
}
pub fn write_buffers(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) {
match &mut self.buffers {
ViewClusterBuffers::Uniform {
clusterable_object_index_lists,
cluster_offsets_and_counts,
} => {
clusterable_object_index_lists.write_buffer(render_device, render_queue);
cluster_offsets_and_counts.write_buffer(render_device, render_queue);
}
ViewClusterBuffers::Storage {
clusterable_object_index_lists,
cluster_offsets_and_counts,
} => {
clusterable_object_index_lists.write_buffer(render_device, render_queue);
cluster_offsets_and_counts.write_buffer(render_device, render_queue);
}
}
}
pub fn clusterable_object_index_lists_binding(&self) -> Option<BindingResource> {
match &self.buffers {
ViewClusterBuffers::Uniform {
clusterable_object_index_lists,
..
} => clusterable_object_index_lists.binding(),
ViewClusterBuffers::Storage {
clusterable_object_index_lists,
..
} => clusterable_object_index_lists.binding(),
}
}
pub fn offsets_and_counts_binding(&self) -> Option<BindingResource> {
match &self.buffers {
ViewClusterBuffers::Uniform {
cluster_offsets_and_counts,
..
} => cluster_offsets_and_counts.binding(),
ViewClusterBuffers::Storage {
cluster_offsets_and_counts,
..
} => cluster_offsets_and_counts.binding(),
}
}
pub fn min_size_clusterable_object_index_lists(
buffer_binding_type: BufferBindingType,
) -> NonZero<u64> {
match buffer_binding_type {
BufferBindingType::Storage { .. } => GpuClusterableObjectIndexListsStorage::min_size(),
BufferBindingType::Uniform => GpuClusterableObjectIndexListsUniform::min_size(),
}
}
pub fn min_size_cluster_offsets_and_counts(
buffer_binding_type: BufferBindingType,
) -> NonZero<u64> {
match buffer_binding_type {
BufferBindingType::Storage { .. } => GpuClusterOffsetsAndCountsStorage::min_size(),
BufferBindingType::Uniform => GpuClusterOffsetsAndCountsUniform::min_size(),
}
}
}
impl ViewClusterBuffers {
fn new(buffer_binding_type: BufferBindingType) -> Self {
match buffer_binding_type {
BufferBindingType::Storage { .. } => Self::storage(),
BufferBindingType::Uniform => Self::uniform(),
}
}
fn uniform() -> Self {
ViewClusterBuffers::Uniform {
clusterable_object_index_lists: UniformBuffer::default(),
cluster_offsets_and_counts: UniformBuffer::default(),
}
}
fn storage() -> Self {
ViewClusterBuffers::Storage {
clusterable_object_index_lists: StorageBuffer::default(),
cluster_offsets_and_counts: StorageBuffer::default(),
}
}
}
// Compresses the offset and counts of point and spot lights so that they fit in
// a UBO.
//
// This function is only used if storage buffers are unavailable on this
// platform: typically, on WebGL 2.
//
// NOTE: With uniform buffer max binding size as 16384 bytes
// that means we can fit 204 clusterable objects in one uniform
// buffer, which means the count can be at most 204 so it
// needs 9 bits.
// The array of indices can also use u8 and that means the
// offset in to the array of indices needs to be able to address
// 16384 values. log2(16384) = 14 bits.
// We use 32 bits to store the offset and counts so
// we pack the offset into the upper 14 bits of a u32,
// the point light count into bits 9-17, and the spot light count into bits 0-8.
// [ 31 .. 18 | 17 .. 9 | 8 .. 0 ]
// [ offset | point light count | spot light count ]
//
// NOTE: This assumes CPU and GPU endianness are the same which is true
// for all common and tested x86/ARM CPUs and AMD/NVIDIA/Intel/Apple/etc GPUs
//
// NOTE: On platforms that use this function, we don't cluster light probes, so
// the number of light probes is irrelevant.
fn pack_offset_and_counts(offset: usize, point_count: u32, spot_count: u32) -> u32 {
((offset as u32 & CLUSTER_OFFSET_MASK) << (CLUSTER_COUNT_SIZE * 2))
| ((point_count & CLUSTER_COUNT_MASK) << CLUSTER_COUNT_SIZE)
| (spot_count & CLUSTER_COUNT_MASK)
}
#[derive(ShaderType)]
struct GpuClusterableObjectIndexListsUniform {
data: Box<[UVec4; ViewClusterBindings::MAX_UNIFORM_ITEMS]>,
}
// NOTE: Assert at compile time that GpuClusterableObjectIndexListsUniform
// fits within the maximum uniform buffer binding size
const _: () = assert!(GpuClusterableObjectIndexListsUniform::SHADER_SIZE.get() <= 16384);
impl Default for GpuClusterableObjectIndexListsUniform {
fn default() -> Self {
Self {
data: Box::new([UVec4::ZERO; ViewClusterBindings::MAX_UNIFORM_ITEMS]),
}
}
}
impl Default for GpuClusterOffsetsAndCountsUniform {
fn default() -> Self {
Self {
data: Box::new([UVec4::ZERO; ViewClusterBindings::MAX_UNIFORM_ITEMS]),
}
}
}

54
vendor/bevy_pbr/src/cluster/test.rs vendored Normal file
View File

@@ -0,0 +1,54 @@
use bevy_math::UVec2;
use crate::{ClusterConfig, Clusters};
fn test_cluster_tiling(config: ClusterConfig, screen_size: UVec2) -> Clusters {
let dims = config.dimensions_for_screen_size(screen_size);
// note: near & far do not affect tiling
let mut clusters = Clusters::default();
clusters.update(screen_size, dims);
// check we cover the screen
assert!(clusters.tile_size.x * clusters.dimensions.x >= screen_size.x);
assert!(clusters.tile_size.y * clusters.dimensions.y >= screen_size.y);
// check a smaller number of clusters would not cover the screen
assert!(clusters.tile_size.x * (clusters.dimensions.x - 1) < screen_size.x);
assert!(clusters.tile_size.y * (clusters.dimensions.y - 1) < screen_size.y);
// check a smaller tile size would not cover the screen
assert!((clusters.tile_size.x - 1) * clusters.dimensions.x < screen_size.x);
assert!((clusters.tile_size.y - 1) * clusters.dimensions.y < screen_size.y);
// check we don't have more clusters than pixels
assert!(clusters.dimensions.x <= screen_size.x);
assert!(clusters.dimensions.y <= screen_size.y);
clusters
}
#[test]
// check tiling for small screen sizes
fn test_default_cluster_setup_small_screensizes() {
for x in 1..100 {
for y in 1..100 {
let screen_size = UVec2::new(x, y);
let clusters = test_cluster_tiling(ClusterConfig::default(), screen_size);
assert!(clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z <= 4096);
}
}
}
#[test]
// check tiling for long thin screen sizes
fn test_default_cluster_setup_small_x() {
for x in 1..10 {
for y in 1..5000 {
let screen_size = UVec2::new(x, y);
let clusters = test_cluster_tiling(ClusterConfig::default(), screen_size);
assert!(clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z <= 4096);
let screen_size = UVec2::new(y, x);
let clusters = test_cluster_tiling(ClusterConfig::default(), screen_size);
assert!(clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z <= 4096);
}
}
}

89
vendor/bevy_pbr/src/components.rs vendored Normal file
View File

@@ -0,0 +1,89 @@
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::component::Component;
use bevy_ecs::entity::{Entity, EntityHashMap};
use bevy_ecs::reflect::ReflectComponent;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::sync_world::MainEntity;
/// Collection of mesh entities visible for 3D lighting.
///
/// This component contains all mesh entities visible from the current light view.
/// The collection is updated automatically by [`crate::SimulationLightSystems`].
#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)]
#[reflect(Component, Debug, Default, Clone)]
pub struct VisibleMeshEntities {
#[reflect(ignore, clone)]
pub entities: Vec<Entity>,
}
#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)]
#[reflect(Component, Debug, Default, Clone)]
pub struct RenderVisibleMeshEntities {
#[reflect(ignore, clone)]
pub entities: Vec<(Entity, MainEntity)>,
}
#[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component, Debug, Default, Clone)]
pub struct CubemapVisibleEntities {
#[reflect(ignore, clone)]
data: [VisibleMeshEntities; 6],
}
impl CubemapVisibleEntities {
pub fn get(&self, i: usize) -> &VisibleMeshEntities {
&self.data[i]
}
pub fn get_mut(&mut self, i: usize) -> &mut VisibleMeshEntities {
&mut self.data[i]
}
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &VisibleMeshEntities> {
self.data.iter()
}
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut VisibleMeshEntities> {
self.data.iter_mut()
}
}
#[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component, Debug, Default, Clone)]
pub struct RenderCubemapVisibleEntities {
#[reflect(ignore, clone)]
pub(crate) data: [RenderVisibleMeshEntities; 6],
}
impl RenderCubemapVisibleEntities {
pub fn get(&self, i: usize) -> &RenderVisibleMeshEntities {
&self.data[i]
}
pub fn get_mut(&mut self, i: usize) -> &mut RenderVisibleMeshEntities {
&mut self.data[i]
}
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &RenderVisibleMeshEntities> {
self.data.iter()
}
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut RenderVisibleMeshEntities> {
self.data.iter_mut()
}
}
#[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component, Default, Clone)]
pub struct CascadesVisibleEntities {
/// Map of view entity to the visible entities for each cascade frustum.
#[reflect(ignore, clone)]
pub entities: EntityHashMap<Vec<VisibleMeshEntities>>,
}
#[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component, Default, Clone)]
pub struct RenderCascadesVisibleEntities {
/// Map of view entity to the visible entities for each cascade frustum.
#[reflect(ignore, clone)]
pub entities: EntityHashMap<Vec<RenderVisibleMeshEntities>>,
}

385
vendor/bevy_pbr/src/decal/clustered.rs vendored Normal file
View File

@@ -0,0 +1,385 @@
//! Clustered decals, bounding regions that project textures onto surfaces.
//!
//! A *clustered decal* is a bounding box that projects a texture onto any
//! surface within its bounds along the positive Z axis. In Bevy, clustered
//! decals use the *clustered forward* rendering technique.
//!
//! Clustered decals are the highest-quality types of decals that Bevy supports,
//! but they require bindless textures. This means that they presently can't be
//! used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used
//! with forward or deferred rendering and don't require a prepass.
//!
//! On their own, clustered decals only project the base color of a texture. You
//! can, however, use the built-in *tag* field to customize the appearance of a
//! clustered decal arbitrarily. See the documentation in `clustered.wgsl` for
//! more information and the `clustered_decals` example for an example of use.
use core::{num::NonZero, ops::Deref};
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::{Entity, EntityHashMap},
prelude::ReflectComponent,
query::With,
resource::Resource,
schedule::IntoScheduleConfigs as _,
system::{Query, Res, ResMut},
};
use bevy_image::Image;
use bevy_math::Mat4;
use bevy_platform::collections::HashMap;
use bevy_reflect::Reflect;
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_asset::RenderAssets,
render_resource::{
binding_types, BindGroupLayoutEntryBuilder, Buffer, BufferUsages, RawBufferVec, Sampler,
SamplerBindingType, Shader, ShaderType, TextureSampleType, TextureView,
},
renderer::{RenderAdapter, RenderDevice, RenderQueue},
sync_world::RenderEntity,
texture::{FallbackImage, GpuImage},
view::{self, ViewVisibility, Visibility, VisibilityClass},
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::{components::GlobalTransform, prelude::Transform};
use bytemuck::{Pod, Zeroable};
use crate::{
binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta, LightVisibilityClass,
};
/// The handle to the `clustered.wgsl` shader.
pub(crate) const CLUSTERED_DECAL_SHADER_HANDLE: Handle<Shader> =
weak_handle!("87929002-3509-42f1-8279-2d2765dd145c");
/// The maximum number of decals that can be present in a view.
///
/// This number is currently relatively low in order to work around the lack of
/// first-class binding arrays in `wgpu`. When that feature is implemented, this
/// limit can be increased.
pub(crate) const MAX_VIEW_DECALS: usize = 8;
/// A plugin that adds support for clustered decals.
///
/// In environments where bindless textures aren't available, clustered decals
/// can still be added to a scene, but they won't project any decals.
pub struct ClusteredDecalPlugin;
/// An object that projects a decal onto surfaces within its bounds.
///
/// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It
/// projects the given [`Self::image`] onto surfaces in the +Z direction (thus
/// you may find [`Transform::looking_at`] useful).
///
/// Clustered decals are the highest-quality types of decals that Bevy supports,
/// but they require bindless textures. This means that they presently can't be
/// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used
/// with forward or deferred rendering and don't require a prepass.
#[derive(Component, Debug, Clone, Reflect, ExtractComponent)]
#[reflect(Component, Debug, Clone)]
#[require(Transform, Visibility, VisibilityClass)]
#[component(on_add = view::add_visibility_class::<LightVisibilityClass>)]
pub struct ClusteredDecal {
/// The image that the clustered decal projects.
///
/// This must be a 2D image. If it has an alpha channel, it'll be alpha
/// blended with the underlying surface and/or other decals. All decal
/// images in the scene must use the same sampler.
pub image: Handle<Image>,
/// An application-specific tag you can use for any purpose you want.
///
/// See the `clustered_decals` example for an example of use.
pub tag: u32,
}
/// Stores information about all the clustered decals in the scene.
#[derive(Resource, Default)]
pub struct RenderClusteredDecals {
/// Maps an index in the shader binding array to the associated decal image.
///
/// [`Self::texture_to_binding_index`] holds the inverse mapping.
binding_index_to_textures: Vec<AssetId<Image>>,
/// Maps a decal image to the shader binding array.
///
/// [`Self::binding_index_to_textures`] holds the inverse mapping.
texture_to_binding_index: HashMap<AssetId<Image>, u32>,
/// The information concerning each decal that we provide to the shader.
decals: Vec<RenderClusteredDecal>,
/// Maps the [`bevy_render::sync_world::RenderEntity`] of each decal to the
/// index of that decal in the [`Self::decals`] list.
entity_to_decal_index: EntityHashMap<usize>,
}
impl RenderClusteredDecals {
/// Clears out this [`RenderClusteredDecals`] in preparation for a new
/// frame.
fn clear(&mut self) {
self.binding_index_to_textures.clear();
self.texture_to_binding_index.clear();
self.decals.clear();
self.entity_to_decal_index.clear();
}
}
/// The per-view bind group entries pertaining to decals.
pub(crate) struct RenderViewClusteredDecalBindGroupEntries<'a> {
/// The list of decals, corresponding to `mesh_view_bindings::decals` in the
/// shader.
pub(crate) decals: &'a Buffer,
/// The list of textures, corresponding to
/// `mesh_view_bindings::decal_textures` in the shader.
pub(crate) texture_views: Vec<&'a <TextureView as Deref>::Target>,
/// The sampler that the shader uses to sample decals, corresponding to
/// `mesh_view_bindings::decal_sampler` in the shader.
pub(crate) sampler: &'a Sampler,
}
/// A render-world resource that holds the buffer of [`ClusteredDecal`]s ready
/// to upload to the GPU.
#[derive(Resource, Deref, DerefMut)]
pub struct DecalsBuffer(RawBufferVec<RenderClusteredDecal>);
impl Default for DecalsBuffer {
fn default() -> Self {
DecalsBuffer(RawBufferVec::new(BufferUsages::STORAGE))
}
}
impl Plugin for ClusteredDecalPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
CLUSTERED_DECAL_SHADER_HANDLE,
"clustered.wgsl",
Shader::from_wgsl
);
app.add_plugins(ExtractComponentPlugin::<ClusteredDecal>::default())
.register_type::<ClusteredDecal>();
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<DecalsBuffer>()
.init_resource::<RenderClusteredDecals>()
.add_systems(ExtractSchedule, extract_decals)
.add_systems(
Render,
prepare_decals
.in_set(RenderSet::ManageViews)
.after(prepare_lights),
)
.add_systems(Render, upload_decals.in_set(RenderSet::PrepareResources));
}
}
/// The GPU data structure that stores information about each decal.
#[derive(Clone, Copy, Default, ShaderType, Pod, Zeroable)]
#[repr(C)]
pub struct RenderClusteredDecal {
/// The inverse of the model matrix.
///
/// The shader uses this in order to back-transform world positions into
/// model space.
local_from_world: Mat4,
/// The index of the decal texture in the binding array.
image_index: u32,
/// A custom tag available for application-defined purposes.
tag: u32,
/// Padding.
pad_a: u32,
/// Padding.
pad_b: u32,
}
/// Extracts decals from the main world into the render world.
pub fn extract_decals(
decals: Extract<
Query<(
RenderEntity,
&ClusteredDecal,
&GlobalTransform,
&ViewVisibility,
)>,
>,
mut render_decals: ResMut<RenderClusteredDecals>,
) {
// Clear out the `RenderDecals` in preparation for a new frame.
render_decals.clear();
// Loop over each decal.
for (decal_entity, clustered_decal, global_transform, view_visibility) in &decals {
// If the decal is invisible, skip it.
if !view_visibility.get() {
continue;
}
// Insert or add the image.
let image_index = render_decals.get_or_insert_image(&clustered_decal.image.id());
// Record the decal.
let decal_index = render_decals.decals.len();
render_decals
.entity_to_decal_index
.insert(decal_entity, decal_index);
render_decals.decals.push(RenderClusteredDecal {
local_from_world: global_transform.affine().inverse().into(),
image_index,
tag: clustered_decal.tag,
pad_a: 0,
pad_b: 0,
});
}
}
/// Adds all decals in the scene to the [`GlobalClusterableObjectMeta`] table.
fn prepare_decals(
decals: Query<Entity, With<ClusteredDecal>>,
mut global_clusterable_object_meta: ResMut<GlobalClusterableObjectMeta>,
render_decals: Res<RenderClusteredDecals>,
) {
for decal_entity in &decals {
if let Some(index) = render_decals.entity_to_decal_index.get(&decal_entity) {
global_clusterable_object_meta
.entity_to_index
.insert(decal_entity, *index);
}
}
}
/// Returns the layout for the clustered-decal-related bind group entries for a
/// single view.
pub(crate) fn get_bind_group_layout_entries(
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
) -> Option<[BindGroupLayoutEntryBuilder; 3]> {
// If binding arrays aren't supported on the current platform, we have no
// bind group layout entries.
if !clustered_decals_are_usable(render_device, render_adapter) {
return None;
}
Some([
// `decals`
binding_types::storage_buffer_read_only::<RenderClusteredDecal>(false),
// `decal_textures`
binding_types::texture_2d(TextureSampleType::Float { filterable: true })
.count(NonZero::<u32>::new(MAX_VIEW_DECALS as u32).unwrap()),
// `decal_sampler`
binding_types::sampler(SamplerBindingType::Filtering),
])
}
impl<'a> RenderViewClusteredDecalBindGroupEntries<'a> {
/// Creates and returns the bind group entries for clustered decals for a
/// single view.
pub(crate) fn get(
render_decals: &RenderClusteredDecals,
decals_buffer: &'a DecalsBuffer,
images: &'a RenderAssets<GpuImage>,
fallback_image: &'a FallbackImage,
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
) -> Option<RenderViewClusteredDecalBindGroupEntries<'a>> {
// Skip the entries if decals are unsupported on the current platform.
if !clustered_decals_are_usable(render_device, render_adapter) {
return None;
}
// We use the first sampler among all the images. This assumes that all
// images use the same sampler, which is a documented restriction. If
// there's no sampler, we just use the one from the fallback image.
let sampler = match render_decals
.binding_index_to_textures
.iter()
.filter_map(|image_id| images.get(*image_id))
.next()
{
Some(gpu_image) => &gpu_image.sampler,
None => &fallback_image.d2.sampler,
};
// Gather up the decal textures.
let mut texture_views = vec![];
for image_id in &render_decals.binding_index_to_textures {
match images.get(*image_id) {
None => texture_views.push(&*fallback_image.d2.texture_view),
Some(gpu_image) => texture_views.push(&*gpu_image.texture_view),
}
}
// Pad out the binding array to its maximum length, which is
// required on some platforms.
while texture_views.len() < MAX_VIEW_DECALS {
texture_views.push(&*fallback_image.d2.texture_view);
}
Some(RenderViewClusteredDecalBindGroupEntries {
decals: decals_buffer.buffer()?,
texture_views,
sampler,
})
}
}
impl RenderClusteredDecals {
/// Returns the index of the given image in the decal texture binding array,
/// adding it to the list if necessary.
fn get_or_insert_image(&mut self, image_id: &AssetId<Image>) -> u32 {
*self
.texture_to_binding_index
.entry(*image_id)
.or_insert_with(|| {
let index = self.binding_index_to_textures.len() as u32;
self.binding_index_to_textures.push(*image_id);
index
})
}
}
/// Uploads the list of decals from [`RenderClusteredDecals::decals`] to the
/// GPU.
fn upload_decals(
render_decals: Res<RenderClusteredDecals>,
mut decals_buffer: ResMut<DecalsBuffer>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
) {
decals_buffer.clear();
for &decal in &render_decals.decals {
decals_buffer.push(decal);
}
// Make sure the buffer is non-empty.
// Otherwise there won't be a buffer to bind.
if decals_buffer.is_empty() {
decals_buffer.push(RenderClusteredDecal::default());
}
decals_buffer.write_buffer(&render_device, &render_queue);
}
/// Returns true if clustered decals are usable on the current platform or false
/// otherwise.
///
/// Clustered decals are currently disabled on macOS and iOS due to insufficient
/// texture bindings and limited bindless support in `wgpu`.
pub fn clustered_decals_are_usable(
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
) -> bool {
// Disable binding arrays on Metal. There aren't enough texture bindings available.
// See issue #17553.
// Re-enable this when `wgpu` has first-class bindless.
binding_arrays_are_usable(render_device, render_adapter)
&& cfg!(not(any(target_os = "macos", target_os = "ios")))
}

183
vendor/bevy_pbr/src/decal/clustered.wgsl vendored Normal file
View File

@@ -0,0 +1,183 @@
// Support code for clustered decals.
//
// This module provides an iterator API, which you may wish to use in your own
// shaders if you want clustered decals to provide textures other than the base
// color. The iterator API allows you to iterate over all decals affecting the
// current fragment. Use `clustered_decal_iterator_new()` and
// `clustered_decal_iterator_next()` as follows:
//
// let view_z = get_view_z(vec4(world_position, 1.0));
// let is_orthographic = view_is_orthographic();
//
// let cluster_index =
// clustered_forward::fragment_cluster_index(frag_coord, view_z, is_orthographic);
// var clusterable_object_index_ranges =
// clustered_forward::unpack_clusterable_object_index_ranges(cluster_index);
//
// var iterator = clustered_decal_iterator_new(world_position, &clusterable_object_index_ranges);
// while (clustered_decal_iterator_next(&iterator)) {
// ... sample from the texture at iterator.texture_index at iterator.uv ...
// }
//
// In this way, in conjunction with a custom material, you can provide your own
// texture arrays that mirror `mesh_view_bindings::clustered_decal_textures` in
// order to support decals with normal maps, etc.
//
// Note that the order in which decals are returned is currently unpredictable,
// though generally stable from frame to frame.
#define_import_path bevy_pbr::decal::clustered
#import bevy_pbr::clustered_forward
#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges
#import bevy_pbr::mesh_view_bindings
#import bevy_render::maths
// An object that allows stepping through all clustered decals that affect a
// single fragment.
struct ClusteredDecalIterator {
// Public fields follow:
// The index of the decal texture in the binding array.
texture_index: i32,
// The UV coordinates at which to sample that decal texture.
uv: vec2<f32>,
// A custom tag you can use for your own purposes.
tag: u32,
// Private fields follow:
// The current offset of the index in the `ClusterableObjectIndexRanges` list.
decal_index_offset: i32,
// The end offset of the index in the `ClusterableObjectIndexRanges` list.
end_offset: i32,
// The world-space position of the fragment.
world_position: vec3<f32>,
}
#ifdef CLUSTERED_DECALS_ARE_USABLE
// Creates a new iterator over the decals at the current fragment.
//
// You can retrieve `clusterable_object_index_ranges` as follows:
//
// let view_z = get_view_z(world_position);
// let is_orthographic = view_is_orthographic();
//
// let cluster_index =
// clustered_forward::fragment_cluster_index(frag_coord, view_z, is_orthographic);
// var clusterable_object_index_ranges =
// clustered_forward::unpack_clusterable_object_index_ranges(cluster_index);
fn clustered_decal_iterator_new(
world_position: vec3<f32>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>
) -> ClusteredDecalIterator {
return ClusteredDecalIterator(
-1,
vec2(0.0),
0u,
// We subtract 1 because the first thing `decal_iterator_next` does is
// add 1.
i32((*clusterable_object_index_ranges).first_decal_offset) - 1,
i32((*clusterable_object_index_ranges).last_clusterable_object_index_offset),
world_position,
);
}
// Populates the `iterator.texture_index` and `iterator.uv` fields for the next
// decal overlapping the current world position.
//
// Returns true if another decal was found or false if no more decals were found
// for this position.
fn clustered_decal_iterator_next(iterator: ptr<function, ClusteredDecalIterator>) -> bool {
if ((*iterator).decal_index_offset == (*iterator).end_offset) {
return false;
}
(*iterator).decal_index_offset += 1;
while ((*iterator).decal_index_offset < (*iterator).end_offset) {
let decal_index = i32(clustered_forward::get_clusterable_object_id(
u32((*iterator).decal_index_offset)
));
let decal_space_vector =
(mesh_view_bindings::clustered_decals.decals[decal_index].local_from_world *
vec4((*iterator).world_position, 1.0)).xyz;
if (all(decal_space_vector >= vec3(-0.5)) && all(decal_space_vector <= vec3(0.5))) {
(*iterator).texture_index =
i32(mesh_view_bindings::clustered_decals.decals[decal_index].image_index);
(*iterator).uv = decal_space_vector.xy * vec2(1.0, -1.0) + vec2(0.5);
(*iterator).tag =
mesh_view_bindings::clustered_decals.decals[decal_index].tag;
return true;
}
(*iterator).decal_index_offset += 1;
}
return false;
}
#endif // CLUSTERED_DECALS_ARE_USABLE
// Returns the view-space Z coordinate for the given world position.
fn get_view_z(world_position: vec3<f32>) -> f32 {
return dot(vec4<f32>(
mesh_view_bindings::view.view_from_world[0].z,
mesh_view_bindings::view.view_from_world[1].z,
mesh_view_bindings::view.view_from_world[2].z,
mesh_view_bindings::view.view_from_world[3].z
), vec4(world_position, 1.0));
}
// Returns true if the current view describes an orthographic projection or
// false otherwise.
fn view_is_orthographic() -> bool {
return mesh_view_bindings::view.clip_from_view[3].w == 1.0;
}
// Modifies the base color at the given position to account for decals.
//
// Returns the new base color with decals taken into account. If no decals
// overlap the current world position, returns the supplied base color
// unmodified.
fn apply_decal_base_color(
world_position: vec3<f32>,
frag_coord: vec2<f32>,
initial_base_color: vec4<f32>,
) -> vec4<f32> {
var base_color = initial_base_color;
#ifdef CLUSTERED_DECALS_ARE_USABLE
// Fetch the clusterable object index ranges for this world position.
let view_z = get_view_z(world_position);
let is_orthographic = view_is_orthographic();
let cluster_index =
clustered_forward::fragment_cluster_index(frag_coord, view_z, is_orthographic);
var clusterable_object_index_ranges =
clustered_forward::unpack_clusterable_object_index_ranges(cluster_index);
// Iterate over decals.
var iterator = clustered_decal_iterator_new(world_position, &clusterable_object_index_ranges);
while (clustered_decal_iterator_next(&iterator)) {
// Sample the current decal.
let decal_base_color = textureSampleLevel(
mesh_view_bindings::clustered_decal_textures[iterator.texture_index],
mesh_view_bindings::clustered_decal_sampler,
iterator.uv,
0.0
);
// Blend with the accumulated fragment.
base_color = vec4(
mix(base_color.rgb, decal_base_color.rgb, decal_base_color.a),
base_color.a + decal_base_color.a
);
}
#endif // CLUSTERED_DECALS_ARE_USABLE
return base_color;
}

155
vendor/bevy_pbr/src/decal/forward.rs vendored Normal file
View File

@@ -0,0 +1,155 @@
use crate::{
ExtendedMaterial, Material, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline,
MaterialPlugin, StandardMaterial,
};
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Asset, Assets, Handle};
use bevy_ecs::component::Component;
use bevy_math::{prelude::Rectangle, Quat, Vec2, Vec3};
use bevy_reflect::{Reflect, TypePath};
use bevy_render::render_asset::RenderAssets;
use bevy_render::render_resource::{AsBindGroupShaderType, ShaderType};
use bevy_render::texture::GpuImage;
use bevy_render::{
alpha::AlphaMode,
mesh::{Mesh, Mesh3d, MeshBuilder, MeshVertexBufferLayoutRef, Meshable},
render_resource::{
AsBindGroup, CompareFunction, RenderPipelineDescriptor, Shader,
SpecializedMeshPipelineError,
},
RenderDebugFlags,
};
const FORWARD_DECAL_MESH_HANDLE: Handle<Mesh> =
weak_handle!("afa817f9-1869-4e0c-ac0d-d8cd1552d38a");
const FORWARD_DECAL_SHADER_HANDLE: Handle<Shader> =
weak_handle!("f8dfbef4-d88b-42ae-9af4-d9661e9f1648");
/// Plugin to render [`ForwardDecal`]s.
pub struct ForwardDecalPlugin;
impl Plugin for ForwardDecalPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
FORWARD_DECAL_SHADER_HANDLE,
"forward_decal.wgsl",
Shader::from_wgsl
);
app.register_type::<ForwardDecal>();
app.world_mut().resource_mut::<Assets<Mesh>>().insert(
FORWARD_DECAL_MESH_HANDLE.id(),
Rectangle::from_size(Vec2::ONE)
.mesh()
.build()
.rotated_by(Quat::from_rotation_arc(Vec3::Z, Vec3::Y))
.with_generated_tangents()
.unwrap(),
);
app.add_plugins(MaterialPlugin::<ForwardDecalMaterial<StandardMaterial>> {
prepass_enabled: false,
shadows_enabled: false,
debug_flags: RenderDebugFlags::default(),
..Default::default()
});
}
}
/// A decal that renders via a 1x1 transparent quad mesh, smoothly alpha-blending with the underlying
/// geometry towards the edges.
///
/// Because forward decals are meshes, you can use arbitrary materials to control their appearance.
///
/// # Usage Notes
///
/// * Spawn this component on an entity with a [`crate::MeshMaterial3d`] component holding a [`ForwardDecalMaterial`].
/// * Any camera rendering a forward decal must have the [`bevy_core_pipeline::prepass::DepthPrepass`] component.
/// * Looking at forward decals at a steep angle can cause distortion. This can be mitigated by padding your decal's
/// texture with extra transparent pixels on the edges.
#[derive(Component, Reflect)]
#[require(Mesh3d(FORWARD_DECAL_MESH_HANDLE))]
pub struct ForwardDecal;
/// Type alias for an extended material with a [`ForwardDecalMaterialExt`] extension.
///
/// Make sure to register the [`MaterialPlugin`] for this material in your app setup.
///
/// [`StandardMaterial`] comes with out of the box support for forward decals.
#[expect(type_alias_bounds, reason = "Type alias generics not yet stable")]
pub type ForwardDecalMaterial<B: Material> = ExtendedMaterial<B, ForwardDecalMaterialExt>;
/// Material extension for a [`ForwardDecal`].
///
/// In addition to wrapping your material type with this extension, your shader must use
/// the `bevy_pbr::decal::forward::get_forward_decal_info` function.
///
/// The `FORWARD_DECAL` shader define will be made available to your shader so that you can gate
/// the forward decal code behind an ifdef.
#[derive(Asset, AsBindGroup, TypePath, Clone, Debug)]
#[uniform(200, ForwardDecalMaterialExtUniform)]
pub struct ForwardDecalMaterialExt {
/// Controls the distance threshold for decal blending with surfaces.
///
/// This parameter determines how far away a surface can be before the decal no longer blends
/// with it and instead renders with full opacity.
///
/// Lower values cause the decal to only blend with close surfaces, while higher values allow
/// blending with more distant surfaces.
///
/// Units are in meters.
pub depth_fade_factor: f32,
}
#[derive(Clone, Default, ShaderType)]
pub struct ForwardDecalMaterialExtUniform {
pub inv_depth_fade_factor: f32,
}
impl AsBindGroupShaderType<ForwardDecalMaterialExtUniform> for ForwardDecalMaterialExt {
fn as_bind_group_shader_type(
&self,
_images: &RenderAssets<GpuImage>,
) -> ForwardDecalMaterialExtUniform {
ForwardDecalMaterialExtUniform {
inv_depth_fade_factor: 1.0 / self.depth_fade_factor.max(0.001),
}
}
}
impl MaterialExtension for ForwardDecalMaterialExt {
fn alpha_mode() -> Option<AlphaMode> {
Some(AlphaMode::Blend)
}
fn specialize(
_pipeline: &MaterialExtensionPipeline,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
_key: MaterialExtensionKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
descriptor.depth_stencil.as_mut().unwrap().depth_compare = CompareFunction::Always;
descriptor.vertex.shader_defs.push("FORWARD_DECAL".into());
if let Some(fragment) = &mut descriptor.fragment {
fragment.shader_defs.push("FORWARD_DECAL".into());
}
if let Some(label) = &mut descriptor.label {
*label = format!("forward_decal_{}", label).into();
}
Ok(())
}
}
impl Default for ForwardDecalMaterialExt {
fn default() -> Self {
Self {
depth_fade_factor: 8.0,
}
}
}

View File

@@ -0,0 +1,52 @@
#define_import_path bevy_pbr::decal::forward
#import bevy_pbr::{
forward_io::VertexOutput,
mesh_functions::get_world_from_local,
mesh_view_bindings::view,
pbr_functions::calculate_tbn_mikktspace,
prepass_utils::prepass_depth,
view_transformations::depth_ndc_to_view_z,
}
#import bevy_render::maths::project_onto
@group(2) @binding(200)
var<uniform> inv_depth_fade_factor: f32;
struct ForwardDecalInformation {
world_position: vec4<f32>,
uv: vec2<f32>,
alpha: f32,
}
fn get_forward_decal_info(in: VertexOutput) -> ForwardDecalInformation {
let world_from_local = get_world_from_local(in.instance_index);
let scale = (world_from_local * vec4(1.0, 1.0, 1.0, 0.0)).xyz;
let scaled_tangent = vec4(in.world_tangent.xyz / scale, in.world_tangent.w);
let V = normalize(view.world_position - in.world_position.xyz);
// Transform V from fragment to camera in world space to tangent space.
let TBN = calculate_tbn_mikktspace(in.world_normal, scaled_tangent);
let T = TBN[0];
let B = TBN[1];
let N = TBN[2];
let Vt = vec3(dot(V, T), dot(V, B), dot(V, N));
let frag_depth = depth_ndc_to_view_z(in.position.z);
let depth_pass_depth = depth_ndc_to_view_z(prepass_depth(in.position, 0u));
let diff_depth = frag_depth - depth_pass_depth;
let diff_depth_abs = abs(diff_depth);
// Apply UV parallax
let contact_on_decal = project_onto(V * diff_depth, in.world_normal);
let normal_depth = length(contact_on_decal);
let view_steepness = abs(Vt.z);
let delta_uv = normal_depth * Vt.xy * vec2(1.0, -1.0) / view_steepness;
let uv = in.uv + delta_uv;
let world_position = vec4(in.world_position.xyz + V * diff_depth_abs, in.world_position.w);
let alpha = saturate(1.0 - (normal_depth * inv_depth_fade_factor));
return ForwardDecalInformation(world_position, uv, alpha);
}

11
vendor/bevy_pbr/src/decal/mod.rs vendored Normal file
View File

@@ -0,0 +1,11 @@
//! Decal rendering.
//!
//! Decals are a material that render on top of the surface that they're placed above.
//! They can be used to render signs, paint, snow, impact craters, and other effects on top of surfaces.
// TODO: Once other decal types are added, write a paragraph comparing the different types in the module docs.
pub mod clustered;
mod forward;
pub use forward::*;

View File

@@ -0,0 +1,88 @@
#import bevy_pbr::{
prepass_utils,
pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT,
pbr_functions,
pbr_deferred_functions::pbr_input_from_deferred_gbuffer,
pbr_deferred_types::unpack_unorm3x4_plus_unorm_20_,
lighting,
mesh_view_bindings::deferred_prepass_texture,
}
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
#import bevy_pbr::mesh_view_bindings::screen_space_ambient_occlusion_texture
#import bevy_pbr::ssao_utils::ssao_multibounce
#endif
struct FullscreenVertexOutput {
@builtin(position)
position: vec4<f32>,
@location(0)
uv: vec2<f32>,
};
struct PbrDeferredLightingDepthId {
depth_id: u32, // limited to u8
#ifdef SIXTEEN_BYTE_ALIGNMENT
// WebGL2 structs must be 16 byte aligned.
_webgl2_padding_0: f32,
_webgl2_padding_1: f32,
_webgl2_padding_2: f32,
#endif
}
@group(1) @binding(0)
var<uniform> depth_id: PbrDeferredLightingDepthId;
@vertex
fn vertex(@builtin(vertex_index) vertex_index: u32) -> FullscreenVertexOutput {
// See the full screen vertex shader for explanation above for how this works.
let uv = vec2<f32>(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0;
// Depth is stored as unorm, so we are dividing the u8 depth_id by 255.0 here.
let clip_position = vec4<f32>(uv * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), f32(depth_id.depth_id) / 255.0, 1.0);
return FullscreenVertexOutput(clip_position, uv);
}
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
var frag_coord = vec4(in.position.xy, 0.0, 0.0);
let deferred_data = textureLoad(deferred_prepass_texture, vec2<i32>(frag_coord.xy), 0);
#ifdef WEBGL2
frag_coord.z = unpack_unorm3x4_plus_unorm_20_(deferred_data.b).w;
#else
#ifdef DEPTH_PREPASS
frag_coord.z = prepass_utils::prepass_depth(in.position, 0u);
#endif
#endif
var pbr_input = pbr_input_from_deferred_gbuffer(frag_coord, deferred_data);
var output_color = vec4(0.0);
// NOTE: Unlit bit not set means == 0 is true, so the true case is if lit
if ((pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) {
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2<i32>(in.position.xy), 0i).r;
let ssao_multibounce = ssao_multibounce(ssao, pbr_input.material.base_color.rgb);
pbr_input.diffuse_occlusion = min(pbr_input.diffuse_occlusion, ssao_multibounce);
// Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886"
let NdotV = max(dot(pbr_input.N, pbr_input.V), 0.0001);
var perceptual_roughness: f32 = pbr_input.material.perceptual_roughness;
let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness);
// Use SSAO to estimate the specular occlusion.
// Lagarde and Rousiers 2014, "Moving Frostbite to Physically Based Rendering"
pbr_input.specular_occlusion = saturate(pow(NdotV + ssao, exp2(-16.0 * roughness - 1.0)) - 1.0 + ssao);
#endif // SCREEN_SPACE_AMBIENT_OCCLUSION
output_color = pbr_functions::apply_pbr_lighting(pbr_input);
} else {
output_color = pbr_input.material.base_color;
}
output_color = pbr_functions::main_pass_post_lighting_processing(pbr_input, output_color);
return output_color;
}

560
vendor/bevy_pbr/src/deferred/mod.rs vendored Normal file
View File

@@ -0,0 +1,560 @@
use crate::{
graph::NodePbr, irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight,
MeshPipeline, MeshViewBindGroup, RenderViewLightProbes, ScreenSpaceAmbientOcclusion,
ScreenSpaceReflectionsUniform, ViewEnvironmentMapUniformOffset, ViewLightProbesUniformOffset,
ViewScreenSpaceReflectionsUniformOffset, TONEMAPPING_LUT_SAMPLER_BINDING_INDEX,
TONEMAPPING_LUT_TEXTURE_BINDING_INDEX,
};
use crate::{
DistanceFog, MeshPipelineKey, ShadowFilteringMethod, ViewFogUniformOffset,
ViewLightsUniformOffset,
};
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_core_pipeline::{
core_3d::graph::{Core3d, Node3d},
deferred::{
copy_lighting_id::DeferredLightingIdDepthTexture, DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT,
},
prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass},
tonemapping::{DebandDither, Tonemapping},
};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_image::BevyDefault as _;
use bevy_render::{
extract_component::{
ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin,
},
render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderContext, RenderDevice},
view::{ExtractedView, ViewTarget, ViewUniformOffset},
Render, RenderApp, RenderSet,
};
pub struct DeferredPbrLightingPlugin;
pub const DEFERRED_LIGHTING_SHADER_HANDLE: Handle<Shader> =
weak_handle!("f4295279-8890-4748-b654-ca4d2183df1c");
pub const DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID: u8 = 1;
/// Component with a `depth_id` for specifying which corresponding materials should be rendered by this specific PBR deferred lighting pass.
///
/// Will be automatically added to entities with the [`DeferredPrepass`] component that don't already have a [`PbrDeferredLightingDepthId`].
#[derive(Component, Clone, Copy, ExtractComponent, ShaderType)]
pub struct PbrDeferredLightingDepthId {
depth_id: u32,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_webgl2_padding_0: f32,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_webgl2_padding_1: f32,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_webgl2_padding_2: f32,
}
impl PbrDeferredLightingDepthId {
pub fn new(value: u8) -> PbrDeferredLightingDepthId {
PbrDeferredLightingDepthId {
depth_id: value as u32,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_webgl2_padding_0: 0.0,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_webgl2_padding_1: 0.0,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_webgl2_padding_2: 0.0,
}
}
pub fn set(&mut self, value: u8) {
self.depth_id = value as u32;
}
pub fn get(&self) -> u8 {
self.depth_id as u8
}
}
impl Default for PbrDeferredLightingDepthId {
fn default() -> Self {
PbrDeferredLightingDepthId {
depth_id: DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID as u32,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_webgl2_padding_0: 0.0,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_webgl2_padding_1: 0.0,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_webgl2_padding_2: 0.0,
}
}
}
impl Plugin for DeferredPbrLightingPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
ExtractComponentPlugin::<PbrDeferredLightingDepthId>::default(),
UniformComponentPlugin::<PbrDeferredLightingDepthId>::default(),
))
.add_systems(PostUpdate, insert_deferred_lighting_pass_id_component);
load_internal_asset!(
app,
DEFERRED_LIGHTING_SHADER_HANDLE,
"deferred_lighting.wgsl",
Shader::from_wgsl
);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<DeferredLightingLayout>>()
.add_systems(
Render,
(prepare_deferred_lighting_pipelines.in_set(RenderSet::Prepare),),
)
.add_render_graph_node::<ViewNodeRunner<DeferredOpaquePass3dPbrLightingNode>>(
Core3d,
NodePbr::DeferredLightingPass,
)
.add_render_graph_edges(
Core3d,
(
Node3d::StartMainPass,
NodePbr::DeferredLightingPass,
Node3d::MainOpaquePass,
),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<DeferredLightingLayout>();
}
}
#[derive(Default)]
pub struct DeferredOpaquePass3dPbrLightingNode;
impl ViewNode for DeferredOpaquePass3dPbrLightingNode {
type ViewQuery = (
&'static ViewUniformOffset,
&'static ViewLightsUniformOffset,
&'static ViewFogUniformOffset,
&'static ViewLightProbesUniformOffset,
&'static ViewScreenSpaceReflectionsUniformOffset,
&'static ViewEnvironmentMapUniformOffset,
&'static MeshViewBindGroup,
&'static ViewTarget,
&'static DeferredLightingIdDepthTexture,
&'static DeferredLightingPipeline,
);
fn run(
&self,
_graph_context: &mut RenderGraphContext,
render_context: &mut RenderContext,
(
view_uniform_offset,
view_lights_offset,
view_fog_offset,
view_light_probes_offset,
view_ssr_offset,
view_environment_map_offset,
mesh_view_bind_group,
target,
deferred_lighting_id_depth_texture,
deferred_lighting_pipeline,
): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>();
let deferred_lighting_layout = world.resource::<DeferredLightingLayout>();
let Some(pipeline) =
pipeline_cache.get_render_pipeline(deferred_lighting_pipeline.pipeline_id)
else {
return Ok(());
};
let deferred_lighting_pass_id =
world.resource::<ComponentUniforms<PbrDeferredLightingDepthId>>();
let Some(deferred_lighting_pass_id_binding) =
deferred_lighting_pass_id.uniforms().binding()
else {
return Ok(());
};
let bind_group_1 = render_context.render_device().create_bind_group(
"deferred_lighting_layout_group_1",
&deferred_lighting_layout.bind_group_layout_1,
&BindGroupEntries::single(deferred_lighting_pass_id_binding),
);
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("deferred_lighting_pass"),
color_attachments: &[Some(target.get_color_attachment())],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &deferred_lighting_id_depth_texture.texture.default_view,
depth_ops: Some(Operations {
load: LoadOp::Load,
store: StoreOp::Discard,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(
0,
&mesh_view_bind_group.value,
&[
view_uniform_offset.offset,
view_lights_offset.offset,
view_fog_offset.offset,
**view_light_probes_offset,
**view_ssr_offset,
**view_environment_map_offset,
],
);
render_pass.set_bind_group(1, &bind_group_1, &[]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}
#[derive(Resource)]
pub struct DeferredLightingLayout {
mesh_pipeline: MeshPipeline,
bind_group_layout_1: BindGroupLayout,
}
#[derive(Component)]
pub struct DeferredLightingPipeline {
pub pipeline_id: CachedRenderPipelineId,
}
impl SpecializedRenderPipeline for DeferredLightingLayout {
type Key = MeshPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let mut shader_defs = Vec::new();
// Let the shader code know that it's running in a deferred pipeline.
shader_defs.push("DEFERRED_LIGHTING_PIPELINE".into());
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
shader_defs.push("WEBGL2".into());
if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) {
shader_defs.push("TONEMAP_IN_SHADER".into());
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(),
TONEMAPPING_LUT_TEXTURE_BINDING_INDEX,
));
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(),
TONEMAPPING_LUT_SAMPLER_BINDING_INDEX,
));
let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS);
if method == MeshPipelineKey::TONEMAP_METHOD_NONE {
shader_defs.push("TONEMAP_METHOD_NONE".into());
} else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD {
shader_defs.push("TONEMAP_METHOD_REINHARD".into());
} else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE {
shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into());
} else if method == MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED {
shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into());
} else if method == MeshPipelineKey::TONEMAP_METHOD_AGX {
shader_defs.push("TONEMAP_METHOD_AGX".into());
} else if method == MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM {
shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into());
} else if method == MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC {
shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into());
} else if method == MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE {
shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into());
}
// Debanding is tied to tonemapping in the shader, cannot run without it.
if key.contains(MeshPipelineKey::DEBAND_DITHER) {
shader_defs.push("DEBAND_DITHER".into());
}
}
if key.contains(MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION) {
shader_defs.push("SCREEN_SPACE_AMBIENT_OCCLUSION".into());
}
if key.contains(MeshPipelineKey::ENVIRONMENT_MAP) {
shader_defs.push("ENVIRONMENT_MAP".into());
}
if key.contains(MeshPipelineKey::IRRADIANCE_VOLUME) {
shader_defs.push("IRRADIANCE_VOLUME".into());
}
if key.contains(MeshPipelineKey::NORMAL_PREPASS) {
shader_defs.push("NORMAL_PREPASS".into());
}
if key.contains(MeshPipelineKey::DEPTH_PREPASS) {
shader_defs.push("DEPTH_PREPASS".into());
}
if key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) {
shader_defs.push("MOTION_VECTOR_PREPASS".into());
}
if key.contains(MeshPipelineKey::SCREEN_SPACE_REFLECTIONS) {
shader_defs.push("SCREEN_SPACE_REFLECTIONS".into());
}
if key.contains(MeshPipelineKey::HAS_PREVIOUS_SKIN) {
shader_defs.push("HAS_PREVIOUS_SKIN".into());
}
if key.contains(MeshPipelineKey::HAS_PREVIOUS_MORPH) {
shader_defs.push("HAS_PREVIOUS_MORPH".into());
}
if key.contains(MeshPipelineKey::DISTANCE_FOG) {
shader_defs.push("DISTANCE_FOG".into());
}
// Always true, since we're in the deferred lighting pipeline
shader_defs.push("DEFERRED_PREPASS".into());
let shadow_filter_method =
key.intersection(MeshPipelineKey::SHADOW_FILTER_METHOD_RESERVED_BITS);
if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2 {
shader_defs.push("SHADOW_FILTER_METHOD_HARDWARE_2X2".into());
} else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN {
shader_defs.push("SHADOW_FILTER_METHOD_GAUSSIAN".into());
} else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL {
shader_defs.push("SHADOW_FILTER_METHOD_TEMPORAL".into());
}
if self.mesh_pipeline.binding_arrays_are_usable {
shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into());
shader_defs.push("MULTIPLE_LIGHTMAPS_IN_ARRAY".into());
}
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into());
RenderPipelineDescriptor {
label: Some("deferred_lighting_pipeline".into()),
layout: vec![
self.mesh_pipeline.get_view_layout(key.into()).clone(),
self.bind_group_layout_1.clone(),
],
vertex: VertexState {
shader: DEFERRED_LIGHTING_SHADER_HANDLE,
shader_defs: shader_defs.clone(),
entry_point: "vertex".into(),
buffers: Vec::new(),
},
fragment: Some(FragmentState {
shader: DEFERRED_LIGHTING_SHADER_HANDLE,
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: if key.contains(MeshPipelineKey::HDR) {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT,
depth_write_enabled: false,
depth_compare: CompareFunction::Equal,
stencil: StencilState {
front: StencilFaceState::IGNORE,
back: StencilFaceState::IGNORE,
read_mask: 0,
write_mask: 0,
},
bias: DepthBiasState {
constant: 0,
slope_scale: 0.0,
clamp: 0.0,
},
}),
multisample: MultisampleState::default(),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
}
}
}
impl FromWorld for DeferredLightingLayout {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let layout = render_device.create_bind_group_layout(
"deferred_lighting_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<PbrDeferredLightingDepthId>(false),
),
);
Self {
mesh_pipeline: world.resource::<MeshPipeline>().clone(),
bind_group_layout_1: layout,
}
}
}
pub fn insert_deferred_lighting_pass_id_component(
mut commands: Commands,
views: Query<Entity, (With<DeferredPrepass>, Without<PbrDeferredLightingDepthId>)>,
) {
for entity in views.iter() {
commands
.entity(entity)
.insert(PbrDeferredLightingDepthId::default());
}
}
pub fn prepare_deferred_lighting_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<DeferredLightingLayout>>,
deferred_lighting_layout: Res<DeferredLightingLayout>,
views: Query<(
Entity,
&ExtractedView,
Option<&Tonemapping>,
Option<&DebandDither>,
Option<&ShadowFilteringMethod>,
(
Has<ScreenSpaceAmbientOcclusion>,
Has<ScreenSpaceReflectionsUniform>,
Has<DistanceFog>,
),
(
Has<NormalPrepass>,
Has<DepthPrepass>,
Has<MotionVectorPrepass>,
Has<DeferredPrepass>,
),
Has<RenderViewLightProbes<EnvironmentMapLight>>,
Has<RenderViewLightProbes<IrradianceVolume>>,
)>,
) {
for (
entity,
view,
tonemapping,
dither,
shadow_filter_method,
(ssao, ssr, distance_fog),
(normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass),
has_environment_maps,
has_irradiance_volumes,
) in &views
{
// If there is no deferred prepass, remove the old pipeline if there was
// one. This handles the case in which a view using deferred stops using
// it.
if !deferred_prepass {
commands.entity(entity).remove::<DeferredLightingPipeline>();
continue;
}
let mut view_key = MeshPipelineKey::from_hdr(view.hdr);
if normal_prepass {
view_key |= MeshPipelineKey::NORMAL_PREPASS;
}
if depth_prepass {
view_key |= MeshPipelineKey::DEPTH_PREPASS;
}
if motion_vector_prepass {
view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS;
}
// Always true, since we're in the deferred lighting pipeline
view_key |= MeshPipelineKey::DEFERRED_PREPASS;
if !view.hdr {
if let Some(tonemapping) = tonemapping {
view_key |= MeshPipelineKey::TONEMAP_IN_SHADER;
view_key |= match tonemapping {
Tonemapping::None => MeshPipelineKey::TONEMAP_METHOD_NONE,
Tonemapping::Reinhard => MeshPipelineKey::TONEMAP_METHOD_REINHARD,
Tonemapping::ReinhardLuminance => {
MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE
}
Tonemapping::AcesFitted => MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED,
Tonemapping::AgX => MeshPipelineKey::TONEMAP_METHOD_AGX,
Tonemapping::SomewhatBoringDisplayTransform => {
MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM
}
Tonemapping::TonyMcMapface => MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE,
Tonemapping::BlenderFilmic => MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC,
};
}
if let Some(DebandDither::Enabled) = dither {
view_key |= MeshPipelineKey::DEBAND_DITHER;
}
}
if ssao {
view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION;
}
if ssr {
view_key |= MeshPipelineKey::SCREEN_SPACE_REFLECTIONS;
}
if distance_fog {
view_key |= MeshPipelineKey::DISTANCE_FOG;
}
// We don't need to check to see whether the environment map is loaded
// because [`gather_light_probes`] already checked that for us before
// adding the [`RenderViewEnvironmentMaps`] component.
if has_environment_maps {
view_key |= MeshPipelineKey::ENVIRONMENT_MAP;
}
if has_irradiance_volumes {
view_key |= MeshPipelineKey::IRRADIANCE_VOLUME;
}
match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) {
ShadowFilteringMethod::Hardware2x2 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2;
}
ShadowFilteringMethod::Gaussian => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN;
}
ShadowFilteringMethod::Temporal => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL;
}
}
let pipeline_id =
pipelines.specialize(&pipeline_cache, &deferred_lighting_layout, view_key);
commands
.entity(entity)
.insert(DeferredLightingPipeline { pipeline_id });
}
}

View File

@@ -0,0 +1,153 @@
#define_import_path bevy_pbr::pbr_deferred_functions
#import bevy_pbr::{
pbr_types::{PbrInput, pbr_input_new, STANDARD_MATERIAL_FLAGS_UNLIT_BIT},
pbr_deferred_types as deferred_types,
pbr_functions,
rgb9e5,
mesh_view_bindings::view,
utils::{octahedral_encode, octahedral_decode},
prepass_io::FragmentOutput,
view_transformations::{position_ndc_to_world, frag_coord_to_ndc},
}
#ifdef MESHLET_MESH_MATERIAL_PASS
#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput
#else
#import bevy_pbr::prepass_io::VertexOutput
#endif
#ifdef MOTION_VECTOR_PREPASS
#import bevy_pbr::pbr_prepass_functions::calculate_motion_vector
#endif
// Creates the deferred gbuffer from a PbrInput.
fn deferred_gbuffer_from_pbr_input(in: PbrInput) -> vec4<u32> {
// Only monochrome occlusion supported. May not be worth including at all.
// Some models have baked occlusion, GLTF only supports monochrome.
// Real time occlusion is applied in the deferred lighting pass.
// Deriving luminance via Rec. 709. coefficients
// https://en.wikipedia.org/wiki/Rec._709
let rec_709_coeffs = vec3<f32>(0.2126, 0.7152, 0.0722);
let diffuse_occlusion = dot(in.diffuse_occlusion, rec_709_coeffs);
// Only monochrome specular supported.
let reflectance = dot(in.material.reflectance, rec_709_coeffs);
#ifdef WEBGL2 // More crunched for webgl so we can also fit depth.
var props = deferred_types::pack_unorm3x4_plus_unorm_20_(vec4(
reflectance,
in.material.metallic,
diffuse_occlusion,
in.frag_coord.z));
#else
var props = deferred_types::pack_unorm4x8_(vec4(
reflectance, // could be fewer bits
in.material.metallic, // could be fewer bits
diffuse_occlusion, // is this worth including?
0.0)); // spare
#endif // WEBGL2
let flags = deferred_types::deferred_flags_from_mesh_material_flags(in.flags, in.material.flags);
let octahedral_normal = octahedral_encode(normalize(in.N));
var base_color_srgb = vec3(0.0);
var emissive = in.material.emissive.rgb;
if ((in.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) != 0u) {
// Material is unlit, use emissive component of gbuffer for color data.
// Unlit materials are effectively emissive.
emissive = in.material.base_color.rgb;
} else {
base_color_srgb = pow(in.material.base_color.rgb, vec3(1.0 / 2.2));
}
// Utilize the emissive channel to transmit the lightmap data. To ensure
// it matches the output in forward shading, pre-multiply it with the
// calculated diffuse color.
let base_color = in.material.base_color.rgb;
let metallic = in.material.metallic;
let specular_transmission = in.material.specular_transmission;
let diffuse_transmission = in.material.diffuse_transmission;
let diffuse_color = pbr_functions::calculate_diffuse_color(
base_color,
metallic,
specular_transmission,
diffuse_transmission
);
emissive += in.lightmap_light * diffuse_color * view.exposure;
let deferred = vec4(
deferred_types::pack_unorm4x8_(vec4(base_color_srgb, in.material.perceptual_roughness)),
rgb9e5::vec3_to_rgb9e5_(emissive),
props,
deferred_types::pack_24bit_normal_and_flags(octahedral_normal, flags),
);
return deferred;
}
// Creates a PbrInput from the deferred gbuffer.
fn pbr_input_from_deferred_gbuffer(frag_coord: vec4<f32>, gbuffer: vec4<u32>) -> PbrInput {
var pbr = pbr_input_new();
let flags = deferred_types::unpack_flags(gbuffer.a);
let deferred_flags = deferred_types::mesh_material_flags_from_deferred_flags(flags);
pbr.flags = deferred_flags.x;
pbr.material.flags = deferred_flags.y;
let base_rough = deferred_types::unpack_unorm4x8_(gbuffer.r);
pbr.material.perceptual_roughness = base_rough.a;
let emissive = rgb9e5::rgb9e5_to_vec3_(gbuffer.g);
if ((pbr.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) != 0u) {
pbr.material.base_color = vec4(emissive, 1.0);
pbr.material.emissive = vec4(vec3(0.0), 0.0);
} else {
pbr.material.base_color = vec4(pow(base_rough.rgb, vec3(2.2)), 1.0);
pbr.material.emissive = vec4(emissive, 0.0);
}
#ifdef WEBGL2 // More crunched for webgl so we can also fit depth.
let props = deferred_types::unpack_unorm3x4_plus_unorm_20_(gbuffer.b);
// Bias to 0.5 since that's the value for almost all materials.
pbr.material.reflectance = vec3(saturate(props.r - 0.03333333333));
#else
let props = deferred_types::unpack_unorm4x8_(gbuffer.b);
pbr.material.reflectance = vec3(props.r);
#endif // WEBGL2
pbr.material.metallic = props.g;
pbr.diffuse_occlusion = vec3(props.b);
let octahedral_normal = deferred_types::unpack_24bit_normal(gbuffer.a);
let N = octahedral_decode(octahedral_normal);
let world_position = vec4(position_ndc_to_world(frag_coord_to_ndc(frag_coord)), 1.0);
let is_orthographic = view.clip_from_view[3].w == 1.0;
let V = pbr_functions::calculate_view(world_position, is_orthographic);
pbr.frag_coord = frag_coord;
pbr.world_normal = N;
pbr.world_position = world_position;
pbr.N = N;
pbr.V = V;
pbr.is_orthographic = is_orthographic;
return pbr;
}
#ifdef PREPASS_PIPELINE
fn deferred_output(in: VertexOutput, pbr_input: PbrInput) -> FragmentOutput {
var out: FragmentOutput;
// gbuffer
out.deferred = deferred_gbuffer_from_pbr_input(pbr_input);
// lighting pass id (used to determine which lighting shader to run for the fragment)
out.deferred_lighting_pass_id = pbr_input.material.deferred_lighting_pass_id;
// normal if required
#ifdef NORMAL_PREPASS
out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0);
#endif
// motion vectors if required
#ifdef MOTION_VECTOR_PREPASS
#ifdef MESHLET_MESH_MATERIAL_PASS
out.motion_vector = in.motion_vector;
#else
out.motion_vector = calculate_motion_vector(in.world_position, in.previous_world_position);
#endif
#endif
return out;
}
#endif

View File

@@ -0,0 +1,89 @@
#define_import_path bevy_pbr::pbr_deferred_types
#import bevy_pbr::{
mesh_types::MESH_FLAGS_SHADOW_RECEIVER_BIT,
pbr_types::{STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT, STANDARD_MATERIAL_FLAGS_UNLIT_BIT},
}
// Maximum of 8 bits available
const DEFERRED_FLAGS_UNLIT_BIT: u32 = 1u;
const DEFERRED_FLAGS_FOG_ENABLED_BIT: u32 = 2u;
const DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 4u;
fn deferred_flags_from_mesh_material_flags(mesh_flags: u32, mat_flags: u32) -> u32 {
var flags = 0u;
flags |= u32((mesh_flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u) * DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT;
flags |= u32((mat_flags & STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) * DEFERRED_FLAGS_FOG_ENABLED_BIT;
flags |= u32((mat_flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) != 0u) * DEFERRED_FLAGS_UNLIT_BIT;
return flags;
}
fn mesh_material_flags_from_deferred_flags(deferred_flags: u32) -> vec2<u32> {
var mat_flags = 0u;
var mesh_flags = 0u;
mesh_flags |= u32((deferred_flags & DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u) * MESH_FLAGS_SHADOW_RECEIVER_BIT;
mat_flags |= u32((deferred_flags & DEFERRED_FLAGS_FOG_ENABLED_BIT) != 0u) * STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT;
mat_flags |= u32((deferred_flags & DEFERRED_FLAGS_UNLIT_BIT) != 0u) * STANDARD_MATERIAL_FLAGS_UNLIT_BIT;
return vec2(mesh_flags, mat_flags);
}
const U12MAXF = 4095.0;
const U16MAXF = 65535.0;
const U20MAXF = 1048575.0;
// Storing normals as oct24.
// Flags are stored in the remaining 8 bits.
// https://jcgt.org/published/0003/02/01/paper.pdf
// Could possibly go down to oct20 if the space is needed.
fn pack_24bit_normal_and_flags(octahedral_normal: vec2<f32>, flags: u32) -> u32 {
let unorm1 = u32(saturate(octahedral_normal.x) * U12MAXF + 0.5);
let unorm2 = u32(saturate(octahedral_normal.y) * U12MAXF + 0.5);
return (unorm1 & 0xFFFu) | ((unorm2 & 0xFFFu) << 12u) | ((flags & 0xFFu) << 24u);
}
fn unpack_24bit_normal(packed: u32) -> vec2<f32> {
let unorm1 = packed & 0xFFFu;
let unorm2 = (packed >> 12u) & 0xFFFu;
return vec2(f32(unorm1) / U12MAXF, f32(unorm2) / U12MAXF);
}
fn unpack_flags(packed: u32) -> u32 {
return (packed >> 24u) & 0xFFu;
}
// The builtin one didn't work in webgl.
// "'unpackUnorm4x8' : no matching overloaded function found"
// https://github.com/gfx-rs/naga/issues/2006
fn unpack_unorm4x8_(v: u32) -> vec4<f32> {
return vec4(
f32(v & 0xFFu),
f32((v >> 8u) & 0xFFu),
f32((v >> 16u) & 0xFFu),
f32((v >> 24u) & 0xFFu)
) / 255.0;
}
// 'packUnorm4x8' : no matching overloaded function found
// https://github.com/gfx-rs/naga/issues/2006
fn pack_unorm4x8_(values: vec4<f32>) -> u32 {
let v = vec4<u32>(saturate(values) * 255.0 + 0.5);
return (v.w << 24u) | (v.z << 16u) | (v.y << 8u) | v.x;
}
// Pack 3x 4bit unorm + 1x 20bit
fn pack_unorm3x4_plus_unorm_20_(v: vec4<f32>) -> u32 {
let sm = vec3<u32>(saturate(v.xyz) * 15.0 + 0.5);
let bg = u32(saturate(v.w) * U20MAXF + 0.5);
return (bg << 12u) | (sm.z << 8u) | (sm.y << 4u) | sm.x;
}
// Unpack 3x 4bit unorm + 1x 20bit
fn unpack_unorm3x4_plus_unorm_20_(v: u32) -> vec4<f32> {
return vec4(
f32(v & 0xfu) / 15.0,
f32((v >> 4u) & 0xFu) / 15.0,
f32((v >> 8u) & 0xFu) / 15.0,
f32((v >> 12u) & 0xFFFFFFu) / U20MAXF,
);
}

430
vendor/bevy_pbr/src/extended_material.rs vendored Normal file
View File

@@ -0,0 +1,430 @@
use alloc::borrow::Cow;
use bevy_asset::{Asset, Handle};
use bevy_ecs::system::SystemParamItem;
use bevy_platform::{collections::HashSet, hash::FixedHasher};
use bevy_reflect::{impl_type_path, Reflect};
use bevy_render::{
alpha::AlphaMode,
mesh::MeshVertexBufferLayoutRef,
render_resource::{
AsBindGroup, AsBindGroupError, BindGroupLayout, BindGroupLayoutEntry, BindlessDescriptor,
BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, Shader,
ShaderRef, SpecializedMeshPipelineError, UnpreparedBindGroup,
},
renderer::RenderDevice,
};
use crate::{Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, MeshPipelineKey};
pub struct MaterialExtensionPipeline {
pub mesh_pipeline: MeshPipeline,
pub material_layout: BindGroupLayout,
pub vertex_shader: Option<Handle<Shader>>,
pub fragment_shader: Option<Handle<Shader>>,
pub bindless: bool,
}
pub struct MaterialExtensionKey<E: MaterialExtension> {
pub mesh_key: MeshPipelineKey,
pub bind_group_data: E::Data,
}
/// A subset of the `Material` trait for defining extensions to a base `Material`, such as the builtin `StandardMaterial`.
///
/// A user type implementing the trait should be used as the `E` generic param in an `ExtendedMaterial` struct.
pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized {
/// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the base material mesh vertex shader
/// will be used.
fn vertex_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the base material mesh fragment shader
/// will be used.
fn fragment_shader() -> ShaderRef {
ShaderRef::Default
}
// Returns this materials AlphaMode. If None is returned, the base material alpha mode will be used.
fn alpha_mode() -> Option<AlphaMode> {
None
}
/// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the base material prepass vertex shader
/// will be used.
fn prepass_vertex_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material prepass fragment shader
/// will be used.
fn prepass_fragment_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this material's deferred vertex shader. If [`ShaderRef::Default`] is returned, the base material deferred vertex shader
/// will be used.
fn deferred_vertex_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material deferred fragment shader
/// will be used.
fn deferred_fragment_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this material's [`crate::meshlet::MeshletMesh`] fragment shader. If [`ShaderRef::Default`] is returned,
/// the default meshlet mesh fragment shader will be used.
#[cfg(feature = "meshlet")]
fn meshlet_mesh_fragment_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this material's [`crate::meshlet::MeshletMesh`] prepass fragment shader. If [`ShaderRef::Default`] is returned,
/// the default meshlet mesh prepass fragment shader will be used.
#[cfg(feature = "meshlet")]
fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef {
ShaderRef::Default
}
/// Returns this material's [`crate::meshlet::MeshletMesh`] deferred fragment shader. If [`ShaderRef::Default`] is returned,
/// the default meshlet mesh deferred fragment shader will be used.
#[cfg(feature = "meshlet")]
fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef {
ShaderRef::Default
}
/// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's
/// [`MaterialPipelineKey`] and [`MeshVertexBufferLayoutRef`] as input.
/// Specialization for the base material is applied before this function is called.
#[expect(
unused_variables,
reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion."
)]
#[inline]
fn specialize(
pipeline: &MaterialExtensionPipeline,
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayoutRef,
key: MaterialExtensionKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
Ok(())
}
}
/// A material that extends a base [`Material`] with additional shaders and data.
///
/// The data from both materials will be combined and made available to the shader
/// so that shader functions built for the base material (and referencing the base material
/// bindings) will work as expected, and custom alterations based on custom data can also be used.
///
/// If the extension `E` returns a non-default result from `vertex_shader()` it will be used in place of the base
/// material's vertex shader.
///
/// If the extension `E` returns a non-default result from `fragment_shader()` it will be used in place of the base
/// fragment shader.
///
/// When used with `StandardMaterial` as the base, all the standard material fields are
/// present, so the `pbr_fragment` shader functions can be called from the extension shader (see
/// the `extended_material` example).
#[derive(Asset, Clone, Debug, Reflect)]
#[reflect(type_path = false)]
#[reflect(Clone)]
pub struct ExtendedMaterial<B: Material, E: MaterialExtension> {
pub base: B,
pub extension: E,
}
impl<B, E> Default for ExtendedMaterial<B, E>
where
B: Material + Default,
E: MaterialExtension + Default,
{
fn default() -> Self {
Self {
base: B::default(),
extension: E::default(),
}
}
}
// We don't use the `TypePath` derive here due to a bug where `#[reflect(type_path = false)]`
// causes the `TypePath` derive to not generate an implementation.
impl_type_path!((in bevy_pbr::extended_material) ExtendedMaterial<B: Material, E: MaterialExtension>);
impl<B: Material, E: MaterialExtension> AsBindGroup for ExtendedMaterial<B, E> {
type Data = (<B as AsBindGroup>::Data, <E as AsBindGroup>::Data);
type Param = (<B as AsBindGroup>::Param, <E as AsBindGroup>::Param);
fn bindless_slot_count() -> Option<BindlessSlabResourceLimit> {
// We only enable bindless if both the base material and its extension
// are bindless. If we do enable bindless, we choose the smaller of the
// two slab size limits.
match (B::bindless_slot_count()?, E::bindless_slot_count()?) {
(BindlessSlabResourceLimit::Auto, BindlessSlabResourceLimit::Auto) => {
Some(BindlessSlabResourceLimit::Auto)
}
(BindlessSlabResourceLimit::Auto, BindlessSlabResourceLimit::Custom(limit))
| (BindlessSlabResourceLimit::Custom(limit), BindlessSlabResourceLimit::Auto) => {
Some(BindlessSlabResourceLimit::Custom(limit))
}
(
BindlessSlabResourceLimit::Custom(base_limit),
BindlessSlabResourceLimit::Custom(extended_limit),
) => Some(BindlessSlabResourceLimit::Custom(
base_limit.min(extended_limit),
)),
}
}
fn unprepared_bind_group(
&self,
layout: &BindGroupLayout,
render_device: &RenderDevice,
(base_param, extended_param): &mut SystemParamItem<'_, '_, Self::Param>,
mut force_non_bindless: bool,
) -> Result<UnpreparedBindGroup<Self::Data>, AsBindGroupError> {
force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none();
// add together the bindings of the base material and the user material
let UnpreparedBindGroup {
mut bindings,
data: base_data,
} = B::unprepared_bind_group(
&self.base,
layout,
render_device,
base_param,
force_non_bindless,
)?;
let extended_bindgroup = E::unprepared_bind_group(
&self.extension,
layout,
render_device,
extended_param,
force_non_bindless,
)?;
bindings.extend(extended_bindgroup.bindings.0);
Ok(UnpreparedBindGroup {
bindings,
data: (base_data, extended_bindgroup.data),
})
}
fn bind_group_layout_entries(
render_device: &RenderDevice,
mut force_non_bindless: bool,
) -> Vec<BindGroupLayoutEntry>
where
Self: Sized,
{
force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none();
// Add together the bindings of the standard material and the user
// material, skipping duplicate bindings. Duplicate bindings will occur
// when bindless mode is on, because of the common bindless resource
// arrays, and we need to eliminate the duplicates or `wgpu` will
// complain.
let mut entries = vec![];
let mut seen_bindings = HashSet::<_>::with_hasher(FixedHasher);
for entry in B::bind_group_layout_entries(render_device, force_non_bindless)
.into_iter()
.chain(E::bind_group_layout_entries(render_device, force_non_bindless).into_iter())
{
if seen_bindings.insert(entry.binding) {
entries.push(entry);
}
}
entries
}
fn bindless_descriptor() -> Option<BindlessDescriptor> {
// We're going to combine the two bindless descriptors.
let base_bindless_descriptor = B::bindless_descriptor()?;
let extended_bindless_descriptor = E::bindless_descriptor()?;
// Combining the buffers and index tables is straightforward.
let mut buffers = base_bindless_descriptor.buffers.to_vec();
let mut index_tables = base_bindless_descriptor.index_tables.to_vec();
buffers.extend(extended_bindless_descriptor.buffers.iter().cloned());
index_tables.extend(extended_bindless_descriptor.index_tables.iter().cloned());
// Combining the resources is a little trickier because the resource
// array is indexed by bindless index, so we have to merge the two
// arrays, not just concatenate them.
let max_bindless_index = base_bindless_descriptor
.resources
.len()
.max(extended_bindless_descriptor.resources.len());
let mut resources = Vec::with_capacity(max_bindless_index);
for bindless_index in 0..max_bindless_index {
// In the event of a conflicting bindless index, we choose the
// base's binding.
match base_bindless_descriptor.resources.get(bindless_index) {
None | Some(&BindlessResourceType::None) => resources.push(
extended_bindless_descriptor
.resources
.get(bindless_index)
.copied()
.unwrap_or(BindlessResourceType::None),
),
Some(&resource_type) => resources.push(resource_type),
}
}
Some(BindlessDescriptor {
resources: Cow::Owned(resources),
buffers: Cow::Owned(buffers),
index_tables: Cow::Owned(index_tables),
})
}
}
impl<B: Material, E: MaterialExtension> Material for ExtendedMaterial<B, E> {
fn vertex_shader() -> ShaderRef {
match E::vertex_shader() {
ShaderRef::Default => B::vertex_shader(),
specified => specified,
}
}
fn fragment_shader() -> ShaderRef {
match E::fragment_shader() {
ShaderRef::Default => B::fragment_shader(),
specified => specified,
}
}
fn alpha_mode(&self) -> AlphaMode {
match E::alpha_mode() {
Some(specified) => specified,
None => B::alpha_mode(&self.base),
}
}
fn opaque_render_method(&self) -> crate::OpaqueRendererMethod {
B::opaque_render_method(&self.base)
}
fn depth_bias(&self) -> f32 {
B::depth_bias(&self.base)
}
fn reads_view_transmission_texture(&self) -> bool {
B::reads_view_transmission_texture(&self.base)
}
fn prepass_vertex_shader() -> ShaderRef {
match E::prepass_vertex_shader() {
ShaderRef::Default => B::prepass_vertex_shader(),
specified => specified,
}
}
fn prepass_fragment_shader() -> ShaderRef {
match E::prepass_fragment_shader() {
ShaderRef::Default => B::prepass_fragment_shader(),
specified => specified,
}
}
fn deferred_vertex_shader() -> ShaderRef {
match E::deferred_vertex_shader() {
ShaderRef::Default => B::deferred_vertex_shader(),
specified => specified,
}
}
fn deferred_fragment_shader() -> ShaderRef {
match E::deferred_fragment_shader() {
ShaderRef::Default => B::deferred_fragment_shader(),
specified => specified,
}
}
#[cfg(feature = "meshlet")]
fn meshlet_mesh_fragment_shader() -> ShaderRef {
match E::meshlet_mesh_fragment_shader() {
ShaderRef::Default => B::meshlet_mesh_fragment_shader(),
specified => specified,
}
}
#[cfg(feature = "meshlet")]
fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef {
match E::meshlet_mesh_prepass_fragment_shader() {
ShaderRef::Default => B::meshlet_mesh_prepass_fragment_shader(),
specified => specified,
}
}
#[cfg(feature = "meshlet")]
fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef {
match E::meshlet_mesh_deferred_fragment_shader() {
ShaderRef::Default => B::meshlet_mesh_deferred_fragment_shader(),
specified => specified,
}
}
fn specialize(
pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayoutRef,
key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
// Call the base material's specialize function
let MaterialPipeline::<Self> {
mesh_pipeline,
material_layout,
vertex_shader,
fragment_shader,
bindless,
..
} = pipeline.clone();
let base_pipeline = MaterialPipeline::<B> {
mesh_pipeline,
material_layout,
vertex_shader,
fragment_shader,
bindless,
marker: Default::default(),
};
let base_key = MaterialPipelineKey::<B> {
mesh_key: key.mesh_key,
bind_group_data: key.bind_group_data.0,
};
B::specialize(&base_pipeline, descriptor, layout, base_key)?;
// Call the extended material's specialize function afterwards
let MaterialPipeline::<Self> {
mesh_pipeline,
material_layout,
vertex_shader,
fragment_shader,
bindless,
..
} = pipeline.clone();
E::specialize(
&MaterialExtensionPipeline {
mesh_pipeline,
material_layout,
vertex_shader,
fragment_shader,
bindless,
},
descriptor,
layout,
MaterialExtensionKey {
mesh_key: key.mesh_key,
bind_group_data: key.bind_group_data.1,
},
)
}
}

476
vendor/bevy_pbr/src/fog.rs vendored Normal file
View File

@@ -0,0 +1,476 @@
use bevy_color::{Color, ColorToComponents, LinearRgba};
use bevy_ecs::prelude::*;
use bevy_math::{ops, Vec3};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{extract_component::ExtractComponent, prelude::Camera};
/// Configures the “classic” computer graphics [distance fog](https://en.wikipedia.org/wiki/Distance_fog) effect,
/// in which objects appear progressively more covered in atmospheric haze the further away they are from the camera.
/// Affects meshes rendered via the PBR [`StandardMaterial`](crate::StandardMaterial).
///
/// ## Falloff
///
/// The rate at which fog intensity increases with distance is controlled by the falloff mode.
/// Currently, the following fog falloff modes are supported:
///
/// - [`FogFalloff::Linear`]
/// - [`FogFalloff::Exponential`]
/// - [`FogFalloff::ExponentialSquared`]
/// - [`FogFalloff::Atmospheric`]
///
/// ## Example
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_core_pipeline::prelude::*;
/// # use bevy_pbr::prelude::*;
/// # use bevy_color::Color;
/// # fn system(mut commands: Commands) {
/// commands.spawn((
/// // Setup your camera as usual
/// Camera3d::default(),
/// // Add fog to the same entity
/// DistanceFog {
/// color: Color::WHITE,
/// falloff: FogFalloff::Exponential { density: 1e-3 },
/// ..Default::default()
/// },
/// ));
/// # }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
///
/// ## Material Override
///
/// Once enabled for a specific camera, the fog effect can also be disabled for individual
/// [`StandardMaterial`](crate::StandardMaterial) instances via the `fog_enabled` flag.
#[derive(Debug, Clone, Component, Reflect, ExtractComponent)]
#[extract_component_filter(With<Camera>)]
#[reflect(Component, Default, Debug, Clone)]
pub struct DistanceFog {
/// The color of the fog effect.
///
/// **Tip:** The alpha channel of the color can be used to “modulate” the fog effect without
/// changing the fog falloff mode or parameters.
pub color: Color,
/// Color used to modulate the influence of directional light colors on the
/// fog, where the view direction aligns with each directional light direction,
/// producing a “glow” or light dispersion effect. (e.g. around the sun)
///
/// Use [`Color::NONE`] to disable the effect.
pub directional_light_color: Color,
/// The exponent applied to the directional light alignment calculation.
/// A higher value means a more concentrated “glow”.
pub directional_light_exponent: f32,
/// Determines which falloff mode to use, and its parameters.
pub falloff: FogFalloff,
}
/// Allows switching between different fog falloff modes, and configuring their parameters.
///
/// ## Convenience Methods
///
/// When using non-linear fog modes it can be hard to determine the right parameter values
/// for a given scene.
///
/// For easier artistic control, instead of creating the enum variants directly, you can use the
/// visibility-based convenience methods:
///
/// - For `FogFalloff::Exponential`:
/// - [`FogFalloff::from_visibility()`]
/// - [`FogFalloff::from_visibility_contrast()`]
///
/// - For `FogFalloff::ExponentialSquared`:
/// - [`FogFalloff::from_visibility_squared()`]
/// - [`FogFalloff::from_visibility_contrast_squared()`]
///
/// - For `FogFalloff::Atmospheric`:
/// - [`FogFalloff::from_visibility_color()`]
/// - [`FogFalloff::from_visibility_colors()`]
/// - [`FogFalloff::from_visibility_contrast_color()`]
/// - [`FogFalloff::from_visibility_contrast_colors()`]
#[derive(Debug, Clone, Reflect)]
#[reflect(Clone)]
pub enum FogFalloff {
/// A linear fog falloff that grows in intensity between `start` and `end` distances.
///
/// This falloff mode is simpler to control than other modes, however it can produce results that look “artificial”, depending on the scene.
///
/// ## Formula
///
/// The fog intensity for a given point in the scene is determined by the following formula:
///
/// ```text
/// let fog_intensity = 1.0 - ((end - distance) / (end - start)).clamp(0.0, 1.0);
/// ```
///
/// <svg width="370" height="212" viewBox="0 0 370 212" fill="none">
/// <title>Plot showing how linear fog falloff behaves for start and end values of 0.8 and 2.2, respectively.</title>
/// <path d="M331 151H42V49" stroke="currentColor" stroke-width="2"/>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-family="Inter" font-size="12" letter-spacing="0em"><tspan x="136" y="173.864">1</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-family="Inter" font-size="12" letter-spacing="0em"><tspan x="30" y="53.8636">1</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-family="Inter" font-size="12" letter-spacing="0em"><tspan x="42" y="173.864">0</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-family="Inter" font-size="12" letter-spacing="0em"><tspan x="232" y="173.864">2</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-family="Inter" font-size="12" letter-spacing="0em"><tspan x="332" y="173.864">3</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-family="Inter" font-size="12" letter-spacing="0em"><tspan x="161" y="190.864">distance</tspan></text>
/// <text font-family="sans-serif" transform="translate(10 132) rotate(-90)" fill="currentColor" style="white-space: pre" font-family="Inter" font-size="12" letter-spacing="0em"><tspan x="0" y="11.8636">fog intensity</tspan></text>
/// <path d="M43 150H117.227L263 48H331" stroke="#FF00E5"/>
/// <path d="M118 151V49" stroke="#FF00E5" stroke-dasharray="1 4"/>
/// <path d="M263 151V49" stroke="#FF00E5" stroke-dasharray="1 4"/>
/// <text font-family="sans-serif" fill="#FF00E5" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0em"><tspan x="121" y="58.6364">start</tspan></text>
/// <text font-family="sans-serif" fill="#FF00E5" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0em"><tspan x="267" y="58.6364">end</tspan></text>
/// </svg>
Linear {
/// Distance from the camera where fog is completely transparent, in world units.
start: f32,
/// Distance from the camera where fog is completely opaque, in world units.
end: f32,
},
/// An exponential fog falloff with a given `density`.
///
/// Initially gains intensity quickly with distance, then more slowly. Typically produces more natural results than [`FogFalloff::Linear`],
/// but is a bit harder to control.
///
/// To move the fog “further away”, use lower density values. To move it “closer” use higher density values.
///
/// ## Tips
///
/// - Use the [`FogFalloff::from_visibility()`] convenience method to create an exponential falloff with the proper
/// density for a desired visibility distance in world units;
/// - It's not _unusual_ to have very large or very small values for the density, depending on the scene
/// scale. Typically, for scenes with objects in the scale of thousands of units, you might want density values
/// in the ballpark of `0.001`. Conversely, for really small scale scenes you might want really high values of
/// density;
/// - Combine the `density` parameter with the [`DistanceFog`] `color`'s alpha channel for easier artistic control.
///
/// ## Formula
///
/// The fog intensity for a given point in the scene is determined by the following formula:
///
/// ```text
/// let fog_intensity = 1.0 - 1.0 / (distance * density).exp();
/// ```
///
/// <svg width="370" height="212" viewBox="0 0 370 212" fill="none">
/// <title>Plot showing how exponential fog falloff behaves for different density values</title>
/// <mask id="mask0_3_31" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="42" y="42" width="286" height="108">
/// <rect x="42" y="42" width="286" height="108" fill="#D9D9D9"/>
/// </mask>
/// <g mask="url(#mask0_3_31)">
/// <path d="M42 150C42 150 98.3894 53 254.825 53L662 53" stroke="#FF003D" stroke-width="1"/>
/// <path d="M42 150C42 150 139.499 53 409.981 53L1114 53" stroke="#001AFF" stroke-width="1"/>
/// <path d="M42 150C42 150 206.348 53 662.281 53L1849 53" stroke="#14FF00" stroke-width="1"/>
/// </g>
/// <path d="M331 151H42V49" stroke="currentColor" stroke-width="2"/>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="136" y="173.864">1</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="30" y="53.8636">1</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="42" y="173.864">0</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="232" y="173.864">2</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="332" y="173.864">3</tspan></text>
/// <text font-family="sans-serif" fill="#FF003D" style="white-space: pre" font-size="10" letter-spacing="0em"><tspan x="77" y="64.6364">density = 2</tspan></text>
/// <text font-family="sans-serif" fill="#001AFF" style="white-space: pre" font-size="10" letter-spacing="0em"><tspan x="236" y="76.6364">density = 1</tspan></text>
/// <text font-family="sans-serif" fill="#14FF00" style="white-space: pre" font-size="10" letter-spacing="0em"><tspan x="205" y="115.636">density = 0.5</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="161" y="190.864">distance</tspan></text>
/// <text font-family="sans-serif" transform="translate(10 132) rotate(-90)" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="0" y="11.8636">fog intensity</tspan></text>
/// </svg>
Exponential {
/// Multiplier applied to the world distance (within the exponential fog falloff calculation).
density: f32,
},
/// A squared exponential fog falloff with a given `density`.
///
/// Similar to [`FogFalloff::Exponential`], but grows more slowly in intensity for closer distances
/// before “catching up”.
///
/// To move the fog “further away”, use lower density values. To move it “closer” use higher density values.
///
/// ## Tips
///
/// - Use the [`FogFalloff::from_visibility_squared()`] convenience method to create an exponential squared falloff
/// with the proper density for a desired visibility distance in world units;
/// - Combine the `density` parameter with the [`DistanceFog`] `color`'s alpha channel for easier artistic control.
///
/// ## Formula
///
/// The fog intensity for a given point in the scene is determined by the following formula:
///
/// ```text
/// let fog_intensity = 1.0 - 1.0 / (distance * density).squared().exp();
/// ```
///
/// <svg width="370" height="212" viewBox="0 0 370 212" fill="none">
/// <title>Plot showing how exponential squared fog falloff behaves for different density values</title>
/// <mask id="mask0_1_3" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="42" y="42" width="286" height="108">
/// <rect x="42" y="42" width="286" height="108" fill="#D9D9D9"/>
/// </mask>
/// <g mask="url(#mask0_1_3)">
/// <path d="M42 150C75.4552 150 74.9241 53.1724 166.262 53.1724L404 53.1724" stroke="#FF003D" stroke-width="1"/>
/// <path d="M42 150C107.986 150 106.939 53.1724 287.091 53.1724L756 53.1724" stroke="#001AFF" stroke-width="1"/>
/// <path d="M42 150C166.394 150 164.42 53.1724 504.035 53.1724L1388 53.1724" stroke="#14FF00" stroke-width="1"/>
/// </g>
/// <path d="M331 151H42V49" stroke="currentColor" stroke-width="2"/>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="136" y="173.864">1</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="30" y="53.8636">1</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="42" y="173.864">0</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="232" y="173.864">2</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="332" y="173.864">3</tspan></text>
/// <text font-family="sans-serif" fill="#FF003D" style="white-space: pre" font-size="10" letter-spacing="0em"><tspan x="61" y="54.6364">density = 2</tspan></text>
/// <text font-family="sans-serif" fill="#001AFF" style="white-space: pre" font-size="10" letter-spacing="0em"><tspan x="168" y="84.6364">density = 1</tspan></text>
/// <text font-family="sans-serif" fill="#14FF00" style="white-space: pre" font-size="10" letter-spacing="0em"><tspan x="174" y="121.636">density = 0.5</tspan></text>
/// <text font-family="sans-serif" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="161" y="190.864">distance</tspan></text>
/// <text font-family="sans-serif" transform="translate(10 132) rotate(-90)" fill="currentColor" style="white-space: pre" font-size="12" letter-spacing="0em"><tspan x="0" y="11.8636">fog intensity</tspan></text>
/// </svg>
ExponentialSquared {
/// Multiplier applied to the world distance (within the exponential squared fog falloff calculation).
density: f32,
},
/// A more general form of the [`FogFalloff::Exponential`] mode. The falloff formula is separated into
/// two terms, `extinction` and `inscattering`, for a somewhat simplified atmospheric scattering model.
/// Additionally, individual color channels can have their own density values, resulting in a total of
/// six different configuration parameters.
///
/// ## Tips
///
/// - Use the [`FogFalloff::from_visibility_colors()`] or [`FogFalloff::from_visibility_color()`] convenience methods
/// to create an atmospheric falloff with the proper densities for a desired visibility distance in world units and
/// extinction and inscattering colors;
/// - Combine the atmospheric fog parameters with the [`DistanceFog`] `color`'s alpha channel for easier artistic control.
///
/// ## Formula
///
/// Unlike other modes, atmospheric falloff doesn't use a simple intensity-based blend of fog color with
/// object color. Instead, it calculates per-channel extinction and inscattering factors, which are
/// then used to calculate the final color.
///
/// ```text
/// let extinction_factor = 1.0 - 1.0 / (distance * extinction).exp();
/// let inscattering_factor = 1.0 - 1.0 / (distance * inscattering).exp();
/// let result = input_color * (1.0 - extinction_factor) + fog_color * inscattering_factor;
/// ```
///
/// ## Equivalence to [`FogFalloff::Exponential`]
///
/// For a density value of `D`, the following two falloff modes will produce identical visual results:
///
/// ```
/// # use bevy_pbr::prelude::*;
/// # use bevy_math::prelude::*;
/// # const D: f32 = 0.5;
/// #
/// let exponential = FogFalloff::Exponential {
/// density: D,
/// };
///
/// let atmospheric = FogFalloff::Atmospheric {
/// extinction: Vec3::new(D, D, D),
/// inscattering: Vec3::new(D, D, D),
/// };
/// ```
///
/// **Note:** While the results are identical, [`FogFalloff::Atmospheric`] is computationally more expensive.
Atmospheric {
/// Controls how much light is removed due to atmospheric “extinction”, i.e. loss of light due to
/// photons being absorbed by atmospheric particles.
///
/// Each component can be thought of as an independent per `R`/`G`/`B` channel `density` factor from
/// [`FogFalloff::Exponential`]: Multiplier applied to the world distance (within the fog
/// falloff calculation) for that specific channel.
///
/// **Note:**
/// This value is not a `Color`, since it affects the channels exponentially in a non-intuitive way.
/// For artistic control, use the [`FogFalloff::from_visibility_colors()`] convenience method.
extinction: Vec3,
/// Controls how much light is added due to light scattering from the sun through the atmosphere.
///
/// Each component can be thought of as an independent per `R`/`G`/`B` channel `density` factor from
/// [`FogFalloff::Exponential`]: A multiplier applied to the world distance (within the fog
/// falloff calculation) for that specific channel.
///
/// **Note:**
/// This value is not a `Color`, since it affects the channels exponentially in a non-intuitive way.
/// For artistic control, use the [`FogFalloff::from_visibility_colors()`] convenience method.
inscattering: Vec3,
},
}
impl FogFalloff {
/// Creates a [`FogFalloff::Exponential`] value from the given visibility distance in world units,
/// using the revised Koschmieder contrast threshold, [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`].
pub fn from_visibility(visibility: f32) -> FogFalloff {
FogFalloff::from_visibility_contrast(
visibility,
FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD,
)
}
/// Creates a [`FogFalloff::Exponential`] value from the given visibility distance in world units,
/// and a given contrast threshold in the range of `0.0` to `1.0`.
pub fn from_visibility_contrast(visibility: f32, contrast_threshold: f32) -> FogFalloff {
FogFalloff::Exponential {
density: FogFalloff::koschmieder(visibility, contrast_threshold),
}
}
/// Creates a [`FogFalloff::ExponentialSquared`] value from the given visibility distance in world units,
/// using the revised Koschmieder contrast threshold, [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`].
pub fn from_visibility_squared(visibility: f32) -> FogFalloff {
FogFalloff::from_visibility_contrast_squared(
visibility,
FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD,
)
}
/// Creates a [`FogFalloff::ExponentialSquared`] value from the given visibility distance in world units,
/// and a given contrast threshold in the range of `0.0` to `1.0`.
pub fn from_visibility_contrast_squared(
visibility: f32,
contrast_threshold: f32,
) -> FogFalloff {
FogFalloff::ExponentialSquared {
density: (FogFalloff::koschmieder(visibility, contrast_threshold) / visibility).sqrt(),
}
}
/// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units,
/// and a shared color for both extinction and inscattering, using the revised Koschmieder contrast threshold,
/// [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`].
pub fn from_visibility_color(
visibility: f32,
extinction_inscattering_color: Color,
) -> FogFalloff {
FogFalloff::from_visibility_contrast_colors(
visibility,
FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD,
extinction_inscattering_color,
extinction_inscattering_color,
)
}
/// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units,
/// extinction and inscattering colors, using the revised Koschmieder contrast threshold,
/// [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`].
///
/// ## Tips
/// - Alpha values of the provided colors can modulate the `extinction` and `inscattering` effects;
/// - Using an `extinction_color` of [`Color::WHITE`] or [`Color::NONE`] disables the extinction effect;
/// - Using an `inscattering_color` of [`Color::BLACK`] or [`Color::NONE`] disables the inscattering effect.
pub fn from_visibility_colors(
visibility: f32,
extinction_color: Color,
inscattering_color: Color,
) -> FogFalloff {
FogFalloff::from_visibility_contrast_colors(
visibility,
FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD,
extinction_color,
inscattering_color,
)
}
/// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units,
/// a contrast threshold in the range of `0.0` to `1.0`, and a shared color for both extinction and inscattering.
pub fn from_visibility_contrast_color(
visibility: f32,
contrast_threshold: f32,
extinction_inscattering_color: Color,
) -> FogFalloff {
FogFalloff::from_visibility_contrast_colors(
visibility,
contrast_threshold,
extinction_inscattering_color,
extinction_inscattering_color,
)
}
/// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units,
/// a contrast threshold in the range of `0.0` to `1.0`, extinction and inscattering colors.
///
/// ## Tips
/// - Alpha values of the provided colors can modulate the `extinction` and `inscattering` effects;
/// - Using an `extinction_color` of [`Color::WHITE`] or [`Color::NONE`] disables the extinction effect;
/// - Using an `inscattering_color` of [`Color::BLACK`] or [`Color::NONE`] disables the inscattering effect.
pub fn from_visibility_contrast_colors(
visibility: f32,
contrast_threshold: f32,
extinction_color: Color,
inscattering_color: Color,
) -> FogFalloff {
use core::f32::consts::E;
let [r_e, g_e, b_e, a_e] = LinearRgba::from(extinction_color).to_f32_array();
let [r_i, g_i, b_i, a_i] = LinearRgba::from(inscattering_color).to_f32_array();
FogFalloff::Atmospheric {
extinction: Vec3::new(
// Values are subtracted from 1.0 here to preserve the intuitive/artistic meaning of
// colors, since they're later subtracted. (e.g. by giving a blue extinction color, you
// get blue and _not_ yellow results)
ops::powf(1.0 - r_e, E),
ops::powf(1.0 - g_e, E),
ops::powf(1.0 - b_e, E),
) * FogFalloff::koschmieder(visibility, contrast_threshold)
* ops::powf(a_e, E),
inscattering: Vec3::new(ops::powf(r_i, E), ops::powf(g_i, E), ops::powf(b_i, E))
* FogFalloff::koschmieder(visibility, contrast_threshold)
* ops::powf(a_i, E),
}
}
/// A 2% contrast threshold was originally proposed by Koschmieder, being the
/// minimum visual contrast at which a human observer could detect an object.
/// We use a revised 5% contrast threshold, deemed more realistic for typical human observers.
pub const REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD: f32 = 0.05;
/// Calculates the extinction coefficient β, from V and Cₜ, where:
///
/// - Cₜ is the contrast threshold, in the range of `0.0` to `1.0`
/// - V is the visibility distance in which a perfectly black object is still identifiable
/// against the horizon sky within the contrast threshold
///
/// We start with Koschmieder's equation:
///
/// ```text
/// -ln(Cₜ)
/// V = ─────────
/// β
/// ```
///
/// Multiplying both sides by β/V, that gives us:
///
/// ```text
/// -ln(Cₜ)
/// β = ─────────
/// V
/// ```
///
/// See:
/// - <https://en.wikipedia.org/wiki/Visibility>
/// - <https://www.biral.com/wp-content/uploads/2015/02/Introduction_to_visibility-v2-2.pdf>
pub fn koschmieder(v: f32, c_t: f32) -> f32 {
-ops::ln(c_t) / v
}
}
impl Default for DistanceFog {
fn default() -> Self {
DistanceFog {
color: Color::WHITE,
falloff: FogFalloff::Linear {
start: 0.0,
end: 100.0,
},
directional_light_color: Color::NONE,
directional_light_exponent: 8.0,
}
}
}

533
vendor/bevy_pbr/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,533 @@
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png"
)]
extern crate alloc;
#[cfg(feature = "meshlet")]
mod meshlet;
pub mod wireframe;
/// Experimental features that are not yet finished. Please report any issues you encounter!
///
/// Expect bugs, missing features, compatibility issues, low performance, and/or future breaking changes.
#[cfg(feature = "meshlet")]
pub mod experimental {
/// Render high-poly 3d meshes using an efficient GPU-driven method.
/// See [`MeshletPlugin`](meshlet::MeshletPlugin) and [`MeshletMesh`](meshlet::MeshletMesh) for details.
pub mod meshlet {
pub use crate::meshlet::*;
}
}
mod atmosphere;
mod cluster;
mod components;
pub mod decal;
pub mod deferred;
mod extended_material;
mod fog;
mod light;
mod light_probe;
mod lightmap;
mod material;
mod material_bind_groups;
mod mesh_material;
mod parallax;
mod pbr_material;
mod prepass;
mod render;
mod ssao;
mod ssr;
mod volumetric_fog;
use bevy_color::{Color, LinearRgba};
pub use atmosphere::*;
pub use cluster::*;
pub use components::*;
pub use decal::clustered::ClusteredDecalPlugin;
pub use extended_material::*;
pub use fog::*;
pub use light::*;
pub use light_probe::*;
pub use lightmap::*;
pub use material::*;
pub use material_bind_groups::*;
pub use mesh_material::*;
pub use parallax::*;
pub use pbr_material::*;
pub use prepass::*;
pub use render::*;
pub use ssao::*;
pub use ssr::*;
pub use volumetric_fog::{FogVolume, VolumetricFog, VolumetricFogPlugin, VolumetricLight};
/// The PBR prelude.
///
/// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude {
#[doc(hidden)]
pub use crate::{
fog::{DistanceFog, FogFalloff},
light::{light_consts, AmbientLight, DirectionalLight, PointLight, SpotLight},
light_probe::{environment_map::EnvironmentMapLight, LightProbe},
material::{Material, MaterialPlugin},
mesh_material::MeshMaterial3d,
parallax::ParallaxMappingMethod,
pbr_material::StandardMaterial,
ssao::ScreenSpaceAmbientOcclusionPlugin,
};
}
pub mod graph {
use bevy_render::render_graph::RenderLabel;
/// Render graph nodes specific to 3D PBR rendering.
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
pub enum NodePbr {
/// Label for the shadow pass node that draws meshes that were visible
/// from the light last frame.
EarlyShadowPass,
/// Label for the shadow pass node that draws meshes that became visible
/// from the light this frame.
LateShadowPass,
/// Label for the screen space ambient occlusion render node.
ScreenSpaceAmbientOcclusion,
DeferredLightingPass,
/// Label for the volumetric lighting pass.
VolumetricFog,
/// Label for the shader that transforms and culls meshes that were
/// visible last frame.
EarlyGpuPreprocess,
/// Label for the shader that transforms and culls meshes that became
/// visible this frame.
LateGpuPreprocess,
/// Label for the screen space reflections pass.
ScreenSpaceReflections,
/// Label for the node that builds indirect draw parameters for meshes
/// that were visible last frame.
EarlyPrepassBuildIndirectParameters,
/// Label for the node that builds indirect draw parameters for meshes
/// that became visible this frame.
LatePrepassBuildIndirectParameters,
/// Label for the node that builds indirect draw parameters for the main
/// rendering pass, containing all meshes that are visible this frame.
MainBuildIndirectParameters,
ClearIndirectParametersMetadata,
}
}
use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr};
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, weak_handle, AssetApp, Assets, Handle};
use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d};
use bevy_ecs::prelude::*;
use bevy_image::Image;
use bevy_render::{
alpha::AlphaMode,
camera::{sort_cameras, CameraUpdateSystem, Projection},
extract_component::ExtractComponentPlugin,
extract_resource::ExtractResourcePlugin,
render_graph::RenderGraph,
render_resource::Shader,
sync_component::SyncComponentPlugin,
view::VisibilitySystems,
ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSet,
};
use bevy_transform::TransformSystem;
pub const PBR_TYPES_SHADER_HANDLE: Handle<Shader> =
weak_handle!("b0330585-2335-4268-9032-a6c4c2d932f6");
pub const PBR_BINDINGS_SHADER_HANDLE: Handle<Shader> =
weak_handle!("13834c18-c7ec-4c4b-bbbd-432c3ba4cace");
pub const UTILS_HANDLE: Handle<Shader> = weak_handle!("0a32978f-2744-4608-98b6-4c3000a0638d");
pub const CLUSTERED_FORWARD_HANDLE: Handle<Shader> =
weak_handle!("f8e3b4c6-60b7-4b23-8b2e-a6b27bb4ddce");
pub const PBR_LIGHTING_HANDLE: Handle<Shader> =
weak_handle!("de0cf697-2876-49a0-aa0f-f015216f70c2");
pub const PBR_TRANSMISSION_HANDLE: Handle<Shader> =
weak_handle!("22482185-36bb-4c16-9b93-a20e6d4a2725");
pub const SHADOWS_HANDLE: Handle<Shader> = weak_handle!("ff758c5a-3927-4a15-94c3-3fbdfc362590");
pub const SHADOW_SAMPLING_HANDLE: Handle<Shader> =
weak_handle!("f6bf5843-54bc-4e39-bd9d-56bfcd77b033");
pub const PBR_FRAGMENT_HANDLE: Handle<Shader> =
weak_handle!("1bd3c10d-851b-400c-934a-db489d99cc50");
pub const PBR_SHADER_HANDLE: Handle<Shader> = weak_handle!("0eba65ed-3e5b-4752-93ed-e8097e7b0c84");
pub const PBR_PREPASS_SHADER_HANDLE: Handle<Shader> =
weak_handle!("9afeaeab-7c45-43ce-b322-4b97799eaeb9");
pub const PBR_FUNCTIONS_HANDLE: Handle<Shader> =
weak_handle!("815b8618-f557-4a96-91a5-a2fb7e249fb0");
pub const PBR_AMBIENT_HANDLE: Handle<Shader> = weak_handle!("4a90b95b-112a-4a10-9145-7590d6f14260");
pub const PARALLAX_MAPPING_SHADER_HANDLE: Handle<Shader> =
weak_handle!("6cf57d9f-222a-429a-bba4-55ba9586e1d4");
pub const VIEW_TRANSFORMATIONS_SHADER_HANDLE: Handle<Shader> =
weak_handle!("ec047703-cde3-4876-94df-fed121544abb");
pub const PBR_PREPASS_FUNCTIONS_SHADER_HANDLE: Handle<Shader> =
weak_handle!("77b1bd3a-877c-4b2c-981b-b9c68d1b774a");
pub const PBR_DEFERRED_TYPES_HANDLE: Handle<Shader> =
weak_handle!("43060da7-a717-4240-80a8-dbddd92bd25d");
pub const PBR_DEFERRED_FUNCTIONS_HANDLE: Handle<Shader> =
weak_handle!("9dc46746-c51d-45e3-a321-6a50c3963420");
pub const RGB9E5_FUNCTIONS_HANDLE: Handle<Shader> =
weak_handle!("90c19aa3-6a11-4252-8586-d9299352e94f");
const MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE: Handle<Shader> =
weak_handle!("69187376-3dea-4d0f-b3f5-185bde63d6a2");
pub const TONEMAPPING_LUT_TEXTURE_BINDING_INDEX: u32 = 26;
pub const TONEMAPPING_LUT_SAMPLER_BINDING_INDEX: u32 = 27;
/// Sets up the entire PBR infrastructure of bevy.
pub struct PbrPlugin {
/// Controls if the prepass is enabled for the [`StandardMaterial`].
/// For more information about what a prepass is, see the [`bevy_core_pipeline::prepass`] docs.
pub prepass_enabled: bool,
/// Controls if [`DeferredPbrLightingPlugin`] is added.
pub add_default_deferred_lighting_plugin: bool,
/// Controls if GPU [`MeshUniform`] building is enabled.
///
/// This requires compute shader support and so will be forcibly disabled if
/// the platform doesn't support those.
pub use_gpu_instance_buffer_builder: bool,
/// Debugging flags that can optionally be set when constructing the renderer.
pub debug_flags: RenderDebugFlags,
}
impl Default for PbrPlugin {
fn default() -> Self {
Self {
prepass_enabled: true,
add_default_deferred_lighting_plugin: true,
use_gpu_instance_buffer_builder: true,
debug_flags: RenderDebugFlags::default(),
}
}
}
impl Plugin for PbrPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
PBR_TYPES_SHADER_HANDLE,
"render/pbr_types.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
PBR_BINDINGS_SHADER_HANDLE,
"render/pbr_bindings.wgsl",
Shader::from_wgsl
);
load_internal_asset!(app, UTILS_HANDLE, "render/utils.wgsl", Shader::from_wgsl);
load_internal_asset!(
app,
CLUSTERED_FORWARD_HANDLE,
"render/clustered_forward.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
PBR_LIGHTING_HANDLE,
"render/pbr_lighting.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
PBR_TRANSMISSION_HANDLE,
"render/pbr_transmission.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
SHADOWS_HANDLE,
"render/shadows.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
PBR_DEFERRED_TYPES_HANDLE,
"deferred/pbr_deferred_types.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
PBR_DEFERRED_FUNCTIONS_HANDLE,
"deferred/pbr_deferred_functions.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
SHADOW_SAMPLING_HANDLE,
"render/shadow_sampling.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
PBR_FUNCTIONS_HANDLE,
"render/pbr_functions.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
RGB9E5_FUNCTIONS_HANDLE,
"render/rgb9e5.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
PBR_AMBIENT_HANDLE,
"render/pbr_ambient.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
PBR_FRAGMENT_HANDLE,
"render/pbr_fragment.wgsl",
Shader::from_wgsl
);
load_internal_asset!(app, PBR_SHADER_HANDLE, "render/pbr.wgsl", Shader::from_wgsl);
load_internal_asset!(
app,
PBR_PREPASS_FUNCTIONS_SHADER_HANDLE,
"render/pbr_prepass_functions.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
PBR_PREPASS_SHADER_HANDLE,
"render/pbr_prepass.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
PARALLAX_MAPPING_SHADER_HANDLE,
"render/parallax_mapping.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
VIEW_TRANSFORMATIONS_SHADER_HANDLE,
"render/view_transformations.wgsl",
Shader::from_wgsl
);
// Setup dummy shaders for when MeshletPlugin is not used to prevent shader import errors.
load_internal_asset!(
app,
MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE,
"meshlet/dummy_visibility_buffer_resolve.wgsl",
Shader::from_wgsl
);
app.register_asset_reflect::<StandardMaterial>()
.register_type::<AmbientLight>()
.register_type::<CascadeShadowConfig>()
.register_type::<Cascades>()
.register_type::<CascadesVisibleEntities>()
.register_type::<VisibleMeshEntities>()
.register_type::<ClusterConfig>()
.register_type::<CubemapVisibleEntities>()
.register_type::<DirectionalLight>()
.register_type::<DirectionalLightShadowMap>()
.register_type::<NotShadowCaster>()
.register_type::<NotShadowReceiver>()
.register_type::<PointLight>()
.register_type::<PointLightShadowMap>()
.register_type::<SpotLight>()
.register_type::<ShadowFilteringMethod>()
.init_resource::<AmbientLight>()
.init_resource::<GlobalVisibleClusterableObjects>()
.init_resource::<DirectionalLightShadowMap>()
.init_resource::<PointLightShadowMap>()
.register_type::<DefaultOpaqueRendererMethod>()
.init_resource::<DefaultOpaqueRendererMethod>()
.add_plugins((
MeshRenderPlugin {
use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder,
debug_flags: self.debug_flags,
},
MaterialPlugin::<StandardMaterial> {
prepass_enabled: self.prepass_enabled,
debug_flags: self.debug_flags,
..Default::default()
},
ScreenSpaceAmbientOcclusionPlugin,
ExtractResourcePlugin::<AmbientLight>::default(),
FogPlugin,
ExtractResourcePlugin::<DefaultOpaqueRendererMethod>::default(),
ExtractComponentPlugin::<ShadowFilteringMethod>::default(),
LightmapPlugin,
LightProbePlugin,
PbrProjectionPlugin,
GpuMeshPreprocessPlugin {
use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder,
},
VolumetricFogPlugin,
ScreenSpaceReflectionsPlugin,
ClusteredDecalPlugin,
))
.add_plugins((
decal::ForwardDecalPlugin,
SyncComponentPlugin::<DirectionalLight>::default(),
SyncComponentPlugin::<PointLight>::default(),
SyncComponentPlugin::<SpotLight>::default(),
ExtractComponentPlugin::<AmbientLight>::default(),
))
.add_plugins(AtmospherePlugin)
.configure_sets(
PostUpdate,
(
SimulationLightSystems::AddClusters,
SimulationLightSystems::AssignLightsToClusters,
)
.chain(),
)
.configure_sets(
PostUpdate,
SimulationLightSystems::UpdateDirectionalLightCascades
.ambiguous_with(SimulationLightSystems::UpdateDirectionalLightCascades),
)
.configure_sets(
PostUpdate,
SimulationLightSystems::CheckLightVisibility
.ambiguous_with(SimulationLightSystems::CheckLightVisibility),
)
.add_systems(
PostUpdate,
(
add_clusters
.in_set(SimulationLightSystems::AddClusters)
.after(CameraUpdateSystem),
assign_objects_to_clusters
.in_set(SimulationLightSystems::AssignLightsToClusters)
.after(TransformSystem::TransformPropagate)
.after(VisibilitySystems::CheckVisibility)
.after(CameraUpdateSystem),
clear_directional_light_cascades
.in_set(SimulationLightSystems::UpdateDirectionalLightCascades)
.after(TransformSystem::TransformPropagate)
.after(CameraUpdateSystem),
update_directional_light_frusta
.in_set(SimulationLightSystems::UpdateLightFrusta)
// This must run after CheckVisibility because it relies on `ViewVisibility`
.after(VisibilitySystems::CheckVisibility)
.after(TransformSystem::TransformPropagate)
.after(SimulationLightSystems::UpdateDirectionalLightCascades)
// We assume that no entity will be both a directional light and a spot light,
// so these systems will run independently of one another.
// FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481.
.ambiguous_with(update_spot_light_frusta),
update_point_light_frusta
.in_set(SimulationLightSystems::UpdateLightFrusta)
.after(TransformSystem::TransformPropagate)
.after(SimulationLightSystems::AssignLightsToClusters),
update_spot_light_frusta
.in_set(SimulationLightSystems::UpdateLightFrusta)
.after(TransformSystem::TransformPropagate)
.after(SimulationLightSystems::AssignLightsToClusters),
(
check_dir_light_mesh_visibility,
check_point_light_mesh_visibility,
)
.in_set(SimulationLightSystems::CheckLightVisibility)
.after(VisibilitySystems::CalculateBounds)
.after(TransformSystem::TransformPropagate)
.after(SimulationLightSystems::UpdateLightFrusta)
// NOTE: This MUST be scheduled AFTER the core renderer visibility check
// because that resets entity `ViewVisibility` for the first view
// which would override any results from this otherwise
.after(VisibilitySystems::CheckVisibility)
.before(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible),
),
);
if self.add_default_deferred_lighting_plugin {
app.add_plugins(DeferredPbrLightingPlugin);
}
// Initialize the default material handle.
app.world_mut()
.resource_mut::<Assets<StandardMaterial>>()
.insert(
&Handle::<StandardMaterial>::default(),
StandardMaterial {
base_color: Color::srgb(1.0, 0.0, 0.5),
..Default::default()
},
);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
// Extract the required data from the main world
render_app
.add_systems(
ExtractSchedule,
(
extract_clusters,
extract_lights,
late_sweep_material_instances,
),
)
.add_systems(
Render,
(
prepare_lights
.in_set(RenderSet::ManageViews)
.after(sort_cameras),
prepare_clusters.in_set(RenderSet::PrepareResources),
),
)
.init_resource::<LightMeta>()
.init_resource::<RenderMaterialBindings>();
render_app.world_mut().add_observer(add_light_view_entities);
render_app
.world_mut()
.add_observer(remove_light_view_entities);
render_app.world_mut().add_observer(extracted_light_removed);
let early_shadow_pass_node = EarlyShadowPassNode::from_world(render_app.world_mut());
let late_shadow_pass_node = LateShadowPassNode::from_world(render_app.world_mut());
let mut graph = render_app.world_mut().resource_mut::<RenderGraph>();
let draw_3d_graph = graph.get_sub_graph_mut(Core3d).unwrap();
draw_3d_graph.add_node(NodePbr::EarlyShadowPass, early_shadow_pass_node);
draw_3d_graph.add_node(NodePbr::LateShadowPass, late_shadow_pass_node);
draw_3d_graph.add_node_edges((
NodePbr::EarlyShadowPass,
NodePbr::LateShadowPass,
Node3d::StartMainPass,
));
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
// Extract the required data from the main world
render_app
.init_resource::<ShadowSamplers>()
.init_resource::<GlobalClusterableObjectMeta>()
.init_resource::<FallbackBindlessResources>();
}
}
/// Camera projection PBR functionality.
#[derive(Default)]
pub struct PbrProjectionPlugin;
impl Plugin for PbrProjectionPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PostUpdate,
build_directional_light_cascades
.in_set(SimulationLightSystems::UpdateDirectionalLightCascades)
.after(clear_directional_light_cascades),
);
}
}

View File

@@ -0,0 +1,57 @@
use super::*;
/// An ambient light, which lights the entire scene equally.
///
/// This resource is inserted by the [`PbrPlugin`] and by default it is set to a low ambient light.
///
/// It can also be added to a camera to override the resource (or default) ambient for that camera only.
///
/// # Examples
///
/// Make ambient light slightly brighter:
///
/// ```
/// # use bevy_ecs::system::ResMut;
/// # use bevy_pbr::AmbientLight;
/// fn setup_ambient_light(mut ambient_light: ResMut<AmbientLight>) {
/// ambient_light.brightness = 100.0;
/// }
/// ```
#[derive(Resource, Component, Clone, Debug, ExtractResource, ExtractComponent, Reflect)]
#[reflect(Resource, Component, Debug, Default, Clone)]
#[require(Camera)]
pub struct AmbientLight {
pub color: Color,
/// A direct scale factor multiplied with `color` before being passed to the shader.
///
/// After applying this multiplier, the resulting value should be in units of [cd/m^2].
///
/// [cd/m^2]: https://en.wikipedia.org/wiki/Candela_per_square_metre
pub brightness: f32,
/// Whether this ambient light has an effect on meshes with lightmaps.
///
/// Set this to false if your lightmap baking tool bakes the ambient light
/// into the lightmaps, to avoid rendering that light twice.
///
/// By default, this is set to true.
pub affects_lightmapped_meshes: bool,
}
impl Default for AmbientLight {
fn default() -> Self {
Self {
color: Color::WHITE,
brightness: 80.0,
affects_lightmapped_meshes: true,
}
}
}
impl AmbientLight {
pub const NONE: AmbientLight = AmbientLight {
color: Color::WHITE,
brightness: 0.0,
affects_lightmapped_meshes: true,
};
}

View File

@@ -0,0 +1,143 @@
use bevy_render::view::{self, Visibility};
use super::*;
/// A Directional light.
///
/// Directional lights don't exist in reality but they are a good
/// approximation for light sources VERY far away, like the sun or
/// the moon.
///
/// The light shines along the forward direction of the entity's transform. With a default transform
/// this would be along the negative-Z axis.
///
/// Valid values for `illuminance` are:
///
/// | Illuminance (lux) | Surfaces illuminated by |
/// |-------------------|------------------------------------------------|
/// | 0.0001 | Moonless, overcast night sky (starlight) |
/// | 0.002 | Moonless clear night sky with airglow |
/// | 0.050.3 | Full moon on a clear night |
/// | 3.4 | Dark limit of civil twilight under a clear sky |
/// | 2050 | Public areas with dark surroundings |
/// | 50 | Family living room lights |
/// | 80 | Office building hallway/toilet lighting |
/// | 100 | Very dark overcast day |
/// | 150 | Train station platforms |
/// | 320500 | Office lighting |
/// | 400 | Sunrise or sunset on a clear day. |
/// | 1000 | Overcast day; typical TV studio lighting |
/// | 10,00025,000 | Full daylight (not direct sun) |
/// | 32,000100,000 | Direct sunlight |
///
/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lux)
///
/// ## Shadows
///
/// To enable shadows, set the `shadows_enabled` property to `true`.
///
/// Shadows are produced via [cascaded shadow maps](https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf).
///
/// To modify the cascade setup, such as the number of cascades or the maximum shadow distance,
/// change the [`CascadeShadowConfig`] component of the entity with the [`DirectionalLight`].
///
/// To control the resolution of the shadow maps, use the [`DirectionalLightShadowMap`] resource.
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
#[require(
Cascades,
CascadesFrusta,
CascadeShadowConfig,
CascadesVisibleEntities,
Transform,
Visibility,
VisibilityClass
)]
#[component(on_add = view::add_visibility_class::<LightVisibilityClass>)]
pub struct DirectionalLight {
/// The color of the light.
///
/// By default, this is white.
pub color: Color,
/// Illuminance in lux (lumens per square meter), representing the amount of
/// light projected onto surfaces by this light source. Lux is used here
/// instead of lumens because a directional light illuminates all surfaces
/// more-or-less the same way (depending on the angle of incidence). Lumens
/// can only be specified for light sources which emit light from a specific
/// area.
pub illuminance: f32,
/// Whether this light casts shadows.
///
/// Note that shadows are rather expensive and become more so with every
/// light that casts them. In general, it's best to aggressively limit the
/// number of lights with shadows enabled to one or two at most.
pub shadows_enabled: bool,
/// Whether soft shadows are enabled, and if so, the size of the light.
///
/// Soft shadows, also known as *percentage-closer soft shadows* or PCSS,
/// cause shadows to become blurrier (i.e. their penumbra increases in
/// radius) as they extend away from objects. The blurriness of the shadow
/// depends on the size of the light; larger lights result in larger
/// penumbras and therefore blurrier shadows.
///
/// Currently, soft shadows are rather noisy if not using the temporal mode.
/// If you enable soft shadows, consider choosing
/// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing
/// (TAA) to smooth the noise out over time.
///
/// Note that soft shadows are significantly more expensive to render than
/// hard shadows.
#[cfg(feature = "experimental_pbr_pcss")]
pub soft_shadow_size: Option<f32>,
/// Whether this directional light contributes diffuse lighting to meshes
/// with lightmaps.
///
/// Set this to false if your lightmap baking tool bakes the direct diffuse
/// light from this directional light into the lightmaps in order to avoid
/// counting the radiance from this light twice. Note that the specular
/// portion of the light is always considered, because Bevy currently has no
/// means to bake specular light.
///
/// By default, this is set to true.
pub affects_lightmapped_mesh_diffuse: bool,
/// A value that adjusts the tradeoff between self-shadowing artifacts and
/// proximity of shadows to their casters.
///
/// This value frequently must be tuned to the specific scene; this is
/// normal and a well-known part of the shadow mapping workflow. If set too
/// low, unsightly shadow patterns appear on objects not in shadow as
/// objects incorrectly cast shadows on themselves, known as *shadow acne*.
/// If set too high, shadows detach from the objects casting them and seem
/// to "fly" off the objects, known as *Peter Panning*.
pub shadow_depth_bias: f32,
/// A bias applied along the direction of the fragment's surface normal. It
/// is scaled to the shadow map's texel size so that it is automatically
/// adjusted to the orthographic projection.
pub shadow_normal_bias: f32,
}
impl Default for DirectionalLight {
fn default() -> Self {
DirectionalLight {
color: Color::WHITE,
illuminance: light_consts::lux::AMBIENT_DAYLIGHT,
shadows_enabled: false,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
affects_lightmapped_mesh_diffuse: true,
#[cfg(feature = "experimental_pbr_pcss")]
soft_shadow_size: None,
}
}
}
impl DirectionalLight {
pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02;
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8;
}

1110
vendor/bevy_pbr/src/light/mod.rs vendored Normal file

File diff suppressed because it is too large Load Diff

138
vendor/bevy_pbr/src/light/point_light.rs vendored Normal file
View File

@@ -0,0 +1,138 @@
use bevy_render::view::{self, Visibility};
use super::*;
/// A light that emits light in all directions from a central point.
///
/// Real-world values for `intensity` (luminous power in lumens) based on the electrical power
/// consumption of the type of real-world light are:
///
/// | Luminous Power (lumen) (i.e. the intensity member) | Incandescent non-halogen (Watts) | Incandescent halogen (Watts) | Compact fluorescent (Watts) | LED (Watts) |
/// |------|-----|----|--------|-------|
/// | 200 | 25 | | 3-5 | 3 |
/// | 450 | 40 | 29 | 9-11 | 5-8 |
/// | 800 | 60 | | 13-15 | 8-12 |
/// | 1100 | 75 | 53 | 18-20 | 10-16 |
/// | 1600 | 100 | 72 | 24-28 | 14-17 |
/// | 2400 | 150 | | 30-52 | 24-30 |
/// | 3100 | 200 | | 49-75 | 32 |
/// | 4000 | 300 | | 75-100 | 40.5 |
///
/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lumen_(unit)#Lighting)
///
/// ## Shadows
///
/// To enable shadows, set the `shadows_enabled` property to `true`.
///
/// To control the resolution of the shadow maps, use the [`PointLightShadowMap`] resource.
#[derive(Component, Debug, Clone, Copy, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
#[require(
CubemapFrusta,
CubemapVisibleEntities,
Transform,
Visibility,
VisibilityClass
)]
#[component(on_add = view::add_visibility_class::<LightVisibilityClass>)]
pub struct PointLight {
/// The color of this light source.
pub color: Color,
/// Luminous power in lumens, representing the amount of light emitted by this source in all directions.
pub intensity: f32,
/// Cut-off for the light's area-of-effect. Fragments outside this range will not be affected by
/// this light at all, so it's important to tune this together with `intensity` to prevent hard
/// lighting cut-offs.
pub range: f32,
/// Simulates a light source coming from a spherical volume with the given
/// radius.
///
/// This affects the size of specular highlights created by this light, as
/// well as the soft shadow penumbra size. Because of this, large values may
/// not produce the intended result -- for example, light radius does not
/// affect shadow softness or diffuse lighting.
pub radius: f32,
/// Whether this light casts shadows.
pub shadows_enabled: bool,
/// Whether soft shadows are enabled.
///
/// Soft shadows, also known as *percentage-closer soft shadows* or PCSS,
/// cause shadows to become blurrier (i.e. their penumbra increases in
/// radius) as they extend away from objects. The blurriness of the shadow
/// depends on the [`PointLight::radius`] of the light; larger lights result
/// in larger penumbras and therefore blurrier shadows.
///
/// Currently, soft shadows are rather noisy if not using the temporal mode.
/// If you enable soft shadows, consider choosing
/// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing
/// (TAA) to smooth the noise out over time.
///
/// Note that soft shadows are significantly more expensive to render than
/// hard shadows.
#[cfg(feature = "experimental_pbr_pcss")]
pub soft_shadows_enabled: bool,
/// Whether this point light contributes diffuse lighting to meshes with
/// lightmaps.
///
/// Set this to false if your lightmap baking tool bakes the direct diffuse
/// light from this point light into the lightmaps in order to avoid
/// counting the radiance from this light twice. Note that the specular
/// portion of the light is always considered, because Bevy currently has no
/// means to bake specular light.
///
/// By default, this is set to true.
pub affects_lightmapped_mesh_diffuse: bool,
/// A bias used when sampling shadow maps to avoid "shadow-acne", or false shadow occlusions
/// that happen as a result of shadow-map fragments not mapping 1:1 to screen-space fragments.
/// Too high of a depth bias can lead to shadows detaching from their casters, or
/// "peter-panning". This bias can be tuned together with `shadow_normal_bias` to correct shadow
/// artifacts for a given scene.
pub shadow_depth_bias: f32,
/// A bias applied along the direction of the fragment's surface normal. It is scaled to the
/// shadow map's texel size so that it can be small close to the camera and gets larger further
/// away.
pub shadow_normal_bias: f32,
/// The distance from the light to near Z plane in the shadow map.
///
/// Objects closer than this distance to the light won't cast shadows.
/// Setting this higher increases the shadow map's precision.
///
/// This only has an effect if shadows are enabled.
pub shadow_map_near_z: f32,
}
impl Default for PointLight {
fn default() -> Self {
PointLight {
color: Color::WHITE,
// 1,000,000 lumens is a very large "cinema light" capable of registering brightly at Bevy's
// default "very overcast day" exposure level. For "indoor lighting" with a lower exposure,
// this would be way too bright.
intensity: 1_000_000.0,
range: 20.0,
radius: 0.0,
shadows_enabled: false,
affects_lightmapped_mesh_diffuse: true,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
#[cfg(feature = "experimental_pbr_pcss")]
soft_shadows_enabled: false,
}
}
}
impl PointLight {
pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.08;
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6;
pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1;
}

142
vendor/bevy_pbr/src/light/spot_light.rs vendored Normal file
View File

@@ -0,0 +1,142 @@
use bevy_render::view::{self, Visibility};
use super::*;
/// A light that emits light in a given direction from a central point.
///
/// Behaves like a point light in a perfectly absorbent housing that
/// shines light only in a given direction. The direction is taken from
/// the transform, and can be specified with [`Transform::looking_at`](Transform::looking_at).
#[derive(Component, Debug, Clone, Copy, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
#[require(Frustum, VisibleMeshEntities, Transform, Visibility, VisibilityClass)]
#[component(on_add = view::add_visibility_class::<LightVisibilityClass>)]
pub struct SpotLight {
/// The color of the light.
///
/// By default, this is white.
pub color: Color,
/// Luminous power in lumens, representing the amount of light emitted by this source in all directions.
pub intensity: f32,
/// Range in meters that this light illuminates.
///
/// Note that this value affects resolution of the shadow maps; generally, the
/// higher you set it, the lower-resolution your shadow maps will be.
/// Consequently, you should set this value to be only the size that you need.
pub range: f32,
/// Simulates a light source coming from a spherical volume with the given
/// radius.
///
/// This affects the size of specular highlights created by this light, as
/// well as the soft shadow penumbra size. Because of this, large values may
/// not produce the intended result -- for example, light radius does not
/// affect shadow softness or diffuse lighting.
pub radius: f32,
/// Whether this light casts shadows.
///
/// Note that shadows are rather expensive and become more so with every
/// light that casts them. In general, it's best to aggressively limit the
/// number of lights with shadows enabled to one or two at most.
pub shadows_enabled: bool,
/// Whether soft shadows are enabled.
///
/// Soft shadows, also known as *percentage-closer soft shadows* or PCSS,
/// cause shadows to become blurrier (i.e. their penumbra increases in
/// radius) as they extend away from objects. The blurriness of the shadow
/// depends on the [`SpotLight::radius`] of the light; larger lights result in larger
/// penumbras and therefore blurrier shadows.
///
/// Currently, soft shadows are rather noisy if not using the temporal mode.
/// If you enable soft shadows, consider choosing
/// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing
/// (TAA) to smooth the noise out over time.
///
/// Note that soft shadows are significantly more expensive to render than
/// hard shadows.
#[cfg(feature = "experimental_pbr_pcss")]
pub soft_shadows_enabled: bool,
/// Whether this spot light contributes diffuse lighting to meshes with
/// lightmaps.
///
/// Set this to false if your lightmap baking tool bakes the direct diffuse
/// light from this directional light into the lightmaps in order to avoid
/// counting the radiance from this light twice. Note that the specular
/// portion of the light is always considered, because Bevy currently has no
/// means to bake specular light.
///
/// By default, this is set to true.
pub affects_lightmapped_mesh_diffuse: bool,
/// A value that adjusts the tradeoff between self-shadowing artifacts and
/// proximity of shadows to their casters.
///
/// This value frequently must be tuned to the specific scene; this is
/// normal and a well-known part of the shadow mapping workflow. If set too
/// low, unsightly shadow patterns appear on objects not in shadow as
/// objects incorrectly cast shadows on themselves, known as *shadow acne*.
/// If set too high, shadows detach from the objects casting them and seem
/// to "fly" off the objects, known as *Peter Panning*.
pub shadow_depth_bias: f32,
/// A bias applied along the direction of the fragment's surface normal. It is scaled to the
/// shadow map's texel size so that it can be small close to the camera and gets larger further
/// away.
pub shadow_normal_bias: f32,
/// The distance from the light to the near Z plane in the shadow map.
///
/// Objects closer than this distance to the light won't cast shadows.
/// Setting this higher increases the shadow map's precision.
///
/// This only has an effect if shadows are enabled.
pub shadow_map_near_z: f32,
/// Angle defining the distance from the spot light direction to the outer limit
/// of the light's cone of effect.
/// `outer_angle` should be < `PI / 2.0`.
/// `PI / 2.0` defines a hemispherical spot light, but shadows become very blocky as the angle
/// approaches this limit.
pub outer_angle: f32,
/// Angle defining the distance from the spot light direction to the inner limit
/// of the light's cone of effect.
/// Light is attenuated from `inner_angle` to `outer_angle` to give a smooth falloff.
/// `inner_angle` should be <= `outer_angle`
pub inner_angle: f32,
}
impl SpotLight {
pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02;
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 1.8;
pub const DEFAULT_SHADOW_MAP_NEAR_Z: f32 = 0.1;
}
impl Default for SpotLight {
fn default() -> Self {
// a quarter arc attenuating from the center
Self {
color: Color::WHITE,
// 1,000,000 lumens is a very large "cinema light" capable of registering brightly at Bevy's
// default "very overcast day" exposure level. For "indoor lighting" with a lower exposure,
// this would be way too bright.
intensity: 1_000_000.0,
range: 20.0,
radius: 0.0,
shadows_enabled: false,
affects_lightmapped_mesh_diffuse: true,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
inner_angle: 0.0,
outer_angle: core::f32::consts::FRAC_PI_4,
#[cfg(feature = "experimental_pbr_pcss")]
soft_shadows_enabled: false,
}
}
}

View File

@@ -0,0 +1,372 @@
//! Environment maps and reflection probes.
//!
//! An *environment map* consists of a pair of diffuse and specular cubemaps
//! that together reflect the static surrounding area of a region in space. When
//! available, the PBR shader uses these to apply diffuse light and calculate
//! specular reflections.
//!
//! Environment maps come in two flavors, depending on what other components the
//! entities they're attached to have:
//!
//! 1. If attached to a view, they represent the objects located a very far
//! distance from the view, in a similar manner to a skybox. Essentially, these
//! *view environment maps* represent a higher-quality replacement for
//! [`AmbientLight`](crate::AmbientLight) for outdoor scenes. The indirect light from such
//! environment maps are added to every point of the scene, including
//! interior enclosed areas.
//!
//! 2. If attached to a [`crate::LightProbe`], environment maps represent the immediate
//! surroundings of a specific location in the scene. These types of
//! environment maps are known as *reflection probes*.
//!
//! Typically, environment maps are static (i.e. "baked", calculated ahead of
//! time) and so only reflect fixed static geometry. The environment maps must
//! be pre-filtered into a pair of cubemaps, one for the diffuse component and
//! one for the specular component, according to the [split-sum approximation].
//! To pre-filter your environment map, you can use the [glTF IBL Sampler] or
//! its [artist-friendly UI]. The diffuse map uses the Lambertian distribution,
//! while the specular map uses the GGX distribution.
//!
//! The Khronos Group has [several pre-filtered environment maps] available for
//! you to use.
//!
//! Currently, reflection probes (i.e. environment maps attached to light
//! probes) use binding arrays (also known as bindless textures) and
//! consequently aren't supported on WebGL2 or WebGPU. Reflection probes are
//! also unsupported if GLSL is in use, due to `naga` limitations. Environment
//! maps attached to views are, however, supported on all platforms.
//!
//! [split-sum approximation]: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
//!
//! [glTF IBL Sampler]: https://github.com/KhronosGroup/glTF-IBL-Sampler
//!
//! [artist-friendly UI]: https://github.com/pcwalton/gltf-ibl-sampler-egui
//!
//! [several pre-filtered environment maps]: https://github.com/KhronosGroup/glTF-Sample-Environments
use bevy_asset::{weak_handle, AssetId, Handle};
use bevy_ecs::{
component::Component, query::QueryItem, reflect::ReflectComponent, system::lifetimeless::Read,
};
use bevy_image::Image;
use bevy_math::Quat;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
extract_instances::ExtractInstance,
render_asset::RenderAssets,
render_resource::{
binding_types::{self, uniform_buffer},
BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader, ShaderStages,
TextureSampleType, TextureView,
},
renderer::{RenderAdapter, RenderDevice},
texture::{FallbackImage, GpuImage},
};
use core::{num::NonZero, ops::Deref};
use crate::{
add_cubemap_texture_view, binding_arrays_are_usable, EnvironmentMapUniform,
MAX_VIEW_LIGHT_PROBES,
};
use super::{LightProbeComponent, RenderViewLightProbes};
/// A handle to the environment map helper shader.
pub const ENVIRONMENT_MAP_SHADER_HANDLE: Handle<Shader> =
weak_handle!("d38c4ec4-e84c-468f-b485-bf44745db937");
/// A pair of cubemap textures that represent the surroundings of a specific
/// area in space.
///
/// See [`crate::environment_map`] for detailed information.
#[derive(Clone, Component, Reflect)]
#[reflect(Component, Default, Clone)]
pub struct EnvironmentMapLight {
/// The blurry image that represents diffuse radiance surrounding a region.
pub diffuse_map: Handle<Image>,
/// The typically-sharper, mipmapped image that represents specular radiance
/// surrounding a region.
pub specular_map: Handle<Image>,
/// Scale factor applied to the diffuse and specular light generated by this component.
///
/// After applying this multiplier, the resulting values should
/// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre).
///
/// See also <https://google.github.io/filament/Filament.html#lighting/imagebasedlights/iblunit>.
pub intensity: f32,
/// World space rotation applied to the environment light cubemaps.
/// This is useful for users who require a different axis, such as the Z-axis, to serve
/// as the vertical axis.
pub rotation: Quat,
/// Whether the light from this environment map contributes diffuse lighting
/// to meshes with lightmaps.
///
/// Set this to false if your lightmap baking tool bakes the diffuse light
/// from this environment light into the lightmaps in order to avoid
/// counting the radiance from this environment map twice.
///
/// By default, this is set to true.
pub affects_lightmapped_mesh_diffuse: bool,
}
impl Default for EnvironmentMapLight {
fn default() -> Self {
EnvironmentMapLight {
diffuse_map: Handle::default(),
specular_map: Handle::default(),
intensity: 0.0,
rotation: Quat::IDENTITY,
affects_lightmapped_mesh_diffuse: true,
}
}
}
/// Like [`EnvironmentMapLight`], but contains asset IDs instead of handles.
///
/// This is for use in the render app.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct EnvironmentMapIds {
/// The blurry image that represents diffuse radiance surrounding a region.
pub(crate) diffuse: AssetId<Image>,
/// The typically-sharper, mipmapped image that represents specular radiance
/// surrounding a region.
pub(crate) specular: AssetId<Image>,
}
/// All the bind group entries necessary for PBR shaders to access the
/// environment maps exposed to a view.
pub(crate) enum RenderViewEnvironmentMapBindGroupEntries<'a> {
/// The version used when binding arrays aren't available on the current
/// platform.
Single {
/// The texture view of the view's diffuse cubemap.
diffuse_texture_view: &'a TextureView,
/// The texture view of the view's specular cubemap.
specular_texture_view: &'a TextureView,
/// The sampler used to sample elements of both `diffuse_texture_views` and
/// `specular_texture_views`.
sampler: &'a Sampler,
},
/// The version used when binding arrays are available on the current
/// platform.
Multiple {
/// A texture view of each diffuse cubemap, in the same order that they are
/// supplied to the view (i.e. in the same order as
/// `binding_index_to_cubemap` in [`RenderViewLightProbes`]).
///
/// This is a vector of `wgpu::TextureView`s. But we don't want to import
/// `wgpu` in this crate, so we refer to it indirectly like this.
diffuse_texture_views: Vec<&'a <TextureView as Deref>::Target>,
/// As above, but for specular cubemaps.
specular_texture_views: Vec<&'a <TextureView as Deref>::Target>,
/// The sampler used to sample elements of both `diffuse_texture_views` and
/// `specular_texture_views`.
sampler: &'a Sampler,
},
}
/// Information about the environment map attached to the view, if any. This is
/// a global environment map that lights everything visible in the view, as
/// opposed to a light probe which affects only a specific area.
pub struct EnvironmentMapViewLightProbeInfo {
/// The index of the diffuse and specular cubemaps in the binding arrays.
pub(crate) cubemap_index: i32,
/// The smallest mip level of the specular cubemap.
pub(crate) smallest_specular_mip_level: u32,
/// The scale factor applied to the diffuse and specular light in the
/// cubemap. This is in units of cd/m² (candela per square meter).
pub(crate) intensity: f32,
/// Whether this lightmap affects the diffuse lighting of lightmapped
/// meshes.
pub(crate) affects_lightmapped_mesh_diffuse: bool,
}
impl ExtractInstance for EnvironmentMapIds {
type QueryData = Read<EnvironmentMapLight>;
type QueryFilter = ();
fn extract(item: QueryItem<'_, Self::QueryData>) -> Option<Self> {
Some(EnvironmentMapIds {
diffuse: item.diffuse_map.id(),
specular: item.specular_map.id(),
})
}
}
/// Returns the bind group layout entries for the environment map diffuse and
/// specular binding arrays respectively, in addition to the sampler.
pub(crate) fn get_bind_group_layout_entries(
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
) -> [BindGroupLayoutEntryBuilder; 4] {
let mut texture_cube_binding =
binding_types::texture_cube(TextureSampleType::Float { filterable: true });
if binding_arrays_are_usable(render_device, render_adapter) {
texture_cube_binding =
texture_cube_binding.count(NonZero::<u32>::new(MAX_VIEW_LIGHT_PROBES as _).unwrap());
}
[
texture_cube_binding,
texture_cube_binding,
binding_types::sampler(SamplerBindingType::Filtering),
uniform_buffer::<EnvironmentMapUniform>(true).visibility(ShaderStages::FRAGMENT),
]
}
impl<'a> RenderViewEnvironmentMapBindGroupEntries<'a> {
/// Looks up and returns the bindings for the environment map diffuse and
/// specular binding arrays respectively, as well as the sampler.
pub(crate) fn get(
render_view_environment_maps: Option<&RenderViewLightProbes<EnvironmentMapLight>>,
images: &'a RenderAssets<GpuImage>,
fallback_image: &'a FallbackImage,
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
) -> RenderViewEnvironmentMapBindGroupEntries<'a> {
if binding_arrays_are_usable(render_device, render_adapter) {
let mut diffuse_texture_views = vec![];
let mut specular_texture_views = vec![];
let mut sampler = None;
if let Some(environment_maps) = render_view_environment_maps {
for &cubemap_id in &environment_maps.binding_index_to_textures {
add_cubemap_texture_view(
&mut diffuse_texture_views,
&mut sampler,
cubemap_id.diffuse,
images,
fallback_image,
);
add_cubemap_texture_view(
&mut specular_texture_views,
&mut sampler,
cubemap_id.specular,
images,
fallback_image,
);
}
}
// Pad out the bindings to the size of the binding array using fallback
// textures. This is necessary on D3D12 and Metal.
diffuse_texture_views.resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.cube.texture_view);
specular_texture_views
.resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.cube.texture_view);
return RenderViewEnvironmentMapBindGroupEntries::Multiple {
diffuse_texture_views,
specular_texture_views,
sampler: sampler.unwrap_or(&fallback_image.cube.sampler),
};
}
if let Some(environment_maps) = render_view_environment_maps {
if let Some(cubemap) = environment_maps.binding_index_to_textures.first() {
if let (Some(diffuse_image), Some(specular_image)) =
(images.get(cubemap.diffuse), images.get(cubemap.specular))
{
return RenderViewEnvironmentMapBindGroupEntries::Single {
diffuse_texture_view: &diffuse_image.texture_view,
specular_texture_view: &specular_image.texture_view,
sampler: &diffuse_image.sampler,
};
}
}
}
RenderViewEnvironmentMapBindGroupEntries::Single {
diffuse_texture_view: &fallback_image.cube.texture_view,
specular_texture_view: &fallback_image.cube.texture_view,
sampler: &fallback_image.cube.sampler,
}
}
}
impl LightProbeComponent for EnvironmentMapLight {
type AssetId = EnvironmentMapIds;
// Information needed to render with the environment map attached to the
// view.
type ViewLightProbeInfo = EnvironmentMapViewLightProbeInfo;
fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId> {
if image_assets.get(&self.diffuse_map).is_none()
|| image_assets.get(&self.specular_map).is_none()
{
None
} else {
Some(EnvironmentMapIds {
diffuse: self.diffuse_map.id(),
specular: self.specular_map.id(),
})
}
}
fn intensity(&self) -> f32 {
self.intensity
}
fn affects_lightmapped_mesh_diffuse(&self) -> bool {
self.affects_lightmapped_mesh_diffuse
}
fn create_render_view_light_probes(
view_component: Option<&EnvironmentMapLight>,
image_assets: &RenderAssets<GpuImage>,
) -> RenderViewLightProbes<Self> {
let mut render_view_light_probes = RenderViewLightProbes::new();
// Find the index of the cubemap associated with the view, and determine
// its smallest mip level.
if let Some(EnvironmentMapLight {
diffuse_map: diffuse_map_handle,
specular_map: specular_map_handle,
intensity,
affects_lightmapped_mesh_diffuse,
..
}) = view_component
{
if let (Some(_), Some(specular_map)) = (
image_assets.get(diffuse_map_handle),
image_assets.get(specular_map_handle),
) {
render_view_light_probes.view_light_probe_info = EnvironmentMapViewLightProbeInfo {
cubemap_index: render_view_light_probes.get_or_insert_cubemap(
&EnvironmentMapIds {
diffuse: diffuse_map_handle.id(),
specular: specular_map_handle.id(),
},
) as i32,
smallest_specular_mip_level: specular_map.mip_level_count - 1,
intensity: *intensity,
affects_lightmapped_mesh_diffuse: *affects_lightmapped_mesh_diffuse,
};
}
};
render_view_light_probes
}
}
impl Default for EnvironmentMapViewLightProbeInfo {
fn default() -> Self {
Self {
cubemap_index: -1,
smallest_specular_mip_level: 0,
intensity: 1.0,
affects_lightmapped_mesh_diffuse: true,
}
}
}

View File

@@ -0,0 +1,281 @@
#define_import_path bevy_pbr::environment_map
#import bevy_pbr::light_probe::query_light_probe
#import bevy_pbr::mesh_view_bindings as bindings
#import bevy_pbr::mesh_view_bindings::light_probes
#import bevy_pbr::mesh_view_bindings::environment_map_uniform
#import bevy_pbr::lighting::{F_Schlick_vec, LightingInput, LayerLightingInput, LAYER_BASE, LAYER_CLEARCOAT}
#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges
struct EnvironmentMapLight {
diffuse: vec3<f32>,
specular: vec3<f32>,
};
struct EnvironmentMapRadiances {
irradiance: vec3<f32>,
radiance: vec3<f32>,
}
// Define two versions of this function, one for the case in which there are
// multiple light probes and one for the case in which only the view light probe
// is present.
#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY
fn compute_radiances(
input: LayerLightingInput,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
world_position: vec3<f32>,
found_diffuse_indirect: bool,
) -> EnvironmentMapRadiances {
// Unpack.
let N = input.N;
let R = input.R;
let NdotV = input.NdotV;
let perceptual_roughness = input.perceptual_roughness;
let roughness = input.roughness;
var radiances: EnvironmentMapRadiances;
// Search for a reflection probe that contains the fragment.
var query_result = query_light_probe(
world_position,
/*is_irradiance_volume=*/ false,
clusterable_object_index_ranges,
);
// If we didn't find a reflection probe, use the view environment map if applicable.
if (query_result.texture_index < 0) {
query_result.texture_index = light_probes.view_cubemap_index;
query_result.intensity = light_probes.intensity_for_view;
query_result.affects_lightmapped_mesh_diffuse =
light_probes.view_environment_map_affects_lightmapped_mesh_diffuse != 0u;
}
// If there's no cubemap, bail out.
if (query_result.texture_index < 0) {
radiances.irradiance = vec3(0.0);
radiances.radiance = vec3(0.0);
return radiances;
}
// Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
let radiance_level = perceptual_roughness * f32(textureNumLevels(
bindings::specular_environment_maps[query_result.texture_index]) - 1u);
// If we're lightmapped, and we shouldn't accumulate diffuse light from the
// environment map, note that.
var enable_diffuse = !found_diffuse_indirect;
#ifdef LIGHTMAP
enable_diffuse = enable_diffuse && query_result.affects_lightmapped_mesh_diffuse;
#endif // LIGHTMAP
if (enable_diffuse) {
var irradiance_sample_dir = N;
// Rotating the world space ray direction by the environment light map transform matrix, it is
// equivalent to rotating the diffuse environment cubemap itself.
irradiance_sample_dir = (environment_map_uniform.transform * vec4(irradiance_sample_dir, 1.0)).xyz;
// Cube maps are left-handed so we negate the z coordinate.
irradiance_sample_dir.z = -irradiance_sample_dir.z;
radiances.irradiance = textureSampleLevel(
bindings::diffuse_environment_maps[query_result.texture_index],
bindings::environment_map_sampler,
irradiance_sample_dir,
0.0).rgb * query_result.intensity;
}
var radiance_sample_dir = radiance_sample_direction(N, R, roughness);
// Rotating the world space ray direction by the environment light map transform matrix, it is
// equivalent to rotating the specular environment cubemap itself.
radiance_sample_dir = (environment_map_uniform.transform * vec4(radiance_sample_dir, 1.0)).xyz;
// Cube maps are left-handed so we negate the z coordinate.
radiance_sample_dir.z = -radiance_sample_dir.z;
radiances.radiance = textureSampleLevel(
bindings::specular_environment_maps[query_result.texture_index],
bindings::environment_map_sampler,
radiance_sample_dir,
radiance_level).rgb * query_result.intensity;
return radiances;
}
#else // MULTIPLE_LIGHT_PROBES_IN_ARRAY
fn compute_radiances(
input: LayerLightingInput,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
world_position: vec3<f32>,
found_diffuse_indirect: bool,
) -> EnvironmentMapRadiances {
// Unpack.
let N = input.N;
let R = input.R;
let NdotV = input.NdotV;
let perceptual_roughness = input.perceptual_roughness;
let roughness = input.roughness;
var radiances: EnvironmentMapRadiances;
if (light_probes.view_cubemap_index < 0) {
radiances.irradiance = vec3(0.0);
radiances.radiance = vec3(0.0);
return radiances;
}
// Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
// Technically we could use textureNumLevels(specular_environment_map) - 1 here, but we use a uniform
// because textureNumLevels() does not work on WebGL2
let radiance_level = perceptual_roughness * f32(light_probes.smallest_specular_mip_level_for_view);
let intensity = light_probes.intensity_for_view;
// If we're lightmapped, and we shouldn't accumulate diffuse light from the
// environment map, note that.
var enable_diffuse = !found_diffuse_indirect;
#ifdef LIGHTMAP
enable_diffuse = enable_diffuse &&
light_probes.view_environment_map_affects_lightmapped_mesh_diffuse;
#endif // LIGHTMAP
if (enable_diffuse) {
var irradiance_sample_dir = N;
// Rotating the world space ray direction by the environment light map transform matrix, it is
// equivalent to rotating the diffuse environment cubemap itself.
irradiance_sample_dir = (environment_map_uniform.transform * vec4(irradiance_sample_dir, 1.0)).xyz;
// Cube maps are left-handed so we negate the z coordinate.
irradiance_sample_dir.z = -irradiance_sample_dir.z;
radiances.irradiance = textureSampleLevel(
bindings::diffuse_environment_map,
bindings::environment_map_sampler,
irradiance_sample_dir,
0.0).rgb * intensity;
}
var radiance_sample_dir = radiance_sample_direction(N, R, roughness);
// Rotating the world space ray direction by the environment light map transform matrix, it is
// equivalent to rotating the specular environment cubemap itself.
radiance_sample_dir = (environment_map_uniform.transform * vec4(radiance_sample_dir, 1.0)).xyz;
// Cube maps are left-handed so we negate the z coordinate.
radiance_sample_dir.z = -radiance_sample_dir.z;
radiances.radiance = textureSampleLevel(
bindings::specular_environment_map,
bindings::environment_map_sampler,
radiance_sample_dir,
radiance_level).rgb * intensity;
return radiances;
}
#endif // MULTIPLE_LIGHT_PROBES_IN_ARRAY
#ifdef STANDARD_MATERIAL_CLEARCOAT
// Adds the environment map light from the clearcoat layer to that of the base
// layer.
fn environment_map_light_clearcoat(
out: ptr<function, EnvironmentMapLight>,
input: ptr<function, LightingInput>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
found_diffuse_indirect: bool,
) {
// Unpack.
let world_position = (*input).P;
let clearcoat_NdotV = (*input).layers[LAYER_CLEARCOAT].NdotV;
let clearcoat_strength = (*input).clearcoat_strength;
// Calculate the Fresnel term `Fc` for the clearcoat layer.
// 0.04 is a hardcoded value for F0 from the Filament spec.
let clearcoat_F0 = vec3<f32>(0.04);
let Fc = F_Schlick_vec(clearcoat_F0, 1.0, clearcoat_NdotV) * clearcoat_strength;
let inv_Fc = 1.0 - Fc;
let clearcoat_radiances = compute_radiances(
(*input).layers[LAYER_CLEARCOAT],
clusterable_object_index_ranges,
world_position,
found_diffuse_indirect,
);
// Composite the clearcoat layer on top of the existing one.
// These formulas are from Filament:
// <https://google.github.io/filament/Filament.md.html#lighting/imagebasedlights/clearcoat>
(*out).diffuse *= inv_Fc;
(*out).specular = (*out).specular * inv_Fc * inv_Fc + clearcoat_radiances.radiance * Fc;
}
#endif // STANDARD_MATERIAL_CLEARCOAT
fn environment_map_light(
input: ptr<function, LightingInput>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
found_diffuse_indirect: bool,
) -> EnvironmentMapLight {
// Unpack.
let roughness = (*input).layers[LAYER_BASE].roughness;
let diffuse_color = (*input).diffuse_color;
let NdotV = (*input).layers[LAYER_BASE].NdotV;
let F_ab = (*input).F_ab;
let F0 = (*input).F0_;
let world_position = (*input).P;
var out: EnvironmentMapLight;
let radiances = compute_radiances(
(*input).layers[LAYER_BASE],
clusterable_object_index_ranges,
world_position,
found_diffuse_indirect,
);
if (all(radiances.irradiance == vec3(0.0)) && all(radiances.radiance == vec3(0.0))) {
out.diffuse = vec3(0.0);
out.specular = vec3(0.0);
return out;
}
// No real world material has specular values under 0.02, so we use this range as a
// "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control.
// See: https://google.github.io/filament/Filament.html#specularocclusion
let specular_occlusion = saturate(dot(F0, vec3(50.0 * 0.33)));
// Multiscattering approximation: https://www.jcgt.org/published/0008/01/03/paper.pdf
// Useful reference: https://bruop.github.io/ibl
let Fr = max(vec3(1.0 - roughness), F0) - F0;
let kS = F0 + Fr * pow(1.0 - NdotV, 5.0);
let Ess = F_ab.x + F_ab.y;
let FssEss = kS * Ess * specular_occlusion;
let Ems = 1.0 - Ess;
let Favg = F0 + (1.0 - F0) / 21.0;
let Fms = FssEss * Favg / (1.0 - Ems * Favg);
let FmsEms = Fms * Ems;
let Edss = 1.0 - (FssEss + FmsEms);
let kD = diffuse_color * Edss;
if (!found_diffuse_indirect) {
out.diffuse = (FmsEms + kD) * radiances.irradiance;
} else {
out.diffuse = vec3(0.0);
}
out.specular = FssEss * radiances.radiance;
#ifdef STANDARD_MATERIAL_CLEARCOAT
environment_map_light_clearcoat(
&out,
input,
clusterable_object_index_ranges,
found_diffuse_indirect,
);
#endif // STANDARD_MATERIAL_CLEARCOAT
return out;
}
// "Moving Frostbite to Physically Based Rendering 3.0", listing 22
// https://seblagarde.wordpress.com/wp-content/uploads/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf#page=70
fn radiance_sample_direction(N: vec3<f32>, R: vec3<f32>, roughness: f32) -> vec3<f32> {
let smoothness = saturate(1.0 - roughness);
let lerp_factor = smoothness * (sqrt(smoothness) + roughness);
return mix(N, R, lerp_factor);
}

View File

@@ -0,0 +1,376 @@
//! Irradiance volumes, also known as voxel global illumination.
//!
//! An *irradiance volume* is a cuboid voxel region consisting of
//! regularly-spaced precomputed samples of diffuse indirect light. They're
//! ideal if you have a dynamic object such as a character that can move about
//! static non-moving geometry such as a level in a game, and you want that
//! dynamic object to be affected by the light bouncing off that static
//! geometry.
//!
//! To use irradiance volumes, you need to precompute, or *bake*, the indirect
//! light in your scene. Bevy doesn't currently come with a way to do this.
//! Fortunately, [Blender] provides a [baking tool] as part of the Eevee
//! renderer, and its irradiance volumes are compatible with those used by Bevy.
//! The [`bevy-baked-gi`] project provides a tool, `export-blender-gi`, that can
//! extract the baked irradiance volumes from the Blender `.blend` file and
//! package them up into a `.ktx2` texture for use by the engine. See the
//! documentation in the `bevy-baked-gi` project for more details on this
//! workflow.
//!
//! Like all light probes in Bevy, irradiance volumes are 1×1×1 cubes that can
//! be arbitrarily scaled, rotated, and positioned in a scene with the
//! [`bevy_transform::components::Transform`] component. The 3D voxel grid will
//! be stretched to fill the interior of the cube, and the illumination from the
//! irradiance volume will apply to all fragments within that bounding region.
//!
//! Bevy's irradiance volumes are based on Valve's [*ambient cubes*] as used in
//! *Half-Life 2* ([Mitchell 2006, slide 27]). These encode a single color of
//! light from the six 3D cardinal directions and blend the sides together
//! according to the surface normal. For an explanation of why ambient cubes
//! were chosen over spherical harmonics, see [Why ambient cubes?] below.
//!
//! If you wish to use a tool other than `export-blender-gi` to produce the
//! irradiance volumes, you'll need to pack the irradiance volumes in the
//! following format. The irradiance volume of resolution *(Rx, Ry, Rz)* is
//! expected to be a 3D texture of dimensions *(Rx, 2Ry, 3Rz)*. The unnormalized
//! texture coordinate *(s, t, p)* of the voxel at coordinate *(x, y, z)* with
//! side *S* ∈ *{-X, +X, -Y, +Y, -Z, +Z}* is as follows:
//!
//! ```text
//! s = x
//!
//! t = y + ⎰ 0 if S ∈ {-X, -Y, -Z}
//! ⎱ Ry if S ∈ {+X, +Y, +Z}
//!
//! ⎧ 0 if S ∈ {-X, +X}
//! p = z + ⎨ Rz if S ∈ {-Y, +Y}
//! ⎩ 2Rz if S ∈ {-Z, +Z}
//! ```
//!
//! Visually, in a left-handed coordinate system with Y up, viewed from the
//! right, the 3D texture looks like a stacked series of voxel grids, one for
//! each cube side, in this order:
//!
//! | **+X** | **+Y** | **+Z** |
//! | ------ | ------ | ------ |
//! | **-X** | **-Y** | **-Z** |
//!
//! A terminology note: Other engines may refer to irradiance volumes as *voxel
//! global illumination*, *VXGI*, or simply as *light probes*. Sometimes *light
//! probe* refers to what Bevy calls a reflection probe. In Bevy, *light probe*
//! is a generic term that encompasses all cuboid bounding regions that capture
//! indirect illumination, whether based on voxels or not.
//!
//! Note that, if binding arrays aren't supported (e.g. on WebGPU or WebGL 2),
//! then only the closest irradiance volume to the view will be taken into
//! account during rendering. The required `wgpu` features are
//! [`bevy_render::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY`] and
//! [`bevy_render::settings::WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING`].
//!
//! ## Why ambient cubes?
//!
//! This section describes the motivation behind the decision to use ambient
//! cubes in Bevy. It's not needed to use the feature; feel free to skip it
//! unless you're interested in its internal design.
//!
//! Bevy uses *Half-Life 2*-style ambient cubes (usually abbreviated as *HL2*)
//! as the representation of irradiance for light probes instead of the
//! more-popular spherical harmonics (*SH*). This might seem to be a surprising
//! choice, but it turns out to work well for the specific case of voxel
//! sampling on the GPU. Spherical harmonics have two problems that make them
//! less ideal for this use case:
//!
//! 1. The level 1 spherical harmonic coefficients can be negative. That
//! prevents the use of the efficient [RGB9E5 texture format], which only
//! encodes unsigned floating point numbers, and forces the use of the
//! less-efficient [RGBA16F format] if hardware interpolation is desired.
//!
//! 2. As an alternative to RGBA16F, level 1 spherical harmonics can be
//! normalized and scaled to the SH0 base color, as [Frostbite] does. This
//! allows them to be packed in standard LDR RGBA8 textures. However, this
//! prevents the use of hardware trilinear filtering, as the nonuniform scale
//! factor means that hardware interpolation no longer produces correct results.
//! The 8 texture fetches needed to interpolate between voxels can be upwards of
//! twice as slow as the hardware interpolation.
//!
//! The following chart summarizes the costs and benefits of ambient cubes,
//! level 1 spherical harmonics, and level 2 spherical harmonics:
//!
//! | Technique | HW-interpolated samples | Texel fetches | Bytes per voxel | Quality |
//! | ------------------------ | ----------------------- | ------------- | --------------- | ------- |
//! | Ambient cubes | 3 | 0 | 24 | Medium |
//! | Level 1 SH, compressed | 0 | 36 | 16 | Low |
//! | Level 1 SH, uncompressed | 4 | 0 | 24 | Low |
//! | Level 2 SH, compressed | 0 | 72 | 28 | High |
//! | Level 2 SH, uncompressed | 9 | 0 | 54 | High |
//!
//! (Note that the number of bytes per voxel can be reduced using various
//! texture compression methods, but the overall ratios remain similar.)
//!
//! From these data, we can see that ambient cubes balance fast lookups (from
//! leveraging hardware interpolation) with relatively-small storage
//! requirements and acceptable quality. Hence, they were chosen for irradiance
//! volumes in Bevy.
//!
//! [*ambient cubes*]: https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf
//!
//! [spherical harmonics]: https://en.wikipedia.org/wiki/Spherical_harmonic_lighting
//!
//! [RGB9E5 texture format]: https://www.khronos.org/opengl/wiki/Small_Float_Formats#RGB9_E5
//!
//! [RGBA16F format]: https://www.khronos.org/opengl/wiki/Small_Float_Formats#Low-bitdepth_floats
//!
//! [Frostbite]: https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/gdc2018-precomputedgiobalilluminationinfrostbite.pdf#page=53
//!
//! [Mitchell 2006, slide 27]: https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf#page=27
//!
//! [Blender]: http://blender.org/
//!
//! [baking tool]: https://docs.blender.org/manual/en/latest/render/eevee/render_settings/indirect_lighting.html
//!
//! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi
//!
//! [Why ambient cubes?]: #why-ambient-cubes
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_image::Image;
use bevy_render::{
render_asset::RenderAssets,
render_resource::{
binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader,
TextureSampleType, TextureView,
},
renderer::{RenderAdapter, RenderDevice},
texture::{FallbackImage, GpuImage},
};
use bevy_utils::default;
use core::{num::NonZero, ops::Deref};
use bevy_asset::{weak_handle, AssetId, Handle};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use crate::{
add_cubemap_texture_view, binding_arrays_are_usable, RenderViewLightProbes,
MAX_VIEW_LIGHT_PROBES,
};
use super::LightProbeComponent;
pub const IRRADIANCE_VOLUME_SHADER_HANDLE: Handle<Shader> =
weak_handle!("7fc7dcd8-3f90-4124-b093-be0e53e08205");
/// On WebGL and WebGPU, we must disable irradiance volumes, as otherwise we can
/// overflow the number of texture bindings when deferred rendering is in use
/// (see issue #11885).
pub(crate) const IRRADIANCE_VOLUMES_ARE_USABLE: bool = cfg!(not(target_arch = "wasm32"));
/// The component that defines an irradiance volume.
///
/// See [`crate::irradiance_volume`] for detailed information.
#[derive(Clone, Reflect, Component, Debug)]
#[reflect(Component, Default, Debug, Clone)]
pub struct IrradianceVolume {
/// The 3D texture that represents the ambient cubes, encoded in the format
/// described in [`crate::irradiance_volume`].
pub voxels: Handle<Image>,
/// Scale factor applied to the diffuse and specular light generated by this component.
///
/// After applying this multiplier, the resulting values should
/// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre).
///
/// See also <https://google.github.io/filament/Filament.html#lighting/imagebasedlights/iblunit>.
pub intensity: f32,
/// Whether the light from this irradiance volume has an effect on meshes
/// with lightmaps.
///
/// Set this to false if your lightmap baking tool bakes the light from this
/// irradiance volume into the lightmaps in order to avoid counting the
/// irradiance twice. Frequently, applications use irradiance volumes as a
/// lower-quality alternative to lightmaps for capturing indirect
/// illumination on dynamic objects, and such applications will want to set
/// this value to false.
///
/// By default, this is set to true.
pub affects_lightmapped_meshes: bool,
}
impl Default for IrradianceVolume {
#[inline]
fn default() -> Self {
IrradianceVolume {
voxels: default(),
intensity: 0.0,
affects_lightmapped_meshes: true,
}
}
}
/// All the bind group entries necessary for PBR shaders to access the
/// irradiance volumes exposed to a view.
pub(crate) enum RenderViewIrradianceVolumeBindGroupEntries<'a> {
/// The version used when binding arrays aren't available on the current platform.
Single {
/// The texture view of the closest light probe.
texture_view: &'a TextureView,
/// A sampler used to sample voxels of the irradiance volume.
sampler: &'a Sampler,
},
/// The version used when binding arrays are available on the current
/// platform.
Multiple {
/// A texture view of the voxels of each irradiance volume, in the same
/// order that they are supplied to the view (i.e. in the same order as
/// `binding_index_to_cubemap` in [`RenderViewLightProbes`]).
///
/// This is a vector of `wgpu::TextureView`s. But we don't want to import
/// `wgpu` in this crate, so we refer to it indirectly like this.
texture_views: Vec<&'a <TextureView as Deref>::Target>,
/// A sampler used to sample voxels of the irradiance volumes.
sampler: &'a Sampler,
},
}
impl<'a> RenderViewIrradianceVolumeBindGroupEntries<'a> {
/// Looks up and returns the bindings for any irradiance volumes visible in
/// the view, as well as the sampler.
pub(crate) fn get(
render_view_irradiance_volumes: Option<&RenderViewLightProbes<IrradianceVolume>>,
images: &'a RenderAssets<GpuImage>,
fallback_image: &'a FallbackImage,
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
) -> RenderViewIrradianceVolumeBindGroupEntries<'a> {
if binding_arrays_are_usable(render_device, render_adapter) {
RenderViewIrradianceVolumeBindGroupEntries::get_multiple(
render_view_irradiance_volumes,
images,
fallback_image,
)
} else {
RenderViewIrradianceVolumeBindGroupEntries::single(
render_view_irradiance_volumes,
images,
fallback_image,
)
}
}
/// Looks up and returns the bindings for any irradiance volumes visible in
/// the view, as well as the sampler. This is the version used when binding
/// arrays are available on the current platform.
fn get_multiple(
render_view_irradiance_volumes: Option<&RenderViewLightProbes<IrradianceVolume>>,
images: &'a RenderAssets<GpuImage>,
fallback_image: &'a FallbackImage,
) -> RenderViewIrradianceVolumeBindGroupEntries<'a> {
let mut texture_views = vec![];
let mut sampler = None;
if let Some(irradiance_volumes) = render_view_irradiance_volumes {
for &cubemap_id in &irradiance_volumes.binding_index_to_textures {
add_cubemap_texture_view(
&mut texture_views,
&mut sampler,
cubemap_id,
images,
fallback_image,
);
}
}
// Pad out the bindings to the size of the binding array using fallback
// textures. This is necessary on D3D12 and Metal.
texture_views.resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.d3.texture_view);
RenderViewIrradianceVolumeBindGroupEntries::Multiple {
texture_views,
sampler: sampler.unwrap_or(&fallback_image.d3.sampler),
}
}
/// Looks up and returns the bindings for any irradiance volumes visible in
/// the view, as well as the sampler. This is the version used when binding
/// arrays aren't available on the current platform.
fn single(
render_view_irradiance_volumes: Option<&RenderViewLightProbes<IrradianceVolume>>,
images: &'a RenderAssets<GpuImage>,
fallback_image: &'a FallbackImage,
) -> RenderViewIrradianceVolumeBindGroupEntries<'a> {
if let Some(irradiance_volumes) = render_view_irradiance_volumes {
if let Some(irradiance_volume) = irradiance_volumes.render_light_probes.first() {
if irradiance_volume.texture_index >= 0 {
if let Some(image_id) = irradiance_volumes
.binding_index_to_textures
.get(irradiance_volume.texture_index as usize)
{
if let Some(image) = images.get(*image_id) {
return RenderViewIrradianceVolumeBindGroupEntries::Single {
texture_view: &image.texture_view,
sampler: &image.sampler,
};
}
}
}
}
}
RenderViewIrradianceVolumeBindGroupEntries::Single {
texture_view: &fallback_image.d3.texture_view,
sampler: &fallback_image.d3.sampler,
}
}
}
/// Returns the bind group layout entries for the voxel texture and sampler
/// respectively.
pub(crate) fn get_bind_group_layout_entries(
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
) -> [BindGroupLayoutEntryBuilder; 2] {
let mut texture_3d_binding =
binding_types::texture_3d(TextureSampleType::Float { filterable: true });
if binding_arrays_are_usable(render_device, render_adapter) {
texture_3d_binding =
texture_3d_binding.count(NonZero::<u32>::new(MAX_VIEW_LIGHT_PROBES as _).unwrap());
}
[
texture_3d_binding,
binding_types::sampler(SamplerBindingType::Filtering),
]
}
impl LightProbeComponent for IrradianceVolume {
type AssetId = AssetId<Image>;
// Irradiance volumes can't be attached to the view, so we store nothing
// here.
type ViewLightProbeInfo = ();
fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId> {
if image_assets.get(&self.voxels).is_none() {
None
} else {
Some(self.voxels.id())
}
}
fn intensity(&self) -> f32 {
self.intensity
}
fn affects_lightmapped_mesh_diffuse(&self) -> bool {
self.affects_lightmapped_meshes
}
fn create_render_view_light_probes(
_: Option<&Self>,
_: &RenderAssets<GpuImage>,
) -> RenderViewLightProbes<Self> {
RenderViewLightProbes::new()
}
}

View File

@@ -0,0 +1,73 @@
#define_import_path bevy_pbr::irradiance_volume
#import bevy_pbr::light_probe::query_light_probe
#import bevy_pbr::mesh_view_bindings::{
irradiance_volumes,
irradiance_volume,
irradiance_volume_sampler,
light_probes,
};
#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges
#ifdef IRRADIANCE_VOLUMES_ARE_USABLE
// See:
// https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf
// Slide 28, "Ambient Cube Basis"
fn irradiance_volume_light(
world_position: vec3<f32>,
N: vec3<f32>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
) -> vec3<f32> {
// Search for an irradiance volume that contains the fragment.
let query_result = query_light_probe(
world_position,
/*is_irradiance_volume=*/ true,
clusterable_object_index_ranges,
);
// If there was no irradiance volume found, bail out.
if (query_result.texture_index < 0) {
return vec3(0.0f);
}
// If we're lightmapped, and the irradiance volume contributes no diffuse
// light, then bail out.
#ifdef LIGHTMAP
if (!query_result.affects_lightmapped_mesh_diffuse) {
return vec3(0.0f);
}
#endif // LIGHTMAP
#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY
let irradiance_volume_texture = irradiance_volumes[query_result.texture_index];
#else
let irradiance_volume_texture = irradiance_volume;
#endif
let atlas_resolution = vec3<f32>(textureDimensions(irradiance_volume_texture));
let resolution = vec3<f32>(textureDimensions(irradiance_volume_texture) / vec3(1u, 2u, 3u));
// Make sure to clamp to the edges to avoid texture bleed.
var unit_pos = (query_result.light_from_world * vec4(world_position, 1.0f)).xyz;
let stp = clamp((unit_pos + 0.5) * resolution, vec3(0.5f), resolution - vec3(0.5f));
let uvw = stp / atlas_resolution;
// The bottom half of each cube slice is the negative part, so choose it if applicable on each
// slice.
let neg_offset = select(vec3(0.0f), vec3(0.5f), N < vec3(0.0f));
let uvw_x = uvw + vec3(0.0f, neg_offset.x, 0.0f);
let uvw_y = uvw + vec3(0.0f, neg_offset.y, 1.0f / 3.0f);
let uvw_z = uvw + vec3(0.0f, neg_offset.z, 2.0f / 3.0f);
let rgb_x = textureSampleLevel(irradiance_volume_texture, irradiance_volume_sampler, uvw_x, 0.0).rgb;
let rgb_y = textureSampleLevel(irradiance_volume_texture, irradiance_volume_sampler, uvw_y, 0.0).rgb;
let rgb_z = textureSampleLevel(irradiance_volume_texture, irradiance_volume_sampler, uvw_z, 0.0).rgb;
// Use Valve's formula to sample.
let NN = N * N;
return (rgb_x * NN.x + rgb_y * NN.y + rgb_z * NN.z) * query_result.intensity;
}
#endif // IRRADIANCE_VOLUMES_ARE_USABLE

View File

@@ -0,0 +1,154 @@
#define_import_path bevy_pbr::light_probe
#import bevy_pbr::clustered_forward
#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges
#import bevy_pbr::mesh_view_bindings::light_probes
#import bevy_pbr::mesh_view_types::LightProbe
// The result of searching for a light probe.
struct LightProbeQueryResult {
// The index of the light probe texture or textures in the binding array or
// arrays.
texture_index: i32,
// A scale factor that's applied to the diffuse and specular light from the
// light probe. This is in units of cd/m² (candela per square meter).
intensity: f32,
// Transform from world space to the light probe model space. In light probe
// model space, the light probe is a 1×1×1 cube centered on the origin.
light_from_world: mat4x4<f32>,
// Whether this light probe contributes diffuse light to lightmapped meshes.
affects_lightmapped_mesh_diffuse: bool,
};
fn transpose_affine_matrix(matrix: mat3x4<f32>) -> mat4x4<f32> {
let matrix4x4 = mat4x4<f32>(
matrix[0],
matrix[1],
matrix[2],
vec4<f32>(0.0, 0.0, 0.0, 1.0));
return transpose(matrix4x4);
}
#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3
// Searches for a light probe that contains the fragment.
//
// This is the version that's used when storage buffers are available and
// light probes are clustered.
//
// TODO: Interpolate between multiple light probes.
fn query_light_probe(
world_position: vec3<f32>,
is_irradiance_volume: bool,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
) -> LightProbeQueryResult {
var result: LightProbeQueryResult;
result.texture_index = -1;
// Reflection probe indices are followed by irradiance volume indices in the
// cluster index list. Use this fact to create our bracketing range of
// indices.
var start_offset: u32;
var end_offset: u32;
if is_irradiance_volume {
start_offset = (*clusterable_object_index_ranges).first_irradiance_volume_index_offset;
end_offset = (*clusterable_object_index_ranges).first_decal_offset;
} else {
start_offset = (*clusterable_object_index_ranges).first_reflection_probe_index_offset;
end_offset = (*clusterable_object_index_ranges).first_irradiance_volume_index_offset;
}
for (var light_probe_index_offset: u32 = start_offset;
light_probe_index_offset < end_offset && result.texture_index < 0;
light_probe_index_offset += 1u) {
let light_probe_index = i32(clustered_forward::get_clusterable_object_id(
light_probe_index_offset));
var light_probe: LightProbe;
if is_irradiance_volume {
light_probe = light_probes.irradiance_volumes[light_probe_index];
} else {
light_probe = light_probes.reflection_probes[light_probe_index];
}
// Unpack the inverse transform.
let light_from_world =
transpose_affine_matrix(light_probe.light_from_world_transposed);
// Check to see if the transformed point is inside the unit cube
// centered at the origin.
let probe_space_pos = (light_from_world * vec4<f32>(world_position, 1.0f)).xyz;
if (all(abs(probe_space_pos) <= vec3(0.5f))) {
result.texture_index = light_probe.cubemap_index;
result.intensity = light_probe.intensity;
result.light_from_world = light_from_world;
result.affects_lightmapped_mesh_diffuse =
light_probe.affects_lightmapped_mesh_diffuse != 0u;
break;
}
}
return result;
}
#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3
// Searches for a light probe that contains the fragment.
//
// This is the version that's used when storage buffers aren't available and
// light probes aren't clustered. It simply does a brute force search of all
// light probes. Because platforms without sufficient SSBO bindings typically
// lack bindless shaders, there will usually only be one of each type of light
// probe present anyway.
fn query_light_probe(
world_position: vec3<f32>,
is_irradiance_volume: bool,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
) -> LightProbeQueryResult {
var result: LightProbeQueryResult;
result.texture_index = -1;
var light_probe_count: i32;
if is_irradiance_volume {
light_probe_count = light_probes.irradiance_volume_count;
} else {
light_probe_count = light_probes.reflection_probe_count;
}
for (var light_probe_index: i32 = 0;
light_probe_index < light_probe_count && result.texture_index < 0;
light_probe_index += 1) {
var light_probe: LightProbe;
if is_irradiance_volume {
light_probe = light_probes.irradiance_volumes[light_probe_index];
} else {
light_probe = light_probes.reflection_probes[light_probe_index];
}
// Unpack the inverse transform.
let light_from_world =
transpose_affine_matrix(light_probe.light_from_world_transposed);
// Check to see if the transformed point is inside the unit cube
// centered at the origin.
let probe_space_pos = (light_from_world * vec4<f32>(world_position, 1.0f)).xyz;
if (all(abs(probe_space_pos) <= vec3(0.5f))) {
result.texture_index = light_probe.cubemap_index;
result.intensity = light_probe.intensity;
result.light_from_world = light_from_world;
result.affects_lightmapped_mesh_diffuse =
light_probe.affects_lightmapped_mesh_diffuse != 0u;
// TODO: Workaround for ICE in DXC https://github.com/microsoft/DirectXShaderCompiler/issues/6183
// We can't use `break` here because of the ICE.
// So instead we rely on the fact that we set `result.texture_index`
// above and check its value in the `for` loop header before
// looping.
// break;
}
}
return result;
}
#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3

804
vendor/bevy_pbr/src/light_probe/mod.rs vendored Normal file
View File

@@ -0,0 +1,804 @@
//! Light probes for baked global illumination.
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle};
use bevy_core_pipeline::core_3d::Camera3d;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
query::With,
reflect::ReflectComponent,
resource::Resource,
schedule::IntoScheduleConfigs,
system::{Commands, Local, Query, Res, ResMut},
};
use bevy_image::Image;
use bevy_math::{Affine3A, FloatOrd, Mat4, Vec3A, Vec4};
use bevy_platform::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
extract_instances::ExtractInstancesPlugin,
primitives::{Aabb, Frustum},
render_asset::RenderAssets,
render_resource::{DynamicUniformBuffer, Sampler, Shader, ShaderType, TextureView},
renderer::{RenderAdapter, RenderDevice, RenderQueue},
settings::WgpuFeatures,
sync_world::RenderEntity,
texture::{FallbackImage, GpuImage},
view::{ExtractedView, Visibility},
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::{components::Transform, prelude::GlobalTransform};
use tracing::error;
use core::{hash::Hash, ops::Deref};
use crate::{
irradiance_volume::IRRADIANCE_VOLUME_SHADER_HANDLE,
light_probe::environment_map::{
EnvironmentMapIds, EnvironmentMapLight, ENVIRONMENT_MAP_SHADER_HANDLE,
},
};
use self::irradiance_volume::IrradianceVolume;
pub const LIGHT_PROBE_SHADER_HANDLE: Handle<Shader> =
weak_handle!("e80a2ae6-1c5a-4d9a-a852-d66ff0e6bf7f");
pub mod environment_map;
pub mod irradiance_volume;
/// The maximum number of each type of light probe that each view will consider.
///
/// Because the fragment shader does a linear search through the list for each
/// fragment, this number needs to be relatively small.
pub const MAX_VIEW_LIGHT_PROBES: usize = 8;
/// How many texture bindings are used in the fragment shader, *not* counting
/// environment maps or irradiance volumes.
const STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS: usize = 16;
/// Adds support for light probes: cuboid bounding regions that apply global
/// illumination to objects within them.
///
/// This also adds support for view environment maps: diffuse and specular
/// cubemaps applied to all objects that a view renders.
pub struct LightProbePlugin;
/// A marker component for a light probe, which is a cuboid region that provides
/// global illumination to all fragments inside it.
///
/// Note that a light probe will have no effect unless the entity contains some
/// kind of illumination, which can either be an [`EnvironmentMapLight`] or an
/// [`IrradianceVolume`].
///
/// The light probe range is conceptually a unit cube (1×1×1) centered on the
/// origin. The [`Transform`] applied to this entity can scale, rotate, or translate
/// that cube so that it contains all fragments that should take this light probe into account.
///
/// When multiple sources of indirect illumination can be applied to a fragment,
/// the highest-quality one is chosen. Diffuse and specular illumination are
/// considered separately, so, for example, Bevy may decide to sample the
/// diffuse illumination from an irradiance volume and the specular illumination
/// from a reflection probe. From highest priority to lowest priority, the
/// ranking is as follows:
///
/// | Rank | Diffuse | Specular |
/// | ---- | -------------------- | -------------------- |
/// | 1 | Lightmap | Lightmap |
/// | 2 | Irradiance volume | Reflection probe |
/// | 3 | Reflection probe | View environment map |
/// | 4 | View environment map | |
///
/// Note that ambient light is always added to the diffuse component and does
/// not participate in the ranking. That is, ambient light is applied in
/// addition to, not instead of, the light sources above.
///
/// A terminology note: Unfortunately, there is little agreement across game and
/// graphics engines as to what to call the various techniques that Bevy groups
/// under the term *light probe*. In Bevy, a *light probe* is the generic term
/// that encompasses both *reflection probes* and *irradiance volumes*. In
/// object-oriented terms, *light probe* is the superclass, and *reflection
/// probe* and *irradiance volume* are subclasses. In other engines, you may see
/// the term *light probe* refer to an irradiance volume with a single voxel, or
/// perhaps some other technique, while in Bevy *light probe* refers not to a
/// specific technique but rather to a class of techniques. Developers familiar
/// with other engines should be aware of this terminology difference.
#[derive(Component, Debug, Clone, Copy, Default, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
#[require(Transform, Visibility)]
pub struct LightProbe;
/// A GPU type that stores information about a light probe.
#[derive(Clone, Copy, ShaderType, Default)]
struct RenderLightProbe {
/// The transform from the world space to the model space. This is used to
/// efficiently check for bounding box intersection.
light_from_world_transposed: [Vec4; 3],
/// The index of the texture or textures in the appropriate binding array or
/// arrays.
///
/// For example, for reflection probes this is the index of the cubemap in
/// the diffuse and specular texture arrays.
texture_index: i32,
/// Scale factor applied to the light generated by this light probe.
///
/// See the comment in [`EnvironmentMapLight`] for details.
intensity: f32,
/// Whether this light probe adds to the diffuse contribution of the
/// irradiance for meshes with lightmaps.
affects_lightmapped_mesh_diffuse: u32,
}
/// A per-view shader uniform that specifies all the light probes that the view
/// takes into account.
#[derive(ShaderType)]
pub struct LightProbesUniform {
/// The list of applicable reflection probes, sorted from nearest to the
/// camera to the farthest away from the camera.
reflection_probes: [RenderLightProbe; MAX_VIEW_LIGHT_PROBES],
/// The list of applicable irradiance volumes, sorted from nearest to the
/// camera to the farthest away from the camera.
irradiance_volumes: [RenderLightProbe; MAX_VIEW_LIGHT_PROBES],
/// The number of reflection probes in the list.
reflection_probe_count: i32,
/// The number of irradiance volumes in the list.
irradiance_volume_count: i32,
/// The index of the diffuse and specular environment maps associated with
/// the view itself. This is used as a fallback if no reflection probe in
/// the list contains the fragment.
view_cubemap_index: i32,
/// The smallest valid mipmap level for the specular environment cubemap
/// associated with the view.
smallest_specular_mip_level_for_view: u32,
/// The intensity of the environment cubemap associated with the view.
///
/// See the comment in [`EnvironmentMapLight`] for details.
intensity_for_view: f32,
/// Whether the environment map attached to the view affects the diffuse
/// lighting for lightmapped meshes.
///
/// This will be 1 if the map does affect lightmapped meshes or 0 otherwise.
view_environment_map_affects_lightmapped_mesh_diffuse: u32,
}
/// A GPU buffer that stores information about all light probes.
#[derive(Resource, Default, Deref, DerefMut)]
pub struct LightProbesBuffer(DynamicUniformBuffer<LightProbesUniform>);
/// A component attached to each camera in the render world that stores the
/// index of the [`LightProbesUniform`] in the [`LightProbesBuffer`].
#[derive(Component, Default, Deref, DerefMut)]
pub struct ViewLightProbesUniformOffset(u32);
/// Information that [`gather_light_probes`] keeps about each light probe.
///
/// This information is parameterized by the [`LightProbeComponent`] type. This
/// will either be [`EnvironmentMapLight`] for reflection probes or
/// [`IrradianceVolume`] for irradiance volumes.
struct LightProbeInfo<C>
where
C: LightProbeComponent,
{
// The transform from world space to light probe space.
light_from_world: Mat4,
// The transform from light probe space to world space.
world_from_light: Affine3A,
// Scale factor applied to the diffuse and specular light generated by this
// reflection probe.
//
// See the comment in [`EnvironmentMapLight`] for details.
intensity: f32,
// Whether this light probe adds to the diffuse contribution of the
// irradiance for meshes with lightmaps.
affects_lightmapped_mesh_diffuse: bool,
// The IDs of all assets associated with this light probe.
//
// Because each type of light probe component may reference different types
// of assets (e.g. a reflection probe references two cubemap assets while an
// irradiance volume references a single 3D texture asset), this is generic.
asset_id: C::AssetId,
}
/// A component, part of the render world, that stores the mapping from asset ID
/// or IDs to the texture index in the appropriate binding arrays.
///
/// Cubemap textures belonging to environment maps are collected into binding
/// arrays, and the index of each texture is presented to the shader for runtime
/// lookup. 3D textures belonging to reflection probes are likewise collected
/// into binding arrays, and the shader accesses the 3D texture by index.
///
/// This component is attached to each view in the render world, because each
/// view may have a different set of light probes that it considers and therefore
/// the texture indices are per-view.
#[derive(Component, Default)]
pub struct RenderViewLightProbes<C>
where
C: LightProbeComponent,
{
/// The list of environment maps presented to the shader, in order.
binding_index_to_textures: Vec<C::AssetId>,
/// The reverse of `binding_index_to_cubemap`: a map from the texture ID to
/// the index in `binding_index_to_cubemap`.
cubemap_to_binding_index: HashMap<C::AssetId, u32>,
/// Information about each light probe, ready for upload to the GPU, sorted
/// in order from closest to the camera to farthest.
///
/// Note that this is not necessarily ordered by binding index. So don't
/// write code like
/// `render_light_probes[cubemap_to_binding_index[asset_id]]`; instead
/// search for the light probe with the appropriate binding index in this
/// array.
render_light_probes: Vec<RenderLightProbe>,
/// Information needed to render the light probe attached directly to the
/// view, if applicable.
///
/// A light probe attached directly to a view represents a "global" light
/// probe that affects all objects not in the bounding region of any light
/// probe. Currently, the only light probe type that supports this is the
/// [`EnvironmentMapLight`].
view_light_probe_info: C::ViewLightProbeInfo,
}
/// A trait implemented by all components that represent light probes.
///
/// Currently, the two light probe types are [`EnvironmentMapLight`] and
/// [`IrradianceVolume`], for reflection probes and irradiance volumes
/// respectively.
///
/// Most light probe systems are written to be generic over the type of light
/// probe. This allows much of the code to be shared and enables easy addition
/// of more light probe types (e.g. real-time reflection planes) in the future.
pub trait LightProbeComponent: Send + Sync + Component + Sized {
/// Holds [`AssetId`]s of the texture or textures that this light probe
/// references.
///
/// This can just be [`AssetId`] if the light probe only references one
/// texture. If it references multiple textures, it will be a structure
/// containing those asset IDs.
type AssetId: Send + Sync + Clone + Eq + Hash;
/// If the light probe can be attached to the view itself (as opposed to a
/// cuboid region within the scene), this contains the information that will
/// be passed to the GPU in order to render it. Otherwise, this will be
/// `()`.
///
/// Currently, only reflection probes (i.e. [`EnvironmentMapLight`]) can be
/// attached directly to views.
type ViewLightProbeInfo: Send + Sync + Default;
/// Returns the asset ID or asset IDs of the texture or textures referenced
/// by this light probe.
fn id(&self, image_assets: &RenderAssets<GpuImage>) -> Option<Self::AssetId>;
/// Returns the intensity of this light probe.
///
/// This is a scaling factor that will be multiplied by the value or values
/// sampled from the texture.
fn intensity(&self) -> f32;
/// Returns true if this light probe contributes diffuse lighting to meshes
/// with lightmaps or false otherwise.
fn affects_lightmapped_mesh_diffuse(&self) -> bool;
/// Creates an instance of [`RenderViewLightProbes`] containing all the
/// information needed to render this light probe.
///
/// This is called for every light probe in view every frame.
fn create_render_view_light_probes(
view_component: Option<&Self>,
image_assets: &RenderAssets<GpuImage>,
) -> RenderViewLightProbes<Self>;
}
impl LightProbe {
/// Creates a new light probe component.
#[inline]
pub fn new() -> Self {
Self
}
}
/// The uniform struct extracted from [`EnvironmentMapLight`].
/// Will be available for use in the Environment Map shader.
#[derive(Component, ShaderType, Clone)]
pub struct EnvironmentMapUniform {
/// The world space transformation matrix of the sample ray for environment cubemaps.
transform: Mat4,
}
impl Default for EnvironmentMapUniform {
fn default() -> Self {
EnvironmentMapUniform {
transform: Mat4::IDENTITY,
}
}
}
/// A GPU buffer that stores the environment map settings for each view.
#[derive(Resource, Default, Deref, DerefMut)]
pub struct EnvironmentMapUniformBuffer(pub DynamicUniformBuffer<EnvironmentMapUniform>);
/// A component that stores the offset within the
/// [`EnvironmentMapUniformBuffer`] for each view.
#[derive(Component, Default, Deref, DerefMut)]
pub struct ViewEnvironmentMapUniformOffset(u32);
impl Plugin for LightProbePlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
LIGHT_PROBE_SHADER_HANDLE,
"light_probe.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
ENVIRONMENT_MAP_SHADER_HANDLE,
"environment_map.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
IRRADIANCE_VOLUME_SHADER_HANDLE,
"irradiance_volume.wgsl",
Shader::from_wgsl
);
app.register_type::<LightProbe>()
.register_type::<EnvironmentMapLight>()
.register_type::<IrradianceVolume>();
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.add_plugins(ExtractInstancesPlugin::<EnvironmentMapIds>::new())
.init_resource::<LightProbesBuffer>()
.init_resource::<EnvironmentMapUniformBuffer>()
.add_systems(ExtractSchedule, gather_environment_map_uniform)
.add_systems(ExtractSchedule, gather_light_probes::<EnvironmentMapLight>)
.add_systems(ExtractSchedule, gather_light_probes::<IrradianceVolume>)
.add_systems(
Render,
(upload_light_probes, prepare_environment_uniform_buffer)
.in_set(RenderSet::PrepareResources),
);
}
}
/// Extracts [`EnvironmentMapLight`] from views and creates [`EnvironmentMapUniform`] for them.
///
/// Compared to the `ExtractComponentPlugin`, this implementation will create a default instance
/// if one does not already exist.
fn gather_environment_map_uniform(
view_query: Extract<Query<(RenderEntity, Option<&EnvironmentMapLight>), With<Camera3d>>>,
mut commands: Commands,
) {
for (view_entity, environment_map_light) in view_query.iter() {
let environment_map_uniform = if let Some(environment_map_light) = environment_map_light {
EnvironmentMapUniform {
transform: Transform::from_rotation(environment_map_light.rotation)
.compute_matrix()
.inverse(),
}
} else {
EnvironmentMapUniform::default()
};
commands
.get_entity(view_entity)
.expect("Environment map light entity wasn't synced.")
.insert(environment_map_uniform);
}
}
/// Gathers up all light probes of a single type in the scene and assigns them
/// to views, performing frustum culling and distance sorting in the process.
fn gather_light_probes<C>(
image_assets: Res<RenderAssets<GpuImage>>,
light_probe_query: Extract<Query<(&GlobalTransform, &C), With<LightProbe>>>,
view_query: Extract<
Query<(RenderEntity, &GlobalTransform, &Frustum, Option<&C>), With<Camera3d>>,
>,
mut reflection_probes: Local<Vec<LightProbeInfo<C>>>,
mut view_reflection_probes: Local<Vec<LightProbeInfo<C>>>,
mut commands: Commands,
) where
C: LightProbeComponent,
{
// Create [`LightProbeInfo`] for every light probe in the scene.
reflection_probes.clear();
reflection_probes.extend(
light_probe_query
.iter()
.filter_map(|query_row| LightProbeInfo::new(query_row, &image_assets)),
);
// Build up the light probes uniform and the key table.
for (view_entity, view_transform, view_frustum, view_component) in view_query.iter() {
// Cull light probes outside the view frustum.
view_reflection_probes.clear();
view_reflection_probes.extend(
reflection_probes
.iter()
.filter(|light_probe_info| light_probe_info.frustum_cull(view_frustum))
.cloned(),
);
// Sort by distance to camera.
view_reflection_probes.sort_by_cached_key(|light_probe_info| {
light_probe_info.camera_distance_sort_key(view_transform)
});
// Create the light probes list.
let mut render_view_light_probes =
C::create_render_view_light_probes(view_component, &image_assets);
// Gather up the light probes in the list.
render_view_light_probes.maybe_gather_light_probes(&view_reflection_probes);
// Record the per-view light probes.
if render_view_light_probes.is_empty() {
commands
.get_entity(view_entity)
.expect("View entity wasn't synced.")
.remove::<RenderViewLightProbes<C>>();
} else {
commands
.get_entity(view_entity)
.expect("View entity wasn't synced.")
.insert(render_view_light_probes);
}
}
}
/// Gathers up environment map settings for each applicable view and
/// writes them into a GPU buffer.
pub fn prepare_environment_uniform_buffer(
mut commands: Commands,
views: Query<(Entity, Option<&EnvironmentMapUniform>), With<ExtractedView>>,
mut environment_uniform_buffer: ResMut<EnvironmentMapUniformBuffer>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
) {
let Some(mut writer) =
environment_uniform_buffer.get_writer(views.iter().len(), &render_device, &render_queue)
else {
return;
};
for (view, environment_uniform) in views.iter() {
let uniform_offset = match environment_uniform {
None => 0,
Some(environment_uniform) => writer.write(environment_uniform),
};
commands
.entity(view)
.insert(ViewEnvironmentMapUniformOffset(uniform_offset));
}
}
// A system that runs after [`gather_light_probes`] and populates the GPU
// uniforms with the results.
//
// Note that, unlike [`gather_light_probes`], this system is not generic over
// the type of light probe. It collects light probes of all types together into
// a single structure, ready to be passed to the shader.
fn upload_light_probes(
mut commands: Commands,
views: Query<Entity, With<ExtractedView>>,
mut light_probes_buffer: ResMut<LightProbesBuffer>,
mut view_light_probes_query: Query<(
Option<&RenderViewLightProbes<EnvironmentMapLight>>,
Option<&RenderViewLightProbes<IrradianceVolume>>,
)>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
) {
// If there are no views, bail.
if views.is_empty() {
return;
}
// Initialize the uniform buffer writer.
let mut writer = light_probes_buffer
.get_writer(views.iter().len(), &render_device, &render_queue)
.unwrap();
// Process each view.
for view_entity in views.iter() {
let Ok((render_view_environment_maps, render_view_irradiance_volumes)) =
view_light_probes_query.get_mut(view_entity)
else {
error!("Failed to find `RenderViewLightProbes` for the view!");
continue;
};
// Initialize the uniform with only the view environment map, if there
// is one.
let mut light_probes_uniform = LightProbesUniform {
reflection_probes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES],
irradiance_volumes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES],
reflection_probe_count: render_view_environment_maps
.map(RenderViewLightProbes::len)
.unwrap_or_default()
.min(MAX_VIEW_LIGHT_PROBES) as i32,
irradiance_volume_count: render_view_irradiance_volumes
.map(RenderViewLightProbes::len)
.unwrap_or_default()
.min(MAX_VIEW_LIGHT_PROBES) as i32,
view_cubemap_index: render_view_environment_maps
.map(|maps| maps.view_light_probe_info.cubemap_index)
.unwrap_or(-1),
smallest_specular_mip_level_for_view: render_view_environment_maps
.map(|maps| maps.view_light_probe_info.smallest_specular_mip_level)
.unwrap_or(0),
intensity_for_view: render_view_environment_maps
.map(|maps| maps.view_light_probe_info.intensity)
.unwrap_or(1.0),
view_environment_map_affects_lightmapped_mesh_diffuse: render_view_environment_maps
.map(|maps| maps.view_light_probe_info.affects_lightmapped_mesh_diffuse as u32)
.unwrap_or(1),
};
// Add any environment maps that [`gather_light_probes`] found to the
// uniform.
if let Some(render_view_environment_maps) = render_view_environment_maps {
render_view_environment_maps.add_to_uniform(
&mut light_probes_uniform.reflection_probes,
&mut light_probes_uniform.reflection_probe_count,
);
}
// Add any irradiance volumes that [`gather_light_probes`] found to the
// uniform.
if let Some(render_view_irradiance_volumes) = render_view_irradiance_volumes {
render_view_irradiance_volumes.add_to_uniform(
&mut light_probes_uniform.irradiance_volumes,
&mut light_probes_uniform.irradiance_volume_count,
);
}
// Queue the view's uniforms to be written to the GPU.
let uniform_offset = writer.write(&light_probes_uniform);
commands
.entity(view_entity)
.insert(ViewLightProbesUniformOffset(uniform_offset));
}
}
impl Default for LightProbesUniform {
fn default() -> Self {
Self {
reflection_probes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES],
irradiance_volumes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES],
reflection_probe_count: 0,
irradiance_volume_count: 0,
view_cubemap_index: -1,
smallest_specular_mip_level_for_view: 0,
intensity_for_view: 1.0,
view_environment_map_affects_lightmapped_mesh_diffuse: 1,
}
}
}
impl<C> LightProbeInfo<C>
where
C: LightProbeComponent,
{
/// Given the set of light probe components, constructs and returns
/// [`LightProbeInfo`]. This is done for every light probe in the scene
/// every frame.
fn new(
(light_probe_transform, environment_map): (&GlobalTransform, &C),
image_assets: &RenderAssets<GpuImage>,
) -> Option<LightProbeInfo<C>> {
environment_map.id(image_assets).map(|id| LightProbeInfo {
world_from_light: light_probe_transform.affine(),
light_from_world: light_probe_transform.compute_matrix().inverse(),
asset_id: id,
intensity: environment_map.intensity(),
affects_lightmapped_mesh_diffuse: environment_map.affects_lightmapped_mesh_diffuse(),
})
}
/// Returns true if this light probe is in the viewing frustum of the camera
/// or false if it isn't.
fn frustum_cull(&self, view_frustum: &Frustum) -> bool {
view_frustum.intersects_obb(
&Aabb {
center: Vec3A::default(),
half_extents: Vec3A::splat(0.5),
},
&self.world_from_light,
true,
false,
)
}
/// Returns the squared distance from this light probe to the camera,
/// suitable for distance sorting.
fn camera_distance_sort_key(&self, view_transform: &GlobalTransform) -> FloatOrd {
FloatOrd(
(self.world_from_light.translation - view_transform.translation_vec3a())
.length_squared(),
)
}
}
impl<C> RenderViewLightProbes<C>
where
C: LightProbeComponent,
{
/// Creates a new empty list of light probes.
fn new() -> RenderViewLightProbes<C> {
RenderViewLightProbes {
binding_index_to_textures: vec![],
cubemap_to_binding_index: HashMap::default(),
render_light_probes: vec![],
view_light_probe_info: C::ViewLightProbeInfo::default(),
}
}
/// Returns true if there are no light probes in the list.
pub(crate) fn is_empty(&self) -> bool {
self.binding_index_to_textures.is_empty()
}
/// Returns the number of light probes in the list.
pub(crate) fn len(&self) -> usize {
self.binding_index_to_textures.len()
}
/// Adds a cubemap to the list of bindings, if it wasn't there already, and
/// returns its index within that list.
pub(crate) fn get_or_insert_cubemap(&mut self, cubemap_id: &C::AssetId) -> u32 {
*self
.cubemap_to_binding_index
.entry((*cubemap_id).clone())
.or_insert_with(|| {
let index = self.binding_index_to_textures.len() as u32;
self.binding_index_to_textures.push((*cubemap_id).clone());
index
})
}
/// Adds all the light probes in this structure to the supplied array, which
/// is expected to be shipped to the GPU.
fn add_to_uniform(
&self,
render_light_probes: &mut [RenderLightProbe; MAX_VIEW_LIGHT_PROBES],
render_light_probe_count: &mut i32,
) {
render_light_probes[0..self.render_light_probes.len()]
.copy_from_slice(&self.render_light_probes[..]);
*render_light_probe_count = self.render_light_probes.len() as i32;
}
/// Gathers up all light probes of the given type in the scene and records
/// them in this structure.
fn maybe_gather_light_probes(&mut self, light_probes: &[LightProbeInfo<C>]) {
for light_probe in light_probes.iter().take(MAX_VIEW_LIGHT_PROBES) {
// Determine the index of the cubemap in the binding array.
let cubemap_index = self.get_or_insert_cubemap(&light_probe.asset_id);
// Transpose the inverse transform to compress the structure on the
// GPU (from 4 `Vec4`s to 3 `Vec4`s). The shader will transpose it
// to recover the original inverse transform.
let light_from_world_transposed = light_probe.light_from_world.transpose();
// Write in the light probe data.
self.render_light_probes.push(RenderLightProbe {
light_from_world_transposed: [
light_from_world_transposed.x_axis,
light_from_world_transposed.y_axis,
light_from_world_transposed.z_axis,
],
texture_index: cubemap_index as i32,
intensity: light_probe.intensity,
affects_lightmapped_mesh_diffuse: light_probe.affects_lightmapped_mesh_diffuse
as u32,
});
}
}
}
impl<C> Clone for LightProbeInfo<C>
where
C: LightProbeComponent,
{
fn clone(&self) -> Self {
Self {
light_from_world: self.light_from_world,
world_from_light: self.world_from_light,
intensity: self.intensity,
affects_lightmapped_mesh_diffuse: self.affects_lightmapped_mesh_diffuse,
asset_id: self.asset_id.clone(),
}
}
}
/// Adds a diffuse or specular texture view to the `texture_views` list, and
/// populates `sampler` if this is the first such view.
pub(crate) fn add_cubemap_texture_view<'a>(
texture_views: &mut Vec<&'a <TextureView as Deref>::Target>,
sampler: &mut Option<&'a Sampler>,
image_id: AssetId<Image>,
images: &'a RenderAssets<GpuImage>,
fallback_image: &'a FallbackImage,
) {
match images.get(image_id) {
None => {
// Use the fallback image if the cubemap isn't loaded yet.
texture_views.push(&*fallback_image.cube.texture_view);
}
Some(image) => {
// If this is the first texture view, populate `sampler`.
if sampler.is_none() {
*sampler = Some(&image.sampler);
}
texture_views.push(&*image.texture_view);
}
}
}
/// Many things can go wrong when attempting to use texture binding arrays
/// (a.k.a. bindless textures). This function checks for these pitfalls:
///
/// 1. If GLSL support is enabled at the feature level, then in debug mode
/// `naga_oil` will attempt to compile all shader modules under GLSL to check
/// validity of names, even if GLSL isn't actually used. This will cause a crash
/// if binding arrays are enabled, because binding arrays are currently
/// unimplemented in the GLSL backend of Naga. Therefore, we disable binding
/// arrays if the `shader_format_glsl` feature is present.
///
/// 2. If there aren't enough texture bindings available to accommodate all the
/// binding arrays, the driver will panic. So we also bail out if there aren't
/// enough texture bindings available in the fragment shader.
///
/// 3. If binding arrays aren't supported on the hardware, then we obviously
/// can't use them. Adreno <= 610 claims to support bindless, but seems to be
/// too buggy to be usable.
///
/// 4. If binding arrays are supported on the hardware, but they can only be
/// accessed by uniform indices, that's not good enough, and we bail out.
///
/// If binding arrays aren't usable, we disable reflection probes and limit the
/// number of irradiance volumes in the scene to 1.
pub(crate) fn binding_arrays_are_usable(
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
) -> bool {
!cfg!(feature = "shader_format_glsl")
&& bevy_render::get_adreno_model(render_adapter).is_none_or(|model| model > 610)
&& render_device.limits().max_storage_textures_per_shader_stage
>= (STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS + MAX_VIEW_LIGHT_PROBES)
as u32
&& render_device.features().contains(
WgpuFeatures::TEXTURE_BINDING_ARRAY
| WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
)
}

View File

@@ -0,0 +1,99 @@
#define_import_path bevy_pbr::lightmap
#import bevy_pbr::mesh_bindings::mesh
#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY
@group(1) @binding(4) var lightmaps_textures: binding_array<texture_2d<f32>, 4>;
@group(1) @binding(5) var lightmaps_samplers: binding_array<sampler, 4>;
#else // MULTIPLE_LIGHTMAPS_IN_ARRAY
@group(1) @binding(4) var lightmaps_texture: texture_2d<f32>;
@group(1) @binding(5) var lightmaps_sampler: sampler;
#endif // MULTIPLE_LIGHTMAPS_IN_ARRAY
// Samples the lightmap, if any, and returns indirect illumination from it.
fn lightmap(uv: vec2<f32>, exposure: f32, instance_index: u32) -> vec3<f32> {
let packed_uv_rect = mesh[instance_index].lightmap_uv_rect;
let uv_rect = vec4<f32>(
unpack2x16unorm(packed_uv_rect.x),
unpack2x16unorm(packed_uv_rect.y),
);
let lightmap_uv = mix(uv_rect.xy, uv_rect.zw, uv);
let lightmap_slot = mesh[instance_index].material_and_lightmap_bind_group_slot >> 16u;
// Bicubic 4-tap
// https://developer.nvidia.com/gpugems/gpugems2/part-iii-high-quality-rendering/chapter-20-fast-third-order-texture-filtering
// https://advances.realtimerendering.com/s2021/jpatry_advances2021/index.html#/111/0/2
#ifdef LIGHTMAP_BICUBIC_SAMPLING
let texture_size = vec2<f32>(lightmap_size(lightmap_slot));
let texel_size = 1.0 / texture_size;
let puv = lightmap_uv * texture_size + 0.5;
let iuv = floor(puv);
let fuv = fract(puv);
let g0x = g0(fuv.x);
let g1x = g1(fuv.x);
let h0x = h0_approx(fuv.x);
let h1x = h1_approx(fuv.x);
let h0y = h0_approx(fuv.y);
let h1y = h1_approx(fuv.y);
let p0 = (vec2(iuv.x + h0x, iuv.y + h0y) - 0.5) * texel_size;
let p1 = (vec2(iuv.x + h1x, iuv.y + h0y) - 0.5) * texel_size;
let p2 = (vec2(iuv.x + h0x, iuv.y + h1y) - 0.5) * texel_size;
let p3 = (vec2(iuv.x + h1x, iuv.y + h1y) - 0.5) * texel_size;
let color = g0(fuv.y) * (g0x * sample(p0, lightmap_slot) + g1x * sample(p1, lightmap_slot)) + g1(fuv.y) * (g0x * sample(p2, lightmap_slot) + g1x * sample(p3, lightmap_slot));
#else
let color = sample(lightmap_uv, lightmap_slot);
#endif
return color * exposure;
}
fn lightmap_size(lightmap_slot: u32) -> vec2<u32> {
#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY
return textureDimensions(lightmaps_textures[lightmap_slot]);
#else
return textureDimensions(lightmaps_texture);
#endif
}
fn sample(uv: vec2<f32>, lightmap_slot: u32) -> vec3<f32> {
// Mipmapping lightmaps is usually a bad idea due to leaking across UV
// islands, so there's no harm in using mip level 0 and it lets us avoid
// control flow uniformity problems.
#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY
return textureSampleLevel(lightmaps_textures[lightmap_slot], lightmaps_samplers[lightmap_slot], uv, 0.0).rgb;
#else
return textureSampleLevel(lightmaps_texture, lightmaps_sampler, uv, 0.0).rgb;
#endif
}
fn w0(a: f32) -> f32 {
return (1.0 / 6.0) * (a * (a * (-a + 3.0) - 3.0) + 1.0);
}
fn w1(a: f32) -> f32 {
return (1.0 / 6.0) * (a * a * (3.0 * a - 6.0) + 4.0);
}
fn w2(a: f32) -> f32 {
return (1.0 / 6.0) * (a * (a * (-3.0 * a + 3.0) + 3.0) + 1.0);
}
fn w3(a: f32) -> f32 {
return (1.0 / 6.0) * (a * a * a);
}
fn g0(a: f32) -> f32 {
return w0(a) + w1(a);
}
fn g1(a: f32) -> f32 {
return w2(a) + w3(a);
}
fn h0_approx(a: f32) -> f32 {
return -0.2 - a * (0.24 * a - 0.44);
}
fn h1_approx(a: f32) -> f32 {
return 1.0 + a * (0.24 * a - 0.04);
}

529
vendor/bevy_pbr/src/lightmap/mod.rs vendored Normal file
View File

@@ -0,0 +1,529 @@
//! Lightmaps, baked lighting textures that can be applied at runtime to provide
//! diffuse global illumination.
//!
//! Bevy doesn't currently have any way to actually bake lightmaps, but they can
//! be baked in an external tool like [Blender](http://blender.org), for example
//! with an addon like [The Lightmapper]. The tools in the [`bevy-baked-gi`]
//! project support other lightmap baking methods.
//!
//! When a [`Lightmap`] component is added to an entity with a [`Mesh3d`] and a
//! [`MeshMaterial3d<StandardMaterial>`], Bevy applies the lightmap when rendering. The brightness
//! of the lightmap may be controlled with the `lightmap_exposure` field on
//! [`StandardMaterial`].
//!
//! During the rendering extraction phase, we extract all lightmaps into the
//! [`RenderLightmaps`] table, which lives in the render world. Mesh bindgroup
//! and mesh uniform creation consults this table to determine which lightmap to
//! supply to the shader. Essentially, the lightmap is a special type of texture
//! that is part of the mesh instance rather than part of the material (because
//! multiple meshes can share the same material, whereas sharing lightmaps is
//! nonsensical).
//!
//! Note that multiple meshes can't be drawn in a single drawcall if they use
//! different lightmap textures, unless bindless textures are in use. If you
//! want to instance a lightmapped mesh, and your platform doesn't support
//! bindless textures, combine the lightmap textures into a single atlas, and
//! set the `uv_rect` field on [`Lightmap`] appropriately.
//!
//! [The Lightmapper]: https://github.com/Naxela/The_Lightmapper
//! [`Mesh3d`]: bevy_render::mesh::Mesh3d
//! [`MeshMaterial3d<StandardMaterial>`]: crate::StandardMaterial
//! [`StandardMaterial`]: crate::StandardMaterial
//! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{Changed, Or},
reflect::ReflectComponent,
removal_detection::RemovedComponents,
resource::Resource,
schedule::IntoScheduleConfigs,
system::{Query, Res, ResMut},
world::{FromWorld, World},
};
use bevy_image::Image;
use bevy_math::{uvec2, vec4, Rect, UVec2};
use bevy_platform::collections::HashSet;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
render_asset::RenderAssets,
render_resource::{Sampler, Shader, TextureView, WgpuSampler, WgpuTextureView},
renderer::RenderAdapter,
sync_world::MainEntity,
texture::{FallbackImage, GpuImage},
view::ViewVisibility,
Extract, ExtractSchedule, RenderApp,
};
use bevy_render::{renderer::RenderDevice, sync_world::MainEntityHashMap};
use bevy_utils::default;
use fixedbitset::FixedBitSet;
use nonmax::{NonMaxU16, NonMaxU32};
use tracing::error;
use crate::{binding_arrays_are_usable, ExtractMeshesSet};
/// The ID of the lightmap shader.
pub const LIGHTMAP_SHADER_HANDLE: Handle<Shader> =
weak_handle!("fc28203f-f258-47f3-973c-ce7d1dd70e59");
/// The number of lightmaps that we store in a single slab, if bindless textures
/// are in use.
///
/// If bindless textures aren't in use, then only a single lightmap can be bound
/// at a time.
pub const LIGHTMAPS_PER_SLAB: usize = 4;
/// A plugin that provides an implementation of lightmaps.
pub struct LightmapPlugin;
/// A component that applies baked indirect diffuse global illumination from a
/// lightmap.
///
/// When assigned to an entity that contains a [`Mesh3d`](bevy_render::mesh::Mesh3d) and a
/// [`MeshMaterial3d<StandardMaterial>`](crate::StandardMaterial), if the mesh
/// has a second UV layer ([`ATTRIBUTE_UV_1`](bevy_render::mesh::Mesh::ATTRIBUTE_UV_1)),
/// then the lightmap will render using those UVs.
#[derive(Component, Clone, Reflect)]
#[reflect(Component, Default, Clone)]
pub struct Lightmap {
/// The lightmap texture.
pub image: Handle<Image>,
/// The rectangle within the lightmap texture that the UVs are relative to.
///
/// The top left coordinate is the `min` part of the rect, and the bottom
/// right coordinate is the `max` part of the rect. The rect ranges from (0,
/// 0) to (1, 1).
///
/// This field allows lightmaps for a variety of meshes to be packed into a
/// single atlas.
pub uv_rect: Rect,
/// Whether bicubic sampling should be used for sampling this lightmap.
///
/// Bicubic sampling is higher quality, but slower, and may lead to light leaks.
///
/// If true, the lightmap texture's sampler must be set to [`bevy_image::ImageSampler::linear`].
pub bicubic_sampling: bool,
}
/// Lightmap data stored in the render world.
///
/// There is one of these per visible lightmapped mesh instance.
#[derive(Debug)]
pub(crate) struct RenderLightmap {
/// The rectangle within the lightmap texture that the UVs are relative to.
///
/// The top left coordinate is the `min` part of the rect, and the bottom
/// right coordinate is the `max` part of the rect. The rect ranges from (0,
/// 0) to (1, 1).
pub(crate) uv_rect: Rect,
/// The index of the slab (i.e. binding array) in which the lightmap is
/// located.
pub(crate) slab_index: LightmapSlabIndex,
/// The index of the slot (i.e. element within the binding array) in which
/// the lightmap is located.
///
/// If bindless lightmaps aren't in use, this will be 0.
pub(crate) slot_index: LightmapSlotIndex,
// Whether or not bicubic sampling should be used for this lightmap.
pub(crate) bicubic_sampling: bool,
}
/// Stores data for all lightmaps in the render world.
///
/// This is cleared and repopulated each frame during the `extract_lightmaps`
/// system.
#[derive(Resource)]
pub struct RenderLightmaps {
/// The mapping from every lightmapped entity to its lightmap info.
///
/// Entities without lightmaps, or for which the mesh or lightmap isn't
/// loaded, won't have entries in this table.
pub(crate) render_lightmaps: MainEntityHashMap<RenderLightmap>,
/// The slabs (binding arrays) containing the lightmaps.
pub(crate) slabs: Vec<LightmapSlab>,
free_slabs: FixedBitSet,
pending_lightmaps: HashSet<(LightmapSlabIndex, LightmapSlotIndex)>,
/// Whether bindless textures are supported on this platform.
pub(crate) bindless_supported: bool,
}
/// A binding array that contains lightmaps.
///
/// This will have a single binding if bindless lightmaps aren't in use.
pub struct LightmapSlab {
/// The GPU images in this slab.
lightmaps: Vec<AllocatedLightmap>,
free_slots_bitmask: u32,
}
struct AllocatedLightmap {
gpu_image: GpuImage,
// This will only be present if the lightmap is allocated but not loaded.
asset_id: Option<AssetId<Image>>,
}
/// The index of the slab (binding array) in which a lightmap is located.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deref, DerefMut)]
#[repr(transparent)]
pub struct LightmapSlabIndex(pub(crate) NonMaxU32);
/// The index of the slot (element within the binding array) in the slab in
/// which a lightmap is located.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deref, DerefMut)]
#[repr(transparent)]
pub struct LightmapSlotIndex(pub(crate) NonMaxU16);
impl Plugin for LightmapPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
LIGHTMAP_SHADER_HANDLE,
"lightmap.wgsl",
Shader::from_wgsl
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<RenderLightmaps>()
.add_systems(ExtractSchedule, extract_lightmaps.after(ExtractMeshesSet));
}
}
/// Extracts all lightmaps from the scene and populates the [`RenderLightmaps`]
/// resource.
fn extract_lightmaps(
render_lightmaps: ResMut<RenderLightmaps>,
changed_lightmaps_query: Extract<
Query<
(Entity, &ViewVisibility, &Lightmap),
Or<(Changed<ViewVisibility>, Changed<Lightmap>)>,
>,
>,
mut removed_lightmaps_query: Extract<RemovedComponents<Lightmap>>,
images: Res<RenderAssets<GpuImage>>,
fallback_images: Res<FallbackImage>,
) {
let render_lightmaps = render_lightmaps.into_inner();
// Loop over each entity.
for (entity, view_visibility, lightmap) in changed_lightmaps_query.iter() {
if render_lightmaps
.render_lightmaps
.contains_key(&MainEntity::from(entity))
{
continue;
}
// Only process visible entities.
if !view_visibility.get() {
continue;
}
let (slab_index, slot_index) =
render_lightmaps.allocate(&fallback_images, lightmap.image.id());
render_lightmaps.render_lightmaps.insert(
entity.into(),
RenderLightmap::new(
lightmap.uv_rect,
slab_index,
slot_index,
lightmap.bicubic_sampling,
),
);
render_lightmaps
.pending_lightmaps
.insert((slab_index, slot_index));
}
for entity in removed_lightmaps_query.read() {
if changed_lightmaps_query.contains(entity) {
continue;
}
let Some(RenderLightmap {
slab_index,
slot_index,
..
}) = render_lightmaps
.render_lightmaps
.remove(&MainEntity::from(entity))
else {
continue;
};
render_lightmaps.remove(&fallback_images, slab_index, slot_index);
render_lightmaps
.pending_lightmaps
.remove(&(slab_index, slot_index));
}
render_lightmaps
.pending_lightmaps
.retain(|&(slab_index, slot_index)| {
let Some(asset_id) = render_lightmaps.slabs[usize::from(slab_index)].lightmaps
[usize::from(slot_index)]
.asset_id
else {
error!(
"Allocated lightmap should have been removed from `pending_lightmaps` by now"
);
return false;
};
let Some(gpu_image) = images.get(asset_id) else {
return true;
};
render_lightmaps.slabs[usize::from(slab_index)].insert(slot_index, gpu_image.clone());
false
});
}
impl RenderLightmap {
/// Creates a new lightmap from a texture, a UV rect, and a slab and slot
/// index pair.
fn new(
uv_rect: Rect,
slab_index: LightmapSlabIndex,
slot_index: LightmapSlotIndex,
bicubic_sampling: bool,
) -> Self {
Self {
uv_rect,
slab_index,
slot_index,
bicubic_sampling,
}
}
}
/// Packs the lightmap UV rect into 64 bits (4 16-bit unsigned integers).
pub(crate) fn pack_lightmap_uv_rect(maybe_rect: Option<Rect>) -> UVec2 {
match maybe_rect {
Some(rect) => {
let rect_uvec4 = (vec4(rect.min.x, rect.min.y, rect.max.x, rect.max.y) * 65535.0)
.round()
.as_uvec4();
uvec2(
rect_uvec4.x | (rect_uvec4.y << 16),
rect_uvec4.z | (rect_uvec4.w << 16),
)
}
None => UVec2::ZERO,
}
}
impl Default for Lightmap {
fn default() -> Self {
Self {
image: Default::default(),
uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0),
bicubic_sampling: false,
}
}
}
impl FromWorld for RenderLightmaps {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let render_adapter = world.resource::<RenderAdapter>();
let bindless_supported = binding_arrays_are_usable(render_device, render_adapter);
RenderLightmaps {
render_lightmaps: default(),
slabs: vec![],
free_slabs: FixedBitSet::new(),
pending_lightmaps: default(),
bindless_supported,
}
}
}
impl RenderLightmaps {
/// Creates a new slab, appends it to the end of the list, and returns its
/// slab index.
fn create_slab(&mut self, fallback_images: &FallbackImage) -> LightmapSlabIndex {
let slab_index = LightmapSlabIndex::from(self.slabs.len());
self.free_slabs.grow_and_insert(slab_index.into());
self.slabs
.push(LightmapSlab::new(fallback_images, self.bindless_supported));
slab_index
}
fn allocate(
&mut self,
fallback_images: &FallbackImage,
image_id: AssetId<Image>,
) -> (LightmapSlabIndex, LightmapSlotIndex) {
let slab_index = match self.free_slabs.minimum() {
None => self.create_slab(fallback_images),
Some(slab_index) => slab_index.into(),
};
let slab = &mut self.slabs[usize::from(slab_index)];
let slot_index = slab.allocate(image_id);
if slab.is_full() {
self.free_slabs.remove(slab_index.into());
}
(slab_index, slot_index)
}
fn remove(
&mut self,
fallback_images: &FallbackImage,
slab_index: LightmapSlabIndex,
slot_index: LightmapSlotIndex,
) {
let slab = &mut self.slabs[usize::from(slab_index)];
slab.remove(fallback_images, slot_index);
if !slab.is_full() {
self.free_slabs.grow_and_insert(slot_index.into());
}
}
}
impl LightmapSlab {
fn new(fallback_images: &FallbackImage, bindless_supported: bool) -> LightmapSlab {
let count = if bindless_supported {
LIGHTMAPS_PER_SLAB
} else {
1
};
LightmapSlab {
lightmaps: (0..count)
.map(|_| AllocatedLightmap {
gpu_image: fallback_images.d2.clone(),
asset_id: None,
})
.collect(),
free_slots_bitmask: (1 << count) - 1,
}
}
fn is_full(&self) -> bool {
self.free_slots_bitmask == 0
}
fn allocate(&mut self, image_id: AssetId<Image>) -> LightmapSlotIndex {
let index = LightmapSlotIndex::from(self.free_slots_bitmask.trailing_zeros());
self.free_slots_bitmask &= !(1 << u32::from(index));
self.lightmaps[usize::from(index)].asset_id = Some(image_id);
index
}
fn insert(&mut self, index: LightmapSlotIndex, gpu_image: GpuImage) {
self.lightmaps[usize::from(index)] = AllocatedLightmap {
gpu_image,
asset_id: None,
}
}
fn remove(&mut self, fallback_images: &FallbackImage, index: LightmapSlotIndex) {
self.lightmaps[usize::from(index)] = AllocatedLightmap {
gpu_image: fallback_images.d2.clone(),
asset_id: None,
};
self.free_slots_bitmask |= 1 << u32::from(index);
}
/// Returns the texture views and samplers for the lightmaps in this slab,
/// ready to be placed into a bind group.
///
/// This is used when constructing bind groups in bindless mode. Before
/// returning, this function pads out the arrays with fallback images in
/// order to fulfill requirements of platforms that require full binding
/// arrays (e.g. DX12).
pub(crate) fn build_binding_arrays(&self) -> (Vec<&WgpuTextureView>, Vec<&WgpuSampler>) {
(
self.lightmaps
.iter()
.map(|allocated_lightmap| &*allocated_lightmap.gpu_image.texture_view)
.collect(),
self.lightmaps
.iter()
.map(|allocated_lightmap| &*allocated_lightmap.gpu_image.sampler)
.collect(),
)
}
/// Returns the texture view and sampler corresponding to the first
/// lightmap, which must exist.
///
/// This is used when constructing bind groups in non-bindless mode.
pub(crate) fn bindings_for_first_lightmap(&self) -> (&TextureView, &Sampler) {
(
&self.lightmaps[0].gpu_image.texture_view,
&self.lightmaps[0].gpu_image.sampler,
)
}
}
impl From<u32> for LightmapSlabIndex {
fn from(value: u32) -> Self {
Self(NonMaxU32::new(value).unwrap())
}
}
impl From<usize> for LightmapSlabIndex {
fn from(value: usize) -> Self {
Self::from(value as u32)
}
}
impl From<u32> for LightmapSlotIndex {
fn from(value: u32) -> Self {
Self(NonMaxU16::new(value as u16).unwrap())
}
}
impl From<usize> for LightmapSlotIndex {
fn from(value: usize) -> Self {
Self::from(value as u32)
}
}
impl From<LightmapSlabIndex> for usize {
fn from(value: LightmapSlabIndex) -> Self {
value.0.get() as usize
}
}
impl From<LightmapSlotIndex> for usize {
fn from(value: LightmapSlotIndex) -> Self {
value.0.get() as usize
}
}
impl From<LightmapSlotIndex> for u16 {
fn from(value: LightmapSlotIndex) -> Self {
value.0.get()
}
}
impl From<LightmapSlotIndex> for u32 {
fn from(value: LightmapSlotIndex) -> Self {
value.0.get() as u32
}
}

1614
vendor/bevy_pbr/src/material.rs vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

75
vendor/bevy_pbr/src/mesh_material.rs vendored Normal file
View File

@@ -0,0 +1,75 @@
use crate::Material;
use bevy_asset::{AsAssetId, AssetId, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use derive_more::derive::From;
/// A [material](Material) used for rendering a [`Mesh3d`].
///
/// See [`Material`] for general information about 3D materials and how to implement your own materials.
///
/// [`Mesh3d`]: bevy_render::mesh::Mesh3d
///
/// # Example
///
/// ```
/// # use bevy_pbr::{Material, MeshMaterial3d, StandardMaterial};
/// # use bevy_ecs::prelude::*;
/// # use bevy_render::mesh::{Mesh, Mesh3d};
/// # use bevy_color::palettes::basic::RED;
/// # use bevy_asset::Assets;
/// # use bevy_math::primitives::Capsule3d;
/// #
/// // Spawn an entity with a mesh using `StandardMaterial`.
/// fn setup(
/// mut commands: Commands,
/// mut meshes: ResMut<Assets<Mesh>>,
/// mut materials: ResMut<Assets<StandardMaterial>>,
/// ) {
/// commands.spawn((
/// Mesh3d(meshes.add(Capsule3d::default())),
/// MeshMaterial3d(materials.add(StandardMaterial {
/// base_color: RED.into(),
/// ..Default::default()
/// })),
/// ));
/// }
/// ```
#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, From)]
#[reflect(Component, Default, Clone, PartialEq)]
pub struct MeshMaterial3d<M: Material>(pub Handle<M>);
impl<M: Material> Default for MeshMaterial3d<M> {
fn default() -> Self {
Self(Handle::default())
}
}
impl<M: Material> PartialEq for MeshMaterial3d<M> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<M: Material> Eq for MeshMaterial3d<M> {}
impl<M: Material> From<MeshMaterial3d<M>> for AssetId<M> {
fn from(material: MeshMaterial3d<M>) -> Self {
material.id()
}
}
impl<M: Material> From<&MeshMaterial3d<M>> for AssetId<M> {
fn from(material: &MeshMaterial3d<M>) -> Self {
material.id()
}
}
impl<M: Material> AsAssetId for MeshMaterial3d<M> {
type Asset = M;
fn as_asset_id(&self) -> AssetId<Self::Asset> {
self.id()
}
}

276
vendor/bevy_pbr/src/meshlet/asset.rs vendored Normal file
View File

@@ -0,0 +1,276 @@
use alloc::sync::Arc;
use bevy_asset::{
io::{Reader, Writer},
saver::{AssetSaver, SavedAsset},
Asset, AssetLoader, AsyncReadExt, AsyncWriteExt, LoadContext,
};
use bevy_math::{Vec2, Vec3};
use bevy_reflect::TypePath;
use bevy_tasks::block_on;
use bytemuck::{Pod, Zeroable};
use half::f16;
use lz4_flex::frame::{FrameDecoder, FrameEncoder};
use std::io::{Read, Write};
use thiserror::Error;
/// Unique identifier for the [`MeshletMesh`] asset format.
const MESHLET_MESH_ASSET_MAGIC: u64 = 1717551717668;
/// The current version of the [`MeshletMesh`] asset format.
pub const MESHLET_MESH_ASSET_VERSION: u64 = 1;
/// A mesh that has been pre-processed into multiple small clusters of triangles called meshlets.
///
/// A [`bevy_render::mesh::Mesh`] can be converted to a [`MeshletMesh`] using `MeshletMesh::from_mesh` when the `meshlet_processor` cargo feature is enabled.
/// The conversion step is very slow, and is meant to be ran once ahead of time, and not during runtime. This type of mesh is not suitable for
/// dynamically generated geometry.
///
/// There are restrictions on the [`crate::Material`] functionality that can be used with this type of mesh.
/// * Materials have no control over the vertex shader or vertex attributes.
/// * Materials must be opaque. Transparent, alpha masked, and transmissive materials are not supported.
/// * Do not use normal maps baked from higher-poly geometry. Use the high-poly geometry directly and skip the normal map.
/// * If additional detail is needed, a smaller tiling normal map not baked from a mesh is ok.
/// * Material shaders must not use builtin functions that automatically calculate derivatives <https://gpuweb.github.io/gpuweb/wgsl/#derivatives>.
/// * Performing manual arithmetic on texture coordinates (UVs) is forbidden. Use the chain-rule version of arithmetic functions instead (TODO: not yet implemented).
/// * Limited control over [`bevy_render::render_resource::RenderPipelineDescriptor`] attributes.
/// * Materials must use the [`crate::Material::meshlet_mesh_fragment_shader`] method (and similar variants for prepass/deferred shaders)
/// which requires certain shader patterns that differ from the regular material shaders.
///
/// See also [`super::MeshletMesh3d`] and [`super::MeshletPlugin`].
#[derive(Asset, TypePath, Clone)]
pub struct MeshletMesh {
/// Quantized and bitstream-packed vertex positions for meshlet vertices.
pub(crate) vertex_positions: Arc<[u32]>,
/// Octahedral-encoded and 2x16snorm packed normals for meshlet vertices.
pub(crate) vertex_normals: Arc<[u32]>,
/// Uncompressed vertex texture coordinates for meshlet vertices.
pub(crate) vertex_uvs: Arc<[Vec2]>,
/// Triangle indices for meshlets.
pub(crate) indices: Arc<[u8]>,
/// The list of meshlets making up this mesh.
pub(crate) meshlets: Arc<[Meshlet]>,
/// Spherical bounding volumes.
pub(crate) meshlet_bounding_spheres: Arc<[MeshletBoundingSpheres]>,
/// Meshlet group and parent group simplification errors.
pub(crate) meshlet_simplification_errors: Arc<[MeshletSimplificationError]>,
}
/// A single meshlet within a [`MeshletMesh`].
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct Meshlet {
/// The bit offset within the parent mesh's [`MeshletMesh::vertex_positions`] buffer where the vertex positions for this meshlet begin.
pub start_vertex_position_bit: u32,
/// The offset within the parent mesh's [`MeshletMesh::vertex_normals`] and [`MeshletMesh::vertex_uvs`] buffers
/// where non-position vertex attributes for this meshlet begin.
pub start_vertex_attribute_id: u32,
/// The offset within the parent mesh's [`MeshletMesh::indices`] buffer where the indices for this meshlet begin.
pub start_index_id: u32,
/// The amount of vertices in this meshlet.
pub vertex_count: u8,
/// The amount of triangles in this meshlet.
pub triangle_count: u8,
/// Unused.
pub padding: u16,
/// Number of bits used to store the X channel of vertex positions within this meshlet.
pub bits_per_vertex_position_channel_x: u8,
/// Number of bits used to store the Y channel of vertex positions within this meshlet.
pub bits_per_vertex_position_channel_y: u8,
/// Number of bits used to store the Z channel of vertex positions within this meshlet.
pub bits_per_vertex_position_channel_z: u8,
/// Power of 2 factor used to quantize vertex positions within this meshlet.
pub vertex_position_quantization_factor: u8,
/// Minimum quantized X channel value of vertex positions within this meshlet.
pub min_vertex_position_channel_x: f32,
/// Minimum quantized Y channel value of vertex positions within this meshlet.
pub min_vertex_position_channel_y: f32,
/// Minimum quantized Z channel value of vertex positions within this meshlet.
pub min_vertex_position_channel_z: f32,
}
/// Bounding spheres used for culling and choosing level of detail for a [`Meshlet`].
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct MeshletBoundingSpheres {
/// Bounding sphere used for frustum and occlusion culling for this meshlet.
pub culling_sphere: MeshletBoundingSphere,
/// Bounding sphere used for determining if this meshlet's group is at the correct level of detail for a given view.
pub lod_group_sphere: MeshletBoundingSphere,
/// Bounding sphere used for determining if this meshlet's parent group is at the correct level of detail for a given view.
pub lod_parent_group_sphere: MeshletBoundingSphere,
}
/// A spherical bounding volume used for a [`Meshlet`].
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct MeshletBoundingSphere {
pub center: Vec3,
pub radius: f32,
}
/// Simplification error used for choosing level of detail for a [`Meshlet`].
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct MeshletSimplificationError {
/// Simplification error used for determining if this meshlet's group is at the correct level of detail for a given view.
pub group_error: f16,
/// Simplification error used for determining if this meshlet's parent group is at the correct level of detail for a given view.
pub parent_group_error: f16,
}
/// An [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets.
pub struct MeshletMeshSaver;
impl AssetSaver for MeshletMeshSaver {
type Asset = MeshletMesh;
type Settings = ();
type OutputLoader = MeshletMeshLoader;
type Error = MeshletMeshSaveOrLoadError;
async fn save(
&self,
writer: &mut Writer,
asset: SavedAsset<'_, MeshletMesh>,
_settings: &(),
) -> Result<(), MeshletMeshSaveOrLoadError> {
// Write asset magic number
writer
.write_all(&MESHLET_MESH_ASSET_MAGIC.to_le_bytes())
.await?;
// Write asset version
writer
.write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes())
.await?;
// Compress and write asset data
let mut writer = FrameEncoder::new(AsyncWriteSyncAdapter(writer));
write_slice(&asset.vertex_positions, &mut writer)?;
write_slice(&asset.vertex_normals, &mut writer)?;
write_slice(&asset.vertex_uvs, &mut writer)?;
write_slice(&asset.indices, &mut writer)?;
write_slice(&asset.meshlets, &mut writer)?;
write_slice(&asset.meshlet_bounding_spheres, &mut writer)?;
write_slice(&asset.meshlet_simplification_errors, &mut writer)?;
writer.finish()?;
Ok(())
}
}
/// An [`AssetLoader`] for `.meshlet_mesh` [`MeshletMesh`] assets.
pub struct MeshletMeshLoader;
impl AssetLoader for MeshletMeshLoader {
type Asset = MeshletMesh;
type Settings = ();
type Error = MeshletMeshSaveOrLoadError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &(),
_load_context: &mut LoadContext<'_>,
) -> Result<MeshletMesh, MeshletMeshSaveOrLoadError> {
// Load and check magic number
let magic = async_read_u64(reader).await?;
if magic != MESHLET_MESH_ASSET_MAGIC {
return Err(MeshletMeshSaveOrLoadError::WrongFileType);
}
// Load and check asset version
let version = async_read_u64(reader).await?;
if version != MESHLET_MESH_ASSET_VERSION {
return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version });
}
// Load and decompress asset data
let reader = &mut FrameDecoder::new(AsyncReadSyncAdapter(reader));
let vertex_positions = read_slice(reader)?;
let vertex_normals = read_slice(reader)?;
let vertex_uvs = read_slice(reader)?;
let indices = read_slice(reader)?;
let meshlets = read_slice(reader)?;
let meshlet_bounding_spheres = read_slice(reader)?;
let meshlet_simplification_errors = read_slice(reader)?;
Ok(MeshletMesh {
vertex_positions,
vertex_normals,
vertex_uvs,
indices,
meshlets,
meshlet_bounding_spheres,
meshlet_simplification_errors,
})
}
fn extensions(&self) -> &[&str] {
&["meshlet_mesh"]
}
}
#[derive(Error, Debug)]
pub enum MeshletMeshSaveOrLoadError {
#[error("file was not a MeshletMesh asset")]
WrongFileType,
#[error("expected asset version {MESHLET_MESH_ASSET_VERSION} but found version {found}")]
WrongVersion { found: u64 },
#[error("failed to compress or decompress asset data")]
CompressionOrDecompression(#[from] lz4_flex::frame::Error),
#[error("failed to read or write asset data")]
Io(#[from] std::io::Error),
}
async fn async_read_u64(reader: &mut dyn Reader) -> Result<u64, std::io::Error> {
let mut bytes = [0u8; 8];
reader.read_exact(&mut bytes).await?;
Ok(u64::from_le_bytes(bytes))
}
fn read_u64(reader: &mut dyn Read) -> Result<u64, std::io::Error> {
let mut bytes = [0u8; 8];
reader.read_exact(&mut bytes)?;
Ok(u64::from_le_bytes(bytes))
}
fn write_slice<T: Pod>(
field: &[T],
writer: &mut dyn Write,
) -> Result<(), MeshletMeshSaveOrLoadError> {
writer.write_all(&(field.len() as u64).to_le_bytes())?;
writer.write_all(bytemuck::cast_slice(field))?;
Ok(())
}
fn read_slice<T: Pod>(reader: &mut dyn Read) -> Result<Arc<[T]>, std::io::Error> {
let len = read_u64(reader)? as usize;
let mut data: Arc<[T]> = core::iter::repeat_with(T::zeroed).take(len).collect();
let slice = Arc::get_mut(&mut data).unwrap();
reader.read_exact(bytemuck::cast_slice_mut(slice))?;
Ok(data)
}
// TODO: Use async for everything and get rid of this adapter
struct AsyncWriteSyncAdapter<'a>(&'a mut Writer);
impl Write for AsyncWriteSyncAdapter<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
block_on(self.0.write(buf))
}
fn flush(&mut self) -> std::io::Result<()> {
block_on(self.0.flush())
}
}
// TODO: Use async for everything and get rid of this adapter
struct AsyncReadSyncAdapter<'a>(&'a mut dyn Reader);
impl Read for AsyncReadSyncAdapter<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
block_on(self.0.read(buf))
}
}

View File

@@ -0,0 +1,18 @@
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
@group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d<r64uint, write>;
#else
@group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d<r32uint, write>;
#endif
var<push_constant> view_size: vec2<u32>;
@compute
@workgroup_size(16, 16, 1)
fn clear_visibility_buffer(@builtin(global_invocation_id) global_id: vec3<u32>) {
if any(global_id.xy >= view_size) { return; }
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
textureStore(meshlet_visibility_buffer, global_id.xy, vec4(0lu));
#else
textureStore(meshlet_visibility_buffer, global_id.xy, vec4(0u));
#endif
}

View File

@@ -0,0 +1,194 @@
#import bevy_pbr::meshlet_bindings::{
meshlet_cluster_meshlet_ids,
meshlet_bounding_spheres,
meshlet_simplification_errors,
meshlet_cluster_instance_ids,
meshlet_instance_uniforms,
meshlet_second_pass_candidates,
depth_pyramid,
view,
previous_view,
should_cull_instance,
cluster_is_second_pass_candidate,
meshlet_software_raster_indirect_args,
meshlet_hardware_raster_indirect_args,
meshlet_raster_clusters,
constants,
MeshletBoundingSphere,
}
#import bevy_render::maths::affine3_to_square
/// Culls individual clusters (1 per thread) in two passes (two pass occlusion culling), and outputs a bitmask of which clusters survived.
/// 1. The first pass tests instance visibility, frustum culling, LOD selection, and finally occlusion culling using last frame's depth pyramid.
/// 2. The second pass performs occlusion culling (using the depth buffer generated from the first pass) on all clusters that passed
/// the instance, frustum, and LOD tests in the first pass, but were not visible last frame according to the occlusion culling.
@compute
@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1 cluster per thread
fn cull_clusters(
@builtin(workgroup_id) workgroup_id: vec3<u32>,
@builtin(num_workgroups) num_workgroups: vec3<u32>,
@builtin(local_invocation_index) local_invocation_index: u32,
) {
// Calculate the cluster ID for this thread
let cluster_id = local_invocation_index + 128u * dot(workgroup_id, vec3(num_workgroups.x * num_workgroups.x, num_workgroups.x, 1u));
if cluster_id >= constants.scene_cluster_count { return; }
#ifdef MESHLET_SECOND_CULLING_PASS
if !cluster_is_second_pass_candidate(cluster_id) { return; }
#endif
// Check for instance culling
let instance_id = meshlet_cluster_instance_ids[cluster_id];
#ifdef MESHLET_FIRST_CULLING_PASS
if should_cull_instance(instance_id) { return; }
#endif
// Calculate world-space culling bounding sphere for the cluster
let instance_uniform = meshlet_instance_uniforms[instance_id];
let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id];
let world_from_local = affine3_to_square(instance_uniform.world_from_local);
let world_scale = max(length(world_from_local[0]), max(length(world_from_local[1]), length(world_from_local[2])));
let bounding_spheres = meshlet_bounding_spheres[meshlet_id];
let culling_bounding_sphere_center = world_from_local * vec4(bounding_spheres.culling_sphere.center, 1.0);
let culling_bounding_sphere_radius = world_scale * bounding_spheres.culling_sphere.radius;
#ifdef MESHLET_FIRST_CULLING_PASS
// Frustum culling
// TODO: Faster method from https://vkguide.dev/docs/gpudriven/compute_culling/#frustum-culling-function
for (var i = 0u; i < 6u; i++) {
if dot(view.frustum[i], culling_bounding_sphere_center) + culling_bounding_sphere_radius <= 0.0 {
return;
}
}
// Check LOD cut (cluster group error imperceptible, and parent group error not imperceptible)
let simplification_errors = unpack2x16float(meshlet_simplification_errors[meshlet_id]);
let lod_is_ok = lod_error_is_imperceptible(bounding_spheres.lod_group_sphere, simplification_errors.x, world_from_local, world_scale);
let parent_lod_is_ok = lod_error_is_imperceptible(bounding_spheres.lod_parent_group_sphere, simplification_errors.y, world_from_local, world_scale);
if !lod_is_ok || parent_lod_is_ok { return; }
#endif
// Project the culling bounding sphere to view-space for occlusion culling
#ifdef MESHLET_FIRST_CULLING_PASS
let previous_world_from_local = affine3_to_square(instance_uniform.previous_world_from_local);
let previous_world_from_local_scale = max(length(previous_world_from_local[0]), max(length(previous_world_from_local[1]), length(previous_world_from_local[2])));
let occlusion_culling_bounding_sphere_center = previous_world_from_local * vec4(bounding_spheres.culling_sphere.center, 1.0);
let occlusion_culling_bounding_sphere_radius = previous_world_from_local_scale * bounding_spheres.culling_sphere.radius;
let occlusion_culling_bounding_sphere_center_view_space = (previous_view.view_from_world * vec4(occlusion_culling_bounding_sphere_center.xyz, 1.0)).xyz;
#else
let occlusion_culling_bounding_sphere_center = culling_bounding_sphere_center;
let occlusion_culling_bounding_sphere_radius = culling_bounding_sphere_radius;
let occlusion_culling_bounding_sphere_center_view_space = (view.view_from_world * vec4(occlusion_culling_bounding_sphere_center.xyz, 1.0)).xyz;
#endif
var aabb = project_view_space_sphere_to_screen_space_aabb(occlusion_culling_bounding_sphere_center_view_space, occlusion_culling_bounding_sphere_radius);
let depth_pyramid_size_mip_0 = vec2<f32>(textureDimensions(depth_pyramid, 0));
var aabb_width_pixels = (aabb.z - aabb.x) * depth_pyramid_size_mip_0.x;
var aabb_height_pixels = (aabb.w - aabb.y) * depth_pyramid_size_mip_0.y;
let depth_level = max(0, i32(ceil(log2(max(aabb_width_pixels, aabb_height_pixels))))); // TODO: Naga doesn't like this being a u32
let depth_pyramid_size = vec2<f32>(textureDimensions(depth_pyramid, depth_level));
let aabb_top_left = vec2<u32>(aabb.xy * depth_pyramid_size);
let depth_quad_a = textureLoad(depth_pyramid, aabb_top_left, depth_level).x;
let depth_quad_b = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 0u), depth_level).x;
let depth_quad_c = textureLoad(depth_pyramid, aabb_top_left + vec2(0u, 1u), depth_level).x;
let depth_quad_d = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 1u), depth_level).x;
let occluder_depth = min(min(depth_quad_a, depth_quad_b), min(depth_quad_c, depth_quad_d));
// Check whether or not the cluster would be occluded if drawn
var cluster_visible: bool;
if view.clip_from_view[3][3] == 1.0 {
// Orthographic
let sphere_depth = view.clip_from_view[3][2] + (occlusion_culling_bounding_sphere_center_view_space.z + occlusion_culling_bounding_sphere_radius) * view.clip_from_view[2][2];
cluster_visible = sphere_depth >= occluder_depth;
} else {
// Perspective
let sphere_depth = -view.clip_from_view[3][2] / (occlusion_culling_bounding_sphere_center_view_space.z + occlusion_culling_bounding_sphere_radius);
cluster_visible = sphere_depth >= occluder_depth;
}
// Write if the cluster should be occlusion tested in the second pass
#ifdef MESHLET_FIRST_CULLING_PASS
if !cluster_visible {
let bit = 1u << cluster_id % 32u;
atomicOr(&meshlet_second_pass_candidates[cluster_id / 32u], bit);
}
#endif
// Cluster would be occluded if drawn, so don't setup a draw for it
if !cluster_visible { return; }
// Check how big the cluster is in screen space
#ifdef MESHLET_FIRST_CULLING_PASS
let culling_bounding_sphere_center_view_space = (view.view_from_world * vec4(culling_bounding_sphere_center.xyz, 1.0)).xyz;
aabb = project_view_space_sphere_to_screen_space_aabb(culling_bounding_sphere_center_view_space, culling_bounding_sphere_radius);
aabb_width_pixels = (aabb.z - aabb.x) * view.viewport.z;
aabb_height_pixels = (aabb.w - aabb.y) * view.viewport.w;
#endif
let cluster_is_small = all(vec2(aabb_width_pixels, aabb_height_pixels) < vec2(64.0));
// Let the hardware rasterizer handle near-plane clipping
let not_intersects_near_plane = dot(view.frustum[4u], culling_bounding_sphere_center) > culling_bounding_sphere_radius;
var buffer_slot: u32;
if cluster_is_small && not_intersects_near_plane {
// Append this cluster to the list for software rasterization
buffer_slot = atomicAdd(&meshlet_software_raster_indirect_args.x, 1u);
} else {
// Append this cluster to the list for hardware rasterization
buffer_slot = atomicAdd(&meshlet_hardware_raster_indirect_args.instance_count, 1u);
buffer_slot = constants.meshlet_raster_cluster_rightmost_slot - buffer_slot;
}
meshlet_raster_clusters[buffer_slot] = cluster_id;
}
// https://github.com/zeux/meshoptimizer/blob/1e48e96c7e8059321de492865165e9ef071bffba/demo/nanite.cpp#L115
fn lod_error_is_imperceptible(lod_sphere: MeshletBoundingSphere, simplification_error: f32, world_from_local: mat4x4<f32>, world_scale: f32) -> bool {
let sphere_world_space = (world_from_local * vec4(lod_sphere.center, 1.0)).xyz;
let radius_world_space = world_scale * lod_sphere.radius;
let error_world_space = world_scale * simplification_error;
var projected_error = error_world_space;
if view.clip_from_view[3][3] != 1.0 {
// Perspective
let distance_to_closest_point_on_sphere = distance(sphere_world_space, view.world_position) - radius_world_space;
let distance_to_closest_point_on_sphere_clamped_to_znear = max(distance_to_closest_point_on_sphere, view.clip_from_view[3][2]);
projected_error /= distance_to_closest_point_on_sphere_clamped_to_znear;
}
projected_error *= view.clip_from_view[1][1] * 0.5;
projected_error *= view.viewport.w;
return projected_error < 1.0;
}
// https://zeux.io/2023/01/12/approximate-projected-bounds
fn project_view_space_sphere_to_screen_space_aabb(cp: vec3<f32>, r: f32) -> vec4<f32> {
let inv_width = view.clip_from_view[0][0] * 0.5;
let inv_height = view.clip_from_view[1][1] * 0.5;
if view.clip_from_view[3][3] == 1.0 {
// Orthographic
let min_x = cp.x - r;
let max_x = cp.x + r;
let min_y = cp.y - r;
let max_y = cp.y + r;
return vec4(min_x * inv_width, 1.0 - max_y * inv_height, max_x * inv_width, 1.0 - min_y * inv_height);
} else {
// Perspective
let c = vec3(cp.xy, -cp.z);
let cr = c * r;
let czr2 = c.z * c.z - r * r;
let vx = sqrt(c.x * c.x + czr2);
let min_x = (vx * c.x - cr.z) / (vx * c.z + cr.x);
let max_x = (vx * c.x + cr.z) / (vx * c.z - cr.x);
let vy = sqrt(c.y * c.y + czr2);
let min_y = (vy * c.y - cr.z) / (vy * c.z + cr.y);
let max_y = (vy * c.y + cr.z) / (vy * c.z - cr.y);
return vec4(min_x * inv_width, -max_y * inv_height, max_x * inv_width, -min_y * inv_height) + vec4(0.5);
}
}

View File

@@ -0,0 +1,4 @@
#define_import_path bevy_pbr::meshlet_visibility_buffer_resolve
/// Dummy shader to prevent naga_oil from complaining about missing imports when the MeshletPlugin is not loaded,
/// as naga_oil tries to resolve imports even if they're behind an #ifdef.

View File

@@ -0,0 +1,50 @@
#import bevy_pbr::meshlet_bindings::{
scene_instance_count,
meshlet_global_cluster_count,
meshlet_instance_meshlet_counts,
meshlet_instance_meshlet_slice_starts,
meshlet_cluster_instance_ids,
meshlet_cluster_meshlet_ids,
}
/// Writes out instance_id and meshlet_id to the global buffers for each cluster in the scene.
var<workgroup> cluster_slice_start_workgroup: u32;
@compute
@workgroup_size(1024, 1, 1) // 1024 threads per workgroup, 1 instance per workgroup
fn fill_cluster_buffers(
@builtin(workgroup_id) workgroup_id: vec3<u32>,
@builtin(num_workgroups) num_workgroups: vec3<u32>,
@builtin(local_invocation_index) local_invocation_index: u32,
) {
// Calculate the instance ID for this workgroup
var instance_id = workgroup_id.x + (workgroup_id.y * num_workgroups.x);
if instance_id >= scene_instance_count { return; }
let instance_meshlet_count = meshlet_instance_meshlet_counts[instance_id];
let instance_meshlet_slice_start = meshlet_instance_meshlet_slice_starts[instance_id];
// Reserve cluster slots for the instance and broadcast to the workgroup
if local_invocation_index == 0u {
cluster_slice_start_workgroup = atomicAdd(&meshlet_global_cluster_count, instance_meshlet_count);
}
let cluster_slice_start = workgroupUniformLoad(&cluster_slice_start_workgroup);
// Loop enough times to write out all the meshlets for the instance given that each thread writes 1 meshlet in each iteration
for (var clusters_written = 0u; clusters_written < instance_meshlet_count; clusters_written += 1024u) {
// Calculate meshlet ID within this instance's MeshletMesh to process for this thread
let meshlet_id_local = clusters_written + local_invocation_index;
if meshlet_id_local >= instance_meshlet_count { return; }
// Find the overall cluster ID in the global cluster buffer
let cluster_id = cluster_slice_start + meshlet_id_local;
// Find the overall meshlet ID in the global meshlet buffer
let meshlet_id = instance_meshlet_slice_start + meshlet_id_local;
// Write results to buffers
meshlet_cluster_instance_ids[cluster_id] = instance_id;
meshlet_cluster_meshlet_ids[cluster_id] = meshlet_id;
}
}

733
vendor/bevy_pbr/src/meshlet/from_mesh.rs vendored Normal file
View File

@@ -0,0 +1,733 @@
use super::asset::{
Meshlet, MeshletBoundingSphere, MeshletBoundingSpheres, MeshletMesh, MeshletSimplificationError,
};
use alloc::borrow::Cow;
use bevy_math::{ops::log2, IVec3, Vec2, Vec3, Vec3Swizzles};
use bevy_platform::collections::HashMap;
use bevy_render::{
mesh::{Indices, Mesh},
render_resource::PrimitiveTopology,
};
use bitvec::{order::Lsb0, vec::BitVec, view::BitView};
use core::{iter, ops::Range};
use half::f16;
use itertools::Itertools;
use meshopt::{
build_meshlets, ffi::meshopt_Meshlet, generate_vertex_remap_multi,
simplify_with_attributes_and_locks, Meshlets, SimplifyOptions, VertexDataAdapter, VertexStream,
};
use metis::{option::Opt, Graph};
use smallvec::SmallVec;
use thiserror::Error;
// Aim to have 8 meshlets per group
const TARGET_MESHLETS_PER_GROUP: usize = 8;
// Reject groups that keep over 95% of their original triangles
const SIMPLIFICATION_FAILURE_PERCENTAGE: f32 = 0.95;
/// Default vertex position quantization factor for use with [`MeshletMesh::from_mesh`].
///
/// Snaps vertices to the nearest 1/16th of a centimeter (1/2^4).
pub const MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR: u8 = 4;
const CENTIMETERS_PER_METER: f32 = 100.0;
impl MeshletMesh {
/// Process a [`Mesh`] to generate a [`MeshletMesh`].
///
/// This process is very slow, and should be done ahead of time, and not at runtime.
///
/// # Requirements
///
/// This function requires the `meshlet_processor` cargo feature.
///
/// The input mesh must:
/// 1. Use [`PrimitiveTopology::TriangleList`]
/// 2. Use indices
/// 3. Have the exact following set of vertex attributes: `{POSITION, NORMAL, UV_0}` (tangents can be used in material shaders, but are calculated at runtime and are not stored in the mesh)
///
/// # Vertex precision
///
/// `vertex_position_quantization_factor` is the amount of precision to use when quantizing vertex positions.
///
/// Vertices are snapped to the nearest (1/2^x)th of a centimeter, where x = `vertex_position_quantization_factor`.
/// E.g. if x = 4, then vertices are snapped to the nearest 1/2^4 = 1/16th of a centimeter.
///
/// Use [`MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR`] as a default, adjusting lower to save memory and disk space, and higher to prevent artifacts if needed.
///
/// To ensure that two different meshes do not have cracks between them when placed directly next to each other:
/// * Use the same quantization factor when converting each mesh to a meshlet mesh
/// * Ensure that their [`bevy_transform::components::Transform::translation`]s are a multiple of 1/2^x centimeters (note that translations are in meters)
/// * Ensure that their [`bevy_transform::components::Transform::scale`]s are the same
/// * Ensure that their [`bevy_transform::components::Transform::rotation`]s are a multiple of 90 degrees
pub fn from_mesh(
mesh: &Mesh,
vertex_position_quantization_factor: u8,
) -> Result<Self, MeshToMeshletMeshConversionError> {
// Validate mesh format
let indices = validate_input_mesh(mesh)?;
// Get meshlet vertices
let vertex_buffer = mesh.create_packed_vertex_buffer_data();
let vertex_stride = mesh.get_vertex_size() as usize;
let vertices = VertexDataAdapter::new(&vertex_buffer, vertex_stride, 0).unwrap();
let vertex_normals = bytemuck::cast_slice(&vertex_buffer[12..16]);
// Generate a position-only vertex buffer for determining triangle/meshlet connectivity
let (position_only_vertex_count, position_only_vertex_remap) = generate_vertex_remap_multi(
vertices.vertex_count,
&[VertexStream::new_with_stride::<Vec3, _>(
vertex_buffer.as_ptr(),
vertex_stride,
)],
Some(&indices),
);
// Split the mesh into an initial list of meshlets (LOD 0)
let mut meshlets = compute_meshlets(
&indices,
&vertices,
&position_only_vertex_remap,
position_only_vertex_count,
);
let mut bounding_spheres = meshlets
.iter()
.map(|meshlet| compute_meshlet_bounds(meshlet, &vertices))
.map(|bounding_sphere| MeshletBoundingSpheres {
culling_sphere: bounding_sphere,
lod_group_sphere: bounding_sphere,
lod_parent_group_sphere: MeshletBoundingSphere {
center: Vec3::ZERO,
radius: 0.0,
},
})
.collect::<Vec<_>>();
let mut simplification_errors = iter::repeat_n(
MeshletSimplificationError {
group_error: f16::ZERO,
parent_group_error: f16::MAX,
},
meshlets.len(),
)
.collect::<Vec<_>>();
let mut vertex_locks = vec![false; vertices.vertex_count];
// Build further LODs
let mut simplification_queue = 0..meshlets.len();
while simplification_queue.len() > 1 {
// For each meshlet build a list of connected meshlets (meshlets that share a vertex)
let connected_meshlets_per_meshlet = find_connected_meshlets(
simplification_queue.clone(),
&meshlets,
&position_only_vertex_remap,
position_only_vertex_count,
);
// Group meshlets into roughly groups of size TARGET_MESHLETS_PER_GROUP,
// grouping meshlets with a high number of shared vertices
let groups = group_meshlets(
&connected_meshlets_per_meshlet,
simplification_queue.clone(),
);
// Lock borders between groups to prevent cracks when simplifying
lock_group_borders(
&mut vertex_locks,
&groups,
&meshlets,
&position_only_vertex_remap,
position_only_vertex_count,
);
let next_lod_start = meshlets.len();
for group_meshlets in groups.into_iter() {
// If the group only has a single meshlet we can't simplify it
if group_meshlets.len() == 1 {
continue;
}
// Simplify the group to ~50% triangle count
let Some((simplified_group_indices, mut group_error)) = simplify_meshlet_group(
&group_meshlets,
&meshlets,
&vertices,
vertex_normals,
vertex_stride,
&vertex_locks,
) else {
// Couldn't simplify the group enough
continue;
};
// Compute LOD data for the group
let group_bounding_sphere = compute_lod_group_data(
&group_meshlets,
&mut group_error,
&mut bounding_spheres,
&mut simplification_errors,
);
// Build new meshlets using the simplified group
let new_meshlets_count = split_simplified_group_into_new_meshlets(
&simplified_group_indices,
&vertices,
&position_only_vertex_remap,
position_only_vertex_count,
&mut meshlets,
);
// Calculate the culling bounding sphere for the new meshlets and set their LOD group data
let new_meshlet_ids = (meshlets.len() - new_meshlets_count)..meshlets.len();
bounding_spheres.extend(new_meshlet_ids.clone().map(|meshlet_id| {
MeshletBoundingSpheres {
culling_sphere: compute_meshlet_bounds(meshlets.get(meshlet_id), &vertices),
lod_group_sphere: group_bounding_sphere,
lod_parent_group_sphere: MeshletBoundingSphere {
center: Vec3::ZERO,
radius: 0.0,
},
}
}));
simplification_errors.extend(iter::repeat_n(
MeshletSimplificationError {
group_error,
parent_group_error: f16::MAX,
},
new_meshlet_ids.len(),
));
}
// Set simplification queue to the list of newly created meshlets
simplification_queue = next_lod_start..meshlets.len();
}
// Copy vertex attributes per meshlet and compress
let mut vertex_positions = BitVec::<u32, Lsb0>::new();
let mut vertex_normals = Vec::new();
let mut vertex_uvs = Vec::new();
let mut bevy_meshlets = Vec::with_capacity(meshlets.len());
for (i, meshlet) in meshlets.meshlets.iter().enumerate() {
build_and_compress_per_meshlet_vertex_data(
meshlet,
meshlets.get(i).vertices,
&vertex_buffer,
vertex_stride,
&mut vertex_positions,
&mut vertex_normals,
&mut vertex_uvs,
&mut bevy_meshlets,
vertex_position_quantization_factor,
);
}
vertex_positions.set_uninitialized(false);
Ok(Self {
vertex_positions: vertex_positions.into_vec().into(),
vertex_normals: vertex_normals.into(),
vertex_uvs: vertex_uvs.into(),
indices: meshlets.triangles.into(),
meshlets: bevy_meshlets.into(),
meshlet_bounding_spheres: bounding_spheres.into(),
meshlet_simplification_errors: simplification_errors.into(),
})
}
}
fn validate_input_mesh(mesh: &Mesh) -> Result<Cow<'_, [u32]>, MeshToMeshletMeshConversionError> {
if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
return Err(MeshToMeshletMeshConversionError::WrongMeshPrimitiveTopology);
}
if mesh.attributes().map(|(attribute, _)| attribute.id).ne([
Mesh::ATTRIBUTE_POSITION.id,
Mesh::ATTRIBUTE_NORMAL.id,
Mesh::ATTRIBUTE_UV_0.id,
]) {
return Err(MeshToMeshletMeshConversionError::WrongMeshVertexAttributes);
}
match mesh.indices() {
Some(Indices::U32(indices)) => Ok(Cow::Borrowed(indices.as_slice())),
Some(Indices::U16(indices)) => Ok(indices.iter().map(|i| *i as u32).collect()),
_ => Err(MeshToMeshletMeshConversionError::MeshMissingIndices),
}
}
fn compute_meshlets(
indices: &[u32],
vertices: &VertexDataAdapter,
position_only_vertex_remap: &[u32],
position_only_vertex_count: usize,
) -> Meshlets {
// For each vertex, build a list of all triangles that use it
let mut vertices_to_triangles = vec![Vec::new(); position_only_vertex_count];
for (i, index) in indices.iter().enumerate() {
let vertex_id = position_only_vertex_remap[*index as usize];
let vertex_to_triangles = &mut vertices_to_triangles[vertex_id as usize];
vertex_to_triangles.push(i / 3);
}
// For each triangle pair, count how many vertices they share
let mut triangle_pair_to_shared_vertex_count = <HashMap<_, _>>::default();
for vertex_triangle_ids in vertices_to_triangles {
for (triangle_id1, triangle_id2) in vertex_triangle_ids.into_iter().tuple_combinations() {
let count = triangle_pair_to_shared_vertex_count
.entry((
triangle_id1.min(triangle_id2),
triangle_id1.max(triangle_id2),
))
.or_insert(0);
*count += 1;
}
}
// For each triangle, gather all other triangles that share at least one vertex along with their shared vertex count
let triangle_count = indices.len() / 3;
let mut connected_triangles_per_triangle = vec![Vec::new(); triangle_count];
for ((triangle_id1, triangle_id2), shared_vertex_count) in triangle_pair_to_shared_vertex_count
{
// We record both id1->id2 and id2->id1 as adjacency is symmetrical
connected_triangles_per_triangle[triangle_id1].push((triangle_id2, shared_vertex_count));
connected_triangles_per_triangle[triangle_id2].push((triangle_id1, shared_vertex_count));
}
// The order of triangles depends on hash traversal order; to produce deterministic results, sort them
for list in connected_triangles_per_triangle.iter_mut() {
list.sort_unstable();
}
let mut xadj = Vec::with_capacity(triangle_count + 1);
let mut adjncy = Vec::new();
let mut adjwgt = Vec::new();
for connected_triangles in connected_triangles_per_triangle {
xadj.push(adjncy.len() as i32);
for (connected_triangle_id, shared_vertex_count) in connected_triangles {
adjncy.push(connected_triangle_id as i32);
adjwgt.push(shared_vertex_count);
// TODO: Additional weight based on triangle center spatial proximity?
}
}
xadj.push(adjncy.len() as i32);
let mut options = [-1; metis::NOPTIONS];
options[metis::option::Seed::INDEX] = 17;
options[metis::option::UFactor::INDEX] = 1; // Important that there's very little imbalance between partitions
let mut meshlet_per_triangle = vec![0; triangle_count];
let partition_count = triangle_count.div_ceil(126); // Need to undershoot to prevent METIS from going over 128 triangles per meshlet
Graph::new(1, partition_count as i32, &xadj, &adjncy)
.unwrap()
.set_options(&options)
.set_adjwgt(&adjwgt)
.part_recursive(&mut meshlet_per_triangle)
.unwrap();
let mut indices_per_meshlet = vec![Vec::new(); partition_count];
for (triangle_id, meshlet) in meshlet_per_triangle.into_iter().enumerate() {
let meshlet_indices = &mut indices_per_meshlet[meshlet as usize];
let base_index = triangle_id * 3;
meshlet_indices.extend_from_slice(&indices[base_index..(base_index + 3)]);
}
// Use meshopt to build meshlets from the sets of triangles
let mut meshlets = Meshlets {
meshlets: Vec::new(),
vertices: Vec::new(),
triangles: Vec::new(),
};
for meshlet_indices in &indices_per_meshlet {
let meshlet = build_meshlets(meshlet_indices, vertices, 255, 128, 0.0);
let vertex_offset = meshlets.vertices.len() as u32;
let triangle_offset = meshlets.triangles.len() as u32;
meshlets.vertices.extend_from_slice(&meshlet.vertices);
meshlets.triangles.extend_from_slice(&meshlet.triangles);
meshlets
.meshlets
.extend(meshlet.meshlets.into_iter().map(|mut meshlet| {
meshlet.vertex_offset += vertex_offset;
meshlet.triangle_offset += triangle_offset;
meshlet
}));
}
meshlets
}
fn find_connected_meshlets(
simplification_queue: Range<usize>,
meshlets: &Meshlets,
position_only_vertex_remap: &[u32],
position_only_vertex_count: usize,
) -> Vec<Vec<(usize, usize)>> {
// For each vertex, build a list of all meshlets that use it
let mut vertices_to_meshlets = vec![Vec::new(); position_only_vertex_count];
for meshlet_id in simplification_queue.clone() {
let meshlet = meshlets.get(meshlet_id);
for index in meshlet.triangles {
let vertex_id = position_only_vertex_remap[meshlet.vertices[*index as usize] as usize];
let vertex_to_meshlets = &mut vertices_to_meshlets[vertex_id as usize];
// Meshlets are added in order, so we can just check the last element to deduplicate,
// in the case of two triangles sharing the same vertex within a single meshlet
if vertex_to_meshlets.last() != Some(&meshlet_id) {
vertex_to_meshlets.push(meshlet_id);
}
}
}
// For each meshlet pair, count how many vertices they share
let mut meshlet_pair_to_shared_vertex_count = <HashMap<_, _>>::default();
for vertex_meshlet_ids in vertices_to_meshlets {
for (meshlet_id1, meshlet_id2) in vertex_meshlet_ids.into_iter().tuple_combinations() {
let count = meshlet_pair_to_shared_vertex_count
.entry((meshlet_id1.min(meshlet_id2), meshlet_id1.max(meshlet_id2)))
.or_insert(0);
*count += 1;
}
}
// For each meshlet, gather all other meshlets that share at least one vertex along with their shared vertex count
let mut connected_meshlets_per_meshlet = vec![Vec::new(); simplification_queue.len()];
for ((meshlet_id1, meshlet_id2), shared_vertex_count) in meshlet_pair_to_shared_vertex_count {
// We record both id1->id2 and id2->id1 as adjacency is symmetrical
connected_meshlets_per_meshlet[meshlet_id1 - simplification_queue.start]
.push((meshlet_id2, shared_vertex_count));
connected_meshlets_per_meshlet[meshlet_id2 - simplification_queue.start]
.push((meshlet_id1, shared_vertex_count));
}
// The order of meshlets depends on hash traversal order; to produce deterministic results, sort them
for list in connected_meshlets_per_meshlet.iter_mut() {
list.sort_unstable();
}
connected_meshlets_per_meshlet
}
// METIS manual: https://github.com/KarypisLab/METIS/blob/e0f1b88b8efcb24ffa0ec55eabb78fbe61e58ae7/manual/manual.pdf
fn group_meshlets(
connected_meshlets_per_meshlet: &[Vec<(usize, usize)>],
simplification_queue: Range<usize>,
) -> Vec<SmallVec<[usize; TARGET_MESHLETS_PER_GROUP]>> {
let mut xadj = Vec::with_capacity(simplification_queue.len() + 1);
let mut adjncy = Vec::new();
let mut adjwgt = Vec::new();
for connected_meshlets in connected_meshlets_per_meshlet {
xadj.push(adjncy.len() as i32);
for (connected_meshlet_id, shared_vertex_count) in connected_meshlets {
adjncy.push((connected_meshlet_id - simplification_queue.start) as i32);
adjwgt.push(*shared_vertex_count as i32);
// TODO: Additional weight based on meshlet spatial proximity
}
}
xadj.push(adjncy.len() as i32);
let mut options = [-1; metis::NOPTIONS];
options[metis::option::Seed::INDEX] = 17;
options[metis::option::UFactor::INDEX] = 200;
let mut group_per_meshlet = vec![0; simplification_queue.len()];
let partition_count = simplification_queue
.len()
.div_ceil(TARGET_MESHLETS_PER_GROUP); // TODO: Nanite uses groups of 8-32, probably based on some kind of heuristic
Graph::new(1, partition_count as i32, &xadj, &adjncy)
.unwrap()
.set_options(&options)
.set_adjwgt(&adjwgt)
.part_recursive(&mut group_per_meshlet)
.unwrap();
let mut groups = vec![SmallVec::new(); partition_count];
for (i, meshlet_group) in group_per_meshlet.into_iter().enumerate() {
groups[meshlet_group as usize].push(i + simplification_queue.start);
}
groups
}
fn lock_group_borders(
vertex_locks: &mut [bool],
groups: &[SmallVec<[usize; TARGET_MESHLETS_PER_GROUP]>],
meshlets: &Meshlets,
position_only_vertex_remap: &[u32],
position_only_vertex_count: usize,
) {
let mut position_only_locks = vec![-1; position_only_vertex_count];
// Iterate over position-only based vertices of all meshlets in all groups
for (group_id, group_meshlets) in groups.iter().enumerate() {
for meshlet_id in group_meshlets {
let meshlet = meshlets.get(*meshlet_id);
for index in meshlet.triangles {
let vertex_id =
position_only_vertex_remap[meshlet.vertices[*index as usize] as usize] as usize;
// If the vertex is not yet claimed by any group, or was already claimed by this group
if position_only_locks[vertex_id] == -1
|| position_only_locks[vertex_id] == group_id as i32
{
position_only_locks[vertex_id] = group_id as i32; // Then claim the vertex for this group
} else {
position_only_locks[vertex_id] = -2; // Else vertex was already claimed by another group or was already locked, lock it
}
}
}
}
// Lock vertices used by more than 1 group
for i in 0..vertex_locks.len() {
let vertex_id = position_only_vertex_remap[i] as usize;
vertex_locks[i] = position_only_locks[vertex_id] == -2;
}
}
fn simplify_meshlet_group(
group_meshlets: &[usize],
meshlets: &Meshlets,
vertices: &VertexDataAdapter<'_>,
vertex_normals: &[f32],
vertex_stride: usize,
vertex_locks: &[bool],
) -> Option<(Vec<u32>, f16)> {
// Build a new index buffer into the mesh vertex data by combining all meshlet data in the group
let mut group_indices = Vec::new();
for meshlet_id in group_meshlets {
let meshlet = meshlets.get(*meshlet_id);
for meshlet_index in meshlet.triangles {
group_indices.push(meshlet.vertices[*meshlet_index as usize]);
}
}
// Simplify the group to ~50% triangle count
let mut error = 0.0;
let simplified_group_indices = simplify_with_attributes_and_locks(
&group_indices,
vertices,
vertex_normals,
&[0.5; 3],
vertex_stride,
vertex_locks,
group_indices.len() / 2,
f32::MAX,
SimplifyOptions::Sparse | SimplifyOptions::ErrorAbsolute,
Some(&mut error),
);
// Check if we were able to simplify at least a little
if simplified_group_indices.len() as f32 / group_indices.len() as f32
> SIMPLIFICATION_FAILURE_PERCENTAGE
{
return None;
}
Some((simplified_group_indices, f16::from_f32(error)))
}
fn compute_lod_group_data(
group_meshlets: &[usize],
group_error: &mut f16,
bounding_spheres: &mut [MeshletBoundingSpheres],
simplification_errors: &mut [MeshletSimplificationError],
) -> MeshletBoundingSphere {
let mut group_bounding_sphere = MeshletBoundingSphere {
center: Vec3::ZERO,
radius: 0.0,
};
// Compute the lod group sphere center as a weighted average of the children spheres
let mut weight = 0.0;
for meshlet_id in group_meshlets {
let meshlet_lod_bounding_sphere = bounding_spheres[*meshlet_id].lod_group_sphere;
group_bounding_sphere.center +=
meshlet_lod_bounding_sphere.center * meshlet_lod_bounding_sphere.radius;
weight += meshlet_lod_bounding_sphere.radius;
}
group_bounding_sphere.center /= weight;
// Force parent group sphere to contain all child group spheres (we're currently building the parent from its children)
// TODO: This does not produce the absolute minimal bounding sphere. Doing so is non-trivial.
// "Smallest enclosing balls of balls" http://www.inf.ethz.ch/personal/emo/DoctThesisFiles/fischer05.pdf
for meshlet_id in group_meshlets {
let meshlet_lod_bounding_sphere = bounding_spheres[*meshlet_id].lod_group_sphere;
let d = meshlet_lod_bounding_sphere
.center
.distance(group_bounding_sphere.center);
group_bounding_sphere.radius = group_bounding_sphere
.radius
.max(meshlet_lod_bounding_sphere.radius + d);
}
// Force parent error to be >= child error (we're currently building the parent from its children)
for meshlet_id in group_meshlets {
*group_error = group_error.max(simplification_errors[*meshlet_id].group_error);
}
// Set the children's lod parent group data to the new lod group we just made
for meshlet_id in group_meshlets {
bounding_spheres[*meshlet_id].lod_parent_group_sphere = group_bounding_sphere;
simplification_errors[*meshlet_id].parent_group_error = *group_error;
}
group_bounding_sphere
}
fn split_simplified_group_into_new_meshlets(
simplified_group_indices: &[u32],
vertices: &VertexDataAdapter<'_>,
position_only_vertex_remap: &[u32],
position_only_vertex_count: usize,
meshlets: &mut Meshlets,
) -> usize {
let simplified_meshlets = compute_meshlets(
simplified_group_indices,
vertices,
position_only_vertex_remap,
position_only_vertex_count,
);
let new_meshlets_count = simplified_meshlets.len();
let vertex_offset = meshlets.vertices.len() as u32;
let triangle_offset = meshlets.triangles.len() as u32;
meshlets
.vertices
.extend_from_slice(&simplified_meshlets.vertices);
meshlets
.triangles
.extend_from_slice(&simplified_meshlets.triangles);
meshlets
.meshlets
.extend(simplified_meshlets.meshlets.into_iter().map(|mut meshlet| {
meshlet.vertex_offset += vertex_offset;
meshlet.triangle_offset += triangle_offset;
meshlet
}));
new_meshlets_count
}
fn build_and_compress_per_meshlet_vertex_data(
meshlet: &meshopt_Meshlet,
meshlet_vertex_ids: &[u32],
vertex_buffer: &[u8],
vertex_stride: usize,
vertex_positions: &mut BitVec<u32, Lsb0>,
vertex_normals: &mut Vec<u32>,
vertex_uvs: &mut Vec<Vec2>,
meshlets: &mut Vec<Meshlet>,
vertex_position_quantization_factor: u8,
) {
let start_vertex_position_bit = vertex_positions.len() as u32;
let start_vertex_attribute_id = vertex_normals.len() as u32;
let quantization_factor =
(1 << vertex_position_quantization_factor) as f32 * CENTIMETERS_PER_METER;
let mut min_quantized_position_channels = IVec3::MAX;
let mut max_quantized_position_channels = IVec3::MIN;
// Lossy vertex compression
let mut quantized_positions = [IVec3::ZERO; 255];
for (i, vertex_id) in meshlet_vertex_ids.iter().enumerate() {
// Load source vertex attributes
let vertex_id_byte = *vertex_id as usize * vertex_stride;
let vertex_data = &vertex_buffer[vertex_id_byte..(vertex_id_byte + vertex_stride)];
let position = Vec3::from_slice(bytemuck::cast_slice(&vertex_data[0..12]));
let normal = Vec3::from_slice(bytemuck::cast_slice(&vertex_data[12..24]));
let uv = Vec2::from_slice(bytemuck::cast_slice(&vertex_data[24..32]));
// Copy uncompressed UV
vertex_uvs.push(uv);
// Compress normal
vertex_normals.push(pack2x16snorm(octahedral_encode(normal)));
// Quantize position to a fixed-point IVec3
let quantized_position = (position * quantization_factor + 0.5).as_ivec3();
quantized_positions[i] = quantized_position;
// Compute per X/Y/Z-channel quantized position min/max for this meshlet
min_quantized_position_channels = min_quantized_position_channels.min(quantized_position);
max_quantized_position_channels = max_quantized_position_channels.max(quantized_position);
}
// Calculate bits needed to encode each quantized vertex position channel based on the range of each channel
let range = max_quantized_position_channels - min_quantized_position_channels + 1;
let bits_per_vertex_position_channel_x = log2(range.x as f32).ceil() as u8;
let bits_per_vertex_position_channel_y = log2(range.y as f32).ceil() as u8;
let bits_per_vertex_position_channel_z = log2(range.z as f32).ceil() as u8;
// Lossless encoding of vertex positions in the minimum number of bits per channel
for quantized_position in quantized_positions.iter().take(meshlet_vertex_ids.len()) {
// Remap [range_min, range_max] IVec3 to [0, range_max - range_min] UVec3
let position = (quantized_position - min_quantized_position_channels).as_uvec3();
// Store as a packed bitstream
vertex_positions.extend_from_bitslice(
&position.x.view_bits::<Lsb0>()[..bits_per_vertex_position_channel_x as usize],
);
vertex_positions.extend_from_bitslice(
&position.y.view_bits::<Lsb0>()[..bits_per_vertex_position_channel_y as usize],
);
vertex_positions.extend_from_bitslice(
&position.z.view_bits::<Lsb0>()[..bits_per_vertex_position_channel_z as usize],
);
}
meshlets.push(Meshlet {
start_vertex_position_bit,
start_vertex_attribute_id,
start_index_id: meshlet.triangle_offset,
vertex_count: meshlet.vertex_count as u8,
triangle_count: meshlet.triangle_count as u8,
padding: 0,
bits_per_vertex_position_channel_x,
bits_per_vertex_position_channel_y,
bits_per_vertex_position_channel_z,
vertex_position_quantization_factor,
min_vertex_position_channel_x: min_quantized_position_channels.x as f32,
min_vertex_position_channel_y: min_quantized_position_channels.y as f32,
min_vertex_position_channel_z: min_quantized_position_channels.z as f32,
});
}
fn compute_meshlet_bounds(
meshlet: meshopt::Meshlet<'_>,
vertices: &VertexDataAdapter<'_>,
) -> MeshletBoundingSphere {
let bounds = meshopt::compute_meshlet_bounds(meshlet, vertices);
MeshletBoundingSphere {
center: bounds.center.into(),
radius: bounds.radius,
}
}
// TODO: Precise encode variant
fn octahedral_encode(v: Vec3) -> Vec2 {
let n = v / (v.x.abs() + v.y.abs() + v.z.abs());
let octahedral_wrap = (1.0 - n.yx().abs())
* Vec2::new(
if n.x >= 0.0 { 1.0 } else { -1.0 },
if n.y >= 0.0 { 1.0 } else { -1.0 },
);
if n.z >= 0.0 {
n.xy()
} else {
octahedral_wrap
}
}
// https://www.w3.org/TR/WGSL/#pack2x16snorm-builtin
fn pack2x16snorm(v: Vec2) -> u32 {
let v = v.clamp(Vec2::NEG_ONE, Vec2::ONE);
let v = (v * 32767.0 + 0.5).floor().as_i16vec2();
bytemuck::cast(v)
}
/// An error produced by [`MeshletMesh::from_mesh`].
#[derive(Error, Debug)]
pub enum MeshToMeshletMeshConversionError {
#[error("Mesh primitive topology is not TriangleList")]
WrongMeshPrimitiveTopology,
#[error("Mesh vertex attributes are not {{POSITION, NORMAL, UV_0}}")]
WrongMeshVertexAttributes,
#[error("Mesh has no indices")]
MeshMissingIndices,
}

View File

@@ -0,0 +1,296 @@
use super::{meshlet_mesh_manager::MeshletMeshManager, MeshletMesh, MeshletMesh3d};
use crate::{
material::DUMMY_MESH_MATERIAL, Material, MaterialBindingId, MeshFlags, MeshTransforms,
MeshUniform, NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform,
RenderMaterialBindings, RenderMaterialInstances,
};
use bevy_asset::{AssetEvent, AssetServer, Assets, UntypedAssetId};
use bevy_ecs::{
entity::{Entities, Entity, EntityHashMap},
event::EventReader,
query::Has,
resource::Resource,
system::{Local, Query, Res, ResMut, SystemState},
};
use bevy_platform::collections::{HashMap, HashSet};
use bevy_render::{
render_resource::StorageBuffer, sync_world::MainEntity, view::RenderLayers, MainWorld,
};
use bevy_transform::components::GlobalTransform;
use core::ops::{DerefMut, Range};
/// Manages data for each entity with a [`MeshletMesh`].
#[derive(Resource)]
pub struct InstanceManager {
/// Amount of instances in the scene.
pub scene_instance_count: u32,
/// Amount of clusters in the scene.
pub scene_cluster_count: u32,
/// Per-instance [`MainEntity`], [`RenderLayers`], and [`NotShadowCaster`].
pub instances: Vec<(MainEntity, RenderLayers, bool)>,
/// Per-instance [`MeshUniform`].
pub instance_uniforms: StorageBuffer<Vec<MeshUniform>>,
/// Per-instance material ID.
pub instance_material_ids: StorageBuffer<Vec<u32>>,
/// Per-instance count of meshlets in the instance's [`MeshletMesh`].
pub instance_meshlet_counts: StorageBuffer<Vec<u32>>,
/// Per-instance index to the start of the instance's slice of the meshlets buffer.
pub instance_meshlet_slice_starts: StorageBuffer<Vec<u32>>,
/// Per-view per-instance visibility bit. Used for [`RenderLayers`] and [`NotShadowCaster`] support.
pub view_instance_visibility: EntityHashMap<StorageBuffer<Vec<u32>>>,
/// Next material ID available for a [`Material`].
next_material_id: u32,
/// Map of [`Material`] to material ID.
material_id_lookup: HashMap<UntypedAssetId, u32>,
/// Set of material IDs used in the scene.
material_ids_present_in_scene: HashSet<u32>,
}
impl InstanceManager {
pub fn new() -> Self {
Self {
scene_instance_count: 0,
scene_cluster_count: 0,
instances: Vec::new(),
instance_uniforms: {
let mut buffer = StorageBuffer::default();
buffer.set_label(Some("meshlet_instance_uniforms"));
buffer
},
instance_material_ids: {
let mut buffer = StorageBuffer::default();
buffer.set_label(Some("meshlet_instance_material_ids"));
buffer
},
instance_meshlet_counts: {
let mut buffer = StorageBuffer::default();
buffer.set_label(Some("meshlet_instance_meshlet_counts"));
buffer
},
instance_meshlet_slice_starts: {
let mut buffer = StorageBuffer::default();
buffer.set_label(Some("meshlet_instance_meshlet_slice_starts"));
buffer
},
view_instance_visibility: EntityHashMap::default(),
next_material_id: 0,
material_id_lookup: HashMap::default(),
material_ids_present_in_scene: HashSet::default(),
}
}
pub fn add_instance(
&mut self,
instance: MainEntity,
meshlets_slice: Range<u32>,
transform: &GlobalTransform,
previous_transform: Option<&PreviousGlobalTransform>,
render_layers: Option<&RenderLayers>,
mesh_material_ids: &RenderMaterialInstances,
render_material_bindings: &RenderMaterialBindings,
not_shadow_receiver: bool,
not_shadow_caster: bool,
) {
// Build a MeshUniform for the instance
let transform = transform.affine();
let previous_transform = previous_transform.map(|t| t.0).unwrap_or(transform);
let mut flags = if not_shadow_receiver {
MeshFlags::empty()
} else {
MeshFlags::SHADOW_RECEIVER
};
if transform.matrix3.determinant().is_sign_positive() {
flags |= MeshFlags::SIGN_DETERMINANT_MODEL_3X3;
}
let transforms = MeshTransforms {
world_from_local: (&transform).into(),
previous_world_from_local: (&previous_transform).into(),
flags: flags.bits(),
};
let mesh_material = mesh_material_ids.mesh_material(instance);
let mesh_material_binding_id = if mesh_material != DUMMY_MESH_MATERIAL.untyped() {
render_material_bindings
.get(&mesh_material)
.cloned()
.unwrap_or_default()
} else {
// Use a dummy binding ID if the mesh has no material
MaterialBindingId::default()
};
let mesh_uniform = MeshUniform::new(
&transforms,
0,
mesh_material_binding_id.slot,
None,
None,
None,
);
// Append instance data
self.instances.push((
instance,
render_layers.cloned().unwrap_or(RenderLayers::default()),
not_shadow_caster,
));
self.instance_uniforms.get_mut().push(mesh_uniform);
self.instance_material_ids.get_mut().push(0);
self.instance_meshlet_counts
.get_mut()
.push(meshlets_slice.len() as u32);
self.instance_meshlet_slice_starts
.get_mut()
.push(meshlets_slice.start);
self.scene_instance_count += 1;
self.scene_cluster_count += meshlets_slice.len() as u32;
}
/// Get the material ID for a [`crate::Material`].
pub fn get_material_id(&mut self, material_asset_id: UntypedAssetId) -> u32 {
*self
.material_id_lookup
.entry(material_asset_id)
.or_insert_with(|| {
self.next_material_id += 1;
self.next_material_id
})
}
pub fn material_present_in_scene(&self, material_id: &u32) -> bool {
self.material_ids_present_in_scene.contains(material_id)
}
pub fn reset(&mut self, entities: &Entities) {
self.scene_instance_count = 0;
self.scene_cluster_count = 0;
self.instances.clear();
self.instance_uniforms.get_mut().clear();
self.instance_material_ids.get_mut().clear();
self.instance_meshlet_counts.get_mut().clear();
self.instance_meshlet_slice_starts.get_mut().clear();
self.view_instance_visibility
.retain(|view_entity, _| entities.contains(*view_entity));
self.view_instance_visibility
.values_mut()
.for_each(|b| b.get_mut().clear());
self.next_material_id = 0;
self.material_id_lookup.clear();
self.material_ids_present_in_scene.clear();
}
}
pub fn extract_meshlet_mesh_entities(
mut meshlet_mesh_manager: ResMut<MeshletMeshManager>,
mut instance_manager: ResMut<InstanceManager>,
// TODO: Replace main_world and system_state when Extract<ResMut<Assets<MeshletMesh>>> is possible
mut main_world: ResMut<MainWorld>,
mesh_material_ids: Res<RenderMaterialInstances>,
render_material_bindings: Res<RenderMaterialBindings>,
mut system_state: Local<
Option<
SystemState<(
Query<(
Entity,
&MeshletMesh3d,
&GlobalTransform,
Option<&PreviousGlobalTransform>,
Option<&RenderLayers>,
Has<NotShadowReceiver>,
Has<NotShadowCaster>,
)>,
Res<AssetServer>,
ResMut<Assets<MeshletMesh>>,
EventReader<AssetEvent<MeshletMesh>>,
)>,
>,
>,
render_entities: &Entities,
) {
// Get instances query
if system_state.is_none() {
*system_state = Some(SystemState::new(&mut main_world));
}
let system_state = system_state.as_mut().unwrap();
let (instances_query, asset_server, mut assets, mut asset_events) =
system_state.get_mut(&mut main_world);
// Reset per-frame data
instance_manager.reset(render_entities);
// Free GPU buffer space for any modified or dropped MeshletMesh assets
for asset_event in asset_events.read() {
if let AssetEvent::Unused { id } | AssetEvent::Modified { id } = asset_event {
meshlet_mesh_manager.remove(id);
}
}
// Iterate over every instance
for (
instance,
meshlet_mesh,
transform,
previous_transform,
render_layers,
not_shadow_receiver,
not_shadow_caster,
) in &instances_query
{
// Skip instances with an unloaded MeshletMesh asset
// TODO: This is a semi-expensive check
if asset_server.is_managed(meshlet_mesh.id())
&& !asset_server.is_loaded_with_dependencies(meshlet_mesh.id())
{
continue;
}
// Upload the instance's MeshletMesh asset data if not done already done
let meshlets_slice =
meshlet_mesh_manager.queue_upload_if_needed(meshlet_mesh.id(), &mut assets);
// Add the instance's data to the instance manager
instance_manager.add_instance(
instance.into(),
meshlets_slice,
transform,
previous_transform,
render_layers,
&mesh_material_ids,
&render_material_bindings,
not_shadow_receiver,
not_shadow_caster,
);
}
}
/// For each entity in the scene, record what material ID its material was assigned in the `prepare_material_meshlet_meshes` systems,
/// and note that the material is used by at least one entity in the scene.
pub fn queue_material_meshlet_meshes<M: Material>(
mut instance_manager: ResMut<InstanceManager>,
render_material_instances: Res<RenderMaterialInstances>,
) {
let instance_manager = instance_manager.deref_mut();
for (i, (instance, _, _)) in instance_manager.instances.iter().enumerate() {
if let Some(material_instance) = render_material_instances.instances.get(instance) {
if let Ok(material_asset_id) = material_instance.asset_id.try_typed::<M>() {
if let Some(material_id) = instance_manager
.material_id_lookup
.get(&material_asset_id.untyped())
{
instance_manager
.material_ids_present_in_scene
.insert(*material_id);
instance_manager.instance_material_ids.get_mut()[i] = *material_id;
}
}
}
}
}

View File

@@ -0,0 +1,465 @@
use super::{
instance_manager::InstanceManager, resource_manager::ResourceManager,
MESHLET_MESH_MATERIAL_SHADER_HANDLE,
};
use crate::{
environment_map::EnvironmentMapLight, irradiance_volume::IrradianceVolume,
material_bind_groups::MaterialBindGroupAllocator, *,
};
use bevy_asset::AssetServer;
use bevy_core_pipeline::{
core_3d::Camera3d,
prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass},
tonemapping::{DebandDither, Tonemapping},
};
use bevy_derive::{Deref, DerefMut};
use bevy_platform::collections::{HashMap, HashSet};
use bevy_render::{
camera::TemporalJitter,
mesh::{Mesh, MeshVertexBufferLayout, MeshVertexBufferLayoutRef, MeshVertexBufferLayouts},
render_asset::RenderAssets,
render_resource::*,
view::ExtractedView,
};
use core::hash::Hash;
/// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`super::MeshletMainOpaquePass3dNode`].
#[derive(Component, Deref, DerefMut, Default)]
pub struct MeshletViewMaterialsMainOpaquePass(pub Vec<(u32, CachedRenderPipelineId, BindGroup)>);
/// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletMainOpaquePass3dNode`],
/// and register the material with [`InstanceManager`].
pub fn prepare_material_meshlet_meshes_main_opaque_pass<M: Material>(
resource_manager: ResMut<ResourceManager>,
mut instance_manager: ResMut<InstanceManager>,
mut cache: Local<HashMap<MeshPipelineKey, CachedRenderPipelineId>>,
pipeline_cache: Res<PipelineCache>,
material_pipeline: Res<MaterialPipeline<M>>,
mesh_pipeline: Res<MeshPipeline>,
render_materials: Res<RenderAssets<PreparedMaterial<M>>>,
render_material_instances: Res<RenderMaterialInstances>,
material_bind_group_allocator: Res<MaterialBindGroupAllocator<M>>,
asset_server: Res<AssetServer>,
mut mesh_vertex_buffer_layouts: ResMut<MeshVertexBufferLayouts>,
mut views: Query<
(
&mut MeshletViewMaterialsMainOpaquePass,
&ExtractedView,
Option<&Tonemapping>,
Option<&DebandDither>,
Option<&ShadowFilteringMethod>,
(Has<ScreenSpaceAmbientOcclusion>, Has<DistanceFog>),
(
Has<NormalPrepass>,
Has<DepthPrepass>,
Has<MotionVectorPrepass>,
Has<DeferredPrepass>,
),
Has<TemporalJitter>,
Option<&Projection>,
Has<RenderViewLightProbes<EnvironmentMapLight>>,
Has<RenderViewLightProbes<IrradianceVolume>>,
),
With<Camera3d>,
>,
) where
M::Data: PartialEq + Eq + Hash + Clone,
{
let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts);
for (
mut materials,
view,
tonemapping,
dither,
shadow_filter_method,
(ssao, distance_fog),
(normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass),
temporal_jitter,
projection,
has_environment_maps,
has_irradiance_volumes,
) in &mut views
{
let mut view_key =
MeshPipelineKey::from_msaa_samples(1) | MeshPipelineKey::from_hdr(view.hdr);
if normal_prepass {
view_key |= MeshPipelineKey::NORMAL_PREPASS;
}
if depth_prepass {
view_key |= MeshPipelineKey::DEPTH_PREPASS;
}
if motion_vector_prepass {
view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS;
}
if deferred_prepass {
view_key |= MeshPipelineKey::DEFERRED_PREPASS;
}
if temporal_jitter {
view_key |= MeshPipelineKey::TEMPORAL_JITTER;
}
if has_environment_maps {
view_key |= MeshPipelineKey::ENVIRONMENT_MAP;
}
if has_irradiance_volumes {
view_key |= MeshPipelineKey::IRRADIANCE_VOLUME;
}
if let Some(projection) = projection {
view_key |= match projection {
Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE,
Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC,
Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD,
};
}
match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) {
ShadowFilteringMethod::Hardware2x2 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2;
}
ShadowFilteringMethod::Gaussian => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN;
}
ShadowFilteringMethod::Temporal => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL;
}
}
if !view.hdr {
if let Some(tonemapping) = tonemapping {
view_key |= MeshPipelineKey::TONEMAP_IN_SHADER;
view_key |= tonemapping_pipeline_key(*tonemapping);
}
if let Some(DebandDither::Enabled) = dither {
view_key |= MeshPipelineKey::DEBAND_DITHER;
}
}
if ssao {
view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION;
}
if distance_fog {
view_key |= MeshPipelineKey::DISTANCE_FOG;
}
view_key |= MeshPipelineKey::from_primitive_topology(PrimitiveTopology::TriangleList);
for material_id in render_material_instances
.instances
.values()
.flat_map(|instance| instance.asset_id.try_typed::<M>().ok())
.collect::<HashSet<_>>()
{
let Some(material) = render_materials.get(material_id) else {
continue;
};
let Some(material_bind_group) =
material_bind_group_allocator.get(material.binding.group)
else {
continue;
};
if material.properties.render_method != OpaqueRendererMethod::Forward
|| material.properties.alpha_mode != AlphaMode::Opaque
|| material.properties.reads_view_transmission_texture
{
continue;
}
let Ok(material_pipeline_descriptor) = material_pipeline.specialize(
MaterialPipelineKey {
mesh_key: view_key,
bind_group_data: material_bind_group
.get_extra_data(material.binding.slot)
.clone(),
},
fake_vertex_buffer_layout,
) else {
continue;
};
let material_fragment = material_pipeline_descriptor.fragment.unwrap();
let mut shader_defs = material_fragment.shader_defs;
shader_defs.push("MESHLET_MESH_MATERIAL_PASS".into());
let pipeline_descriptor = RenderPipelineDescriptor {
label: material_pipeline_descriptor.label,
layout: vec![
mesh_pipeline.get_view_layout(view_key.into()).clone(),
resource_manager.material_shade_bind_group_layout.clone(),
material_pipeline.material_layout.clone(),
],
push_constant_ranges: vec![],
vertex: VertexState {
shader: MESHLET_MESH_MATERIAL_SHADER_HANDLE,
shader_defs: shader_defs.clone(),
entry_point: material_pipeline_descriptor.vertex.entry_point,
buffers: Vec::new(),
},
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Depth16Unorm,
depth_write_enabled: false,
depth_compare: CompareFunction::Equal,
stencil: StencilState::default(),
bias: DepthBiasState::default(),
}),
multisample: MultisampleState::default(),
fragment: Some(FragmentState {
shader: match M::meshlet_mesh_fragment_shader() {
ShaderRef::Default => MESHLET_MESH_MATERIAL_SHADER_HANDLE,
ShaderRef::Handle(handle) => handle,
ShaderRef::Path(path) => asset_server.load(path),
},
shader_defs,
entry_point: material_fragment.entry_point,
targets: material_fragment.targets,
}),
zero_initialize_workgroup_memory: false,
};
let material_id = instance_manager.get_material_id(material_id.untyped());
let pipeline_id = *cache.entry(view_key).or_insert_with(|| {
pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone())
});
let Some(material_bind_group) =
material_bind_group_allocator.get(material.binding.group)
else {
continue;
};
let Some(bind_group) = material_bind_group.bind_group() else {
continue;
};
materials.push((material_id, pipeline_id, (*bind_group).clone()));
}
}
}
/// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`super::MeshletPrepassNode`].
#[derive(Component, Deref, DerefMut, Default)]
pub struct MeshletViewMaterialsPrepass(pub Vec<(u32, CachedRenderPipelineId, BindGroup)>);
/// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`super::MeshletDeferredGBufferPrepassNode`].
#[derive(Component, Deref, DerefMut, Default)]
pub struct MeshletViewMaterialsDeferredGBufferPrepass(
pub Vec<(u32, CachedRenderPipelineId, BindGroup)>,
);
/// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletPrepassNode`],
/// and [`super::MeshletDeferredGBufferPrepassNode`] and register the material with [`InstanceManager`].
pub fn prepare_material_meshlet_meshes_prepass<M: Material>(
resource_manager: ResMut<ResourceManager>,
mut instance_manager: ResMut<InstanceManager>,
mut cache: Local<HashMap<MeshPipelineKey, CachedRenderPipelineId>>,
pipeline_cache: Res<PipelineCache>,
prepass_pipeline: Res<PrepassPipeline<M>>,
render_materials: Res<RenderAssets<PreparedMaterial<M>>>,
render_material_instances: Res<RenderMaterialInstances>,
mut mesh_vertex_buffer_layouts: ResMut<MeshVertexBufferLayouts>,
material_bind_group_allocator: Res<MaterialBindGroupAllocator<M>>,
asset_server: Res<AssetServer>,
mut views: Query<
(
&mut MeshletViewMaterialsPrepass,
&mut MeshletViewMaterialsDeferredGBufferPrepass,
&ExtractedView,
AnyOf<(&NormalPrepass, &MotionVectorPrepass, &DeferredPrepass)>,
),
With<Camera3d>,
>,
) where
M::Data: PartialEq + Eq + Hash + Clone,
{
let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts);
for (
mut materials,
mut deferred_materials,
view,
(normal_prepass, motion_vector_prepass, deferred_prepass),
) in &mut views
{
let mut view_key =
MeshPipelineKey::from_msaa_samples(1) | MeshPipelineKey::from_hdr(view.hdr);
if normal_prepass.is_some() {
view_key |= MeshPipelineKey::NORMAL_PREPASS;
}
if motion_vector_prepass.is_some() {
view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS;
}
view_key |= MeshPipelineKey::from_primitive_topology(PrimitiveTopology::TriangleList);
for material_id in render_material_instances
.instances
.values()
.flat_map(|instance| instance.asset_id.try_typed::<M>().ok())
.collect::<HashSet<_>>()
{
let Some(material) = render_materials.get(material_id) else {
continue;
};
let Some(material_bind_group) =
material_bind_group_allocator.get(material.binding.group)
else {
continue;
};
if material.properties.alpha_mode != AlphaMode::Opaque
|| material.properties.reads_view_transmission_texture
{
continue;
}
let material_wants_deferred = matches!(
material.properties.render_method,
OpaqueRendererMethod::Deferred
);
if deferred_prepass.is_some() && material_wants_deferred {
view_key |= MeshPipelineKey::DEFERRED_PREPASS;
} else if normal_prepass.is_none() && motion_vector_prepass.is_none() {
continue;
}
let Ok(material_pipeline_descriptor) = prepass_pipeline.specialize(
MaterialPipelineKey {
mesh_key: view_key,
bind_group_data: material_bind_group
.get_extra_data(material.binding.slot)
.clone(),
},
fake_vertex_buffer_layout,
) else {
continue;
};
let material_fragment = material_pipeline_descriptor.fragment.unwrap();
let mut shader_defs = material_fragment.shader_defs;
shader_defs.push("MESHLET_MESH_MATERIAL_PASS".into());
let view_layout = if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) {
prepass_pipeline.internal.view_layout_motion_vectors.clone()
} else {
prepass_pipeline
.internal
.view_layout_no_motion_vectors
.clone()
};
let fragment_shader = if view_key.contains(MeshPipelineKey::DEFERRED_PREPASS) {
M::meshlet_mesh_deferred_fragment_shader()
} else {
M::meshlet_mesh_prepass_fragment_shader()
};
let entry_point = match fragment_shader {
ShaderRef::Default => "prepass_fragment".into(),
_ => material_fragment.entry_point,
};
let pipeline_descriptor = RenderPipelineDescriptor {
label: material_pipeline_descriptor.label,
layout: vec![
view_layout,
resource_manager.material_shade_bind_group_layout.clone(),
prepass_pipeline.internal.material_layout.clone(),
],
push_constant_ranges: vec![],
vertex: VertexState {
shader: MESHLET_MESH_MATERIAL_SHADER_HANDLE,
shader_defs: shader_defs.clone(),
entry_point: material_pipeline_descriptor.vertex.entry_point,
buffers: Vec::new(),
},
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Depth16Unorm,
depth_write_enabled: false,
depth_compare: CompareFunction::Equal,
stencil: StencilState::default(),
bias: DepthBiasState::default(),
}),
multisample: MultisampleState::default(),
fragment: Some(FragmentState {
shader: match fragment_shader {
ShaderRef::Default => MESHLET_MESH_MATERIAL_SHADER_HANDLE,
ShaderRef::Handle(handle) => handle,
ShaderRef::Path(path) => asset_server.load(path),
},
shader_defs,
entry_point,
targets: material_fragment.targets,
}),
zero_initialize_workgroup_memory: false,
};
let material_id = instance_manager.get_material_id(material_id.untyped());
let pipeline_id = *cache.entry(view_key).or_insert_with(|| {
pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone())
});
let Some(material_bind_group) =
material_bind_group_allocator.get(material.binding.group)
else {
continue;
};
let Some(bind_group) = material_bind_group.bind_group() else {
continue;
};
let item = (material_id, pipeline_id, (*bind_group).clone());
if view_key.contains(MeshPipelineKey::DEFERRED_PREPASS) {
deferred_materials.push(item);
} else {
materials.push(item);
}
}
}
}
// Meshlet materials don't use a traditional vertex buffer, but the material specialization requires one.
fn fake_vertex_buffer_layout(layouts: &mut MeshVertexBufferLayouts) -> MeshVertexBufferLayoutRef {
layouts.insert(MeshVertexBufferLayout::new(
vec![
Mesh::ATTRIBUTE_POSITION.id,
Mesh::ATTRIBUTE_NORMAL.id,
Mesh::ATTRIBUTE_UV_0.id,
Mesh::ATTRIBUTE_TANGENT.id,
],
VertexBufferLayout {
array_stride: 48,
step_mode: VertexStepMode::Vertex,
attributes: vec![
VertexAttribute {
format: Mesh::ATTRIBUTE_POSITION.format,
offset: 0,
shader_location: 0,
},
VertexAttribute {
format: Mesh::ATTRIBUTE_NORMAL.format,
offset: 12,
shader_location: 1,
},
VertexAttribute {
format: Mesh::ATTRIBUTE_UV_0.format,
offset: 24,
shader_location: 2,
},
VertexAttribute {
format: Mesh::ATTRIBUTE_TANGENT.format,
offset: 32,
shader_location: 3,
},
],
},
))
}

View File

@@ -0,0 +1,390 @@
use super::{
material_pipeline_prepare::{
MeshletViewMaterialsDeferredGBufferPrepass, MeshletViewMaterialsMainOpaquePass,
MeshletViewMaterialsPrepass,
},
resource_manager::{MeshletViewBindGroups, MeshletViewResources},
InstanceManager,
};
use crate::{
MeshViewBindGroup, PrepassViewBindGroup, ViewEnvironmentMapUniformOffset, ViewFogUniformOffset,
ViewLightProbesUniformOffset, ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset,
};
use bevy_core_pipeline::prepass::{
MotionVectorPrepass, PreviousViewUniformOffset, ViewPrepassTextures,
};
use bevy_ecs::{
query::{Has, QueryItem},
world::World,
};
use bevy_render::{
camera::ExtractedCamera,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_resource::{
LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor,
StoreOp,
},
renderer::RenderContext,
view::{ViewTarget, ViewUniformOffset},
};
/// Fullscreen shading pass based on the visibility buffer generated from rasterizing meshlets.
#[derive(Default)]
pub struct MeshletMainOpaquePass3dNode;
impl ViewNode for MeshletMainOpaquePass3dNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ViewTarget,
&'static MeshViewBindGroup,
&'static ViewUniformOffset,
&'static ViewLightsUniformOffset,
&'static ViewFogUniformOffset,
&'static ViewLightProbesUniformOffset,
&'static ViewScreenSpaceReflectionsUniformOffset,
&'static ViewEnvironmentMapUniformOffset,
&'static MeshletViewMaterialsMainOpaquePass,
&'static MeshletViewBindGroups,
&'static MeshletViewResources,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(
camera,
target,
mesh_view_bind_group,
view_uniform_offset,
view_lights_offset,
view_fog_offset,
view_light_probes_offset,
view_ssr_offset,
view_environment_map_offset,
meshlet_view_materials,
meshlet_view_bind_groups,
meshlet_view_resources,
): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
if meshlet_view_materials.is_empty() {
return Ok(());
}
let (
Some(instance_manager),
Some(pipeline_cache),
Some(meshlet_material_depth),
Some(meshlet_material_shade_bind_group),
) = (
world.get_resource::<InstanceManager>(),
world.get_resource::<PipelineCache>(),
meshlet_view_resources.material_depth.as_ref(),
meshlet_view_bind_groups.material_shade.as_ref(),
)
else {
return Ok(());
};
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("meshlet_main_opaque_pass_3d"),
color_attachments: &[Some(target.get_color_attachment())],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &meshlet_material_depth.default_view,
depth_ops: Some(Operations {
load: LoadOp::Load,
store: StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
render_pass.set_bind_group(
0,
&mesh_view_bind_group.value,
&[
view_uniform_offset.offset,
view_lights_offset.offset,
view_fog_offset.offset,
**view_light_probes_offset,
**view_ssr_offset,
**view_environment_map_offset,
],
);
render_pass.set_bind_group(1, meshlet_material_shade_bind_group, &[]);
// 1 fullscreen triangle draw per material
for (material_id, material_pipeline_id, material_bind_group) in
meshlet_view_materials.iter()
{
if instance_manager.material_present_in_scene(material_id) {
if let Some(material_pipeline) =
pipeline_cache.get_render_pipeline(*material_pipeline_id)
{
let x = *material_id * 3;
render_pass.set_render_pipeline(material_pipeline);
render_pass.set_bind_group(2, material_bind_group, &[]);
render_pass.draw(x..(x + 3), 0..1);
}
}
}
Ok(())
}
}
/// Fullscreen pass to generate prepass textures based on the visibility buffer generated from rasterizing meshlets.
#[derive(Default)]
pub struct MeshletPrepassNode;
impl ViewNode for MeshletPrepassNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ViewPrepassTextures,
&'static ViewUniformOffset,
&'static PreviousViewUniformOffset,
Has<MotionVectorPrepass>,
&'static MeshletViewMaterialsPrepass,
&'static MeshletViewBindGroups,
&'static MeshletViewResources,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(
camera,
view_prepass_textures,
view_uniform_offset,
previous_view_uniform_offset,
view_has_motion_vector_prepass,
meshlet_view_materials,
meshlet_view_bind_groups,
meshlet_view_resources,
): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
if meshlet_view_materials.is_empty() {
return Ok(());
}
let (
Some(prepass_view_bind_group),
Some(instance_manager),
Some(pipeline_cache),
Some(meshlet_material_depth),
Some(meshlet_material_shade_bind_group),
) = (
world.get_resource::<PrepassViewBindGroup>(),
world.get_resource::<InstanceManager>(),
world.get_resource::<PipelineCache>(),
meshlet_view_resources.material_depth.as_ref(),
meshlet_view_bind_groups.material_shade.as_ref(),
)
else {
return Ok(());
};
let color_attachments = vec![
view_prepass_textures
.normal
.as_ref()
.map(|normals_texture| normals_texture.get_attachment()),
view_prepass_textures
.motion_vectors
.as_ref()
.map(|motion_vectors_texture| motion_vectors_texture.get_attachment()),
// Use None in place of Deferred attachments
None,
None,
];
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("meshlet_prepass"),
color_attachments: &color_attachments,
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &meshlet_material_depth.default_view,
depth_ops: Some(Operations {
load: LoadOp::Load,
store: StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
if view_has_motion_vector_prepass {
render_pass.set_bind_group(
0,
prepass_view_bind_group.motion_vectors.as_ref().unwrap(),
&[
view_uniform_offset.offset,
previous_view_uniform_offset.offset,
],
);
} else {
render_pass.set_bind_group(
0,
prepass_view_bind_group.no_motion_vectors.as_ref().unwrap(),
&[view_uniform_offset.offset],
);
}
render_pass.set_bind_group(1, meshlet_material_shade_bind_group, &[]);
// 1 fullscreen triangle draw per material
for (material_id, material_pipeline_id, material_bind_group) in
meshlet_view_materials.iter()
{
if instance_manager.material_present_in_scene(material_id) {
if let Some(material_pipeline) =
pipeline_cache.get_render_pipeline(*material_pipeline_id)
{
let x = *material_id * 3;
render_pass.set_render_pipeline(material_pipeline);
render_pass.set_bind_group(2, material_bind_group, &[]);
render_pass.draw(x..(x + 3), 0..1);
}
}
}
Ok(())
}
}
/// Fullscreen pass to generate a gbuffer based on the visibility buffer generated from rasterizing meshlets.
#[derive(Default)]
pub struct MeshletDeferredGBufferPrepassNode;
impl ViewNode for MeshletDeferredGBufferPrepassNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ViewPrepassTextures,
&'static ViewUniformOffset,
&'static PreviousViewUniformOffset,
Has<MotionVectorPrepass>,
&'static MeshletViewMaterialsDeferredGBufferPrepass,
&'static MeshletViewBindGroups,
&'static MeshletViewResources,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(
camera,
view_prepass_textures,
view_uniform_offset,
previous_view_uniform_offset,
view_has_motion_vector_prepass,
meshlet_view_materials,
meshlet_view_bind_groups,
meshlet_view_resources,
): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
if meshlet_view_materials.is_empty() {
return Ok(());
}
let (
Some(prepass_view_bind_group),
Some(instance_manager),
Some(pipeline_cache),
Some(meshlet_material_depth),
Some(meshlet_material_shade_bind_group),
) = (
world.get_resource::<PrepassViewBindGroup>(),
world.get_resource::<InstanceManager>(),
world.get_resource::<PipelineCache>(),
meshlet_view_resources.material_depth.as_ref(),
meshlet_view_bind_groups.material_shade.as_ref(),
)
else {
return Ok(());
};
let color_attachments = vec![
view_prepass_textures
.normal
.as_ref()
.map(|normals_texture| normals_texture.get_attachment()),
view_prepass_textures
.motion_vectors
.as_ref()
.map(|motion_vectors_texture| motion_vectors_texture.get_attachment()),
view_prepass_textures
.deferred
.as_ref()
.map(|deferred_texture| deferred_texture.get_attachment()),
view_prepass_textures
.deferred_lighting_pass_id
.as_ref()
.map(|deferred_lighting_pass_id| deferred_lighting_pass_id.get_attachment()),
];
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("meshlet_deferred_prepass"),
color_attachments: &color_attachments,
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &meshlet_material_depth.default_view,
depth_ops: Some(Operations {
load: LoadOp::Load,
store: StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
if view_has_motion_vector_prepass {
render_pass.set_bind_group(
0,
prepass_view_bind_group.motion_vectors.as_ref().unwrap(),
&[
view_uniform_offset.offset,
previous_view_uniform_offset.offset,
],
);
} else {
render_pass.set_bind_group(
0,
prepass_view_bind_group.no_motion_vectors.as_ref().unwrap(),
&[view_uniform_offset.offset],
);
}
render_pass.set_bind_group(1, meshlet_material_shade_bind_group, &[]);
// 1 fullscreen triangle draw per material
for (material_id, material_pipeline_id, material_bind_group) in
meshlet_view_materials.iter()
{
if instance_manager.material_present_in_scene(material_id) {
if let Some(material_pipeline) =
pipeline_cache.get_render_pipeline(*material_pipeline_id)
{
let x = *material_id * 3;
render_pass.set_render_pipeline(material_pipeline);
render_pass.set_bind_group(2, material_bind_group, &[]);
render_pass.draw(x..(x + 3), 0..1);
}
}
}
Ok(())
}
}

View File

@@ -0,0 +1,211 @@
#define_import_path bevy_pbr::meshlet_bindings
#import bevy_pbr::mesh_types::Mesh
#import bevy_render::view::View
#import bevy_pbr::prepass_bindings::PreviousViewUniforms
#import bevy_pbr::utils::octahedral_decode_signed
struct Meshlet {
start_vertex_position_bit: u32,
start_vertex_attribute_id: u32,
start_index_id: u32,
packed_a: u32,
packed_b: u32,
min_vertex_position_channel_x: f32,
min_vertex_position_channel_y: f32,
min_vertex_position_channel_z: f32,
}
fn get_meshlet_vertex_count(meshlet: ptr<function, Meshlet>) -> u32 {
return extractBits((*meshlet).packed_a, 0u, 8u);
}
fn get_meshlet_triangle_count(meshlet: ptr<function, Meshlet>) -> u32 {
return extractBits((*meshlet).packed_a, 8u, 8u);
}
struct MeshletBoundingSpheres {
culling_sphere: MeshletBoundingSphere,
lod_group_sphere: MeshletBoundingSphere,
lod_parent_group_sphere: MeshletBoundingSphere,
}
struct MeshletBoundingSphere {
center: vec3<f32>,
radius: f32,
}
struct DispatchIndirectArgs {
x: atomic<u32>,
y: u32,
z: u32,
}
struct DrawIndirectArgs {
vertex_count: u32,
instance_count: atomic<u32>,
first_vertex: u32,
first_instance: u32,
}
const CENTIMETERS_PER_METER = 100.0;
#ifdef MESHLET_FILL_CLUSTER_BUFFERS_PASS
var<push_constant> scene_instance_count: u32;
@group(0) @binding(0) var<storage, read> meshlet_instance_meshlet_counts: array<u32>; // Per entity instance
@group(0) @binding(1) var<storage, read> meshlet_instance_meshlet_slice_starts: array<u32>; // Per entity instance
@group(0) @binding(2) var<storage, read_write> meshlet_cluster_instance_ids: array<u32>; // Per cluster
@group(0) @binding(3) var<storage, read_write> meshlet_cluster_meshlet_ids: array<u32>; // Per cluster
@group(0) @binding(4) var<storage, read_write> meshlet_global_cluster_count: atomic<u32>; // Single object shared between all workgroups
#endif
#ifdef MESHLET_CULLING_PASS
struct Constants { scene_cluster_count: u32, meshlet_raster_cluster_rightmost_slot: u32 }
var<push_constant> constants: Constants;
@group(0) @binding(0) var<storage, read> meshlet_cluster_meshlet_ids: array<u32>; // Per cluster
@group(0) @binding(1) var<storage, read> meshlet_bounding_spheres: array<MeshletBoundingSpheres>; // Per meshlet
@group(0) @binding(2) var<storage, read> meshlet_simplification_errors: array<u32>; // Per meshlet
@group(0) @binding(3) var<storage, read> meshlet_cluster_instance_ids: array<u32>; // Per cluster
@group(0) @binding(4) var<storage, read> meshlet_instance_uniforms: array<Mesh>; // Per entity instance
@group(0) @binding(5) var<storage, read> meshlet_view_instance_visibility: array<u32>; // 1 bit per entity instance, packed as a bitmask
@group(0) @binding(6) var<storage, read_write> meshlet_second_pass_candidates: array<atomic<u32>>; // 1 bit per cluster , packed as a bitmask
@group(0) @binding(7) var<storage, read_write> meshlet_software_raster_indirect_args: DispatchIndirectArgs; // Single object shared between all workgroups
@group(0) @binding(8) var<storage, read_write> meshlet_hardware_raster_indirect_args: DrawIndirectArgs; // Single object shared between all workgroups
@group(0) @binding(9) var<storage, read_write> meshlet_raster_clusters: array<u32>; // Single object shared between all workgroups
@group(0) @binding(10) var depth_pyramid: texture_2d<f32>; // From the end of the last frame for the first culling pass, and from the first raster pass for the second culling pass
@group(0) @binding(11) var<uniform> view: View;
@group(0) @binding(12) var<uniform> previous_view: PreviousViewUniforms;
fn should_cull_instance(instance_id: u32) -> bool {
let bit_offset = instance_id % 32u;
let packed_visibility = meshlet_view_instance_visibility[instance_id / 32u];
return bool(extractBits(packed_visibility, bit_offset, 1u));
}
// TODO: Load 4x per workgroup instead of once per thread?
fn cluster_is_second_pass_candidate(cluster_id: u32) -> bool {
let packed_candidates = meshlet_second_pass_candidates[cluster_id / 32u];
let bit_offset = cluster_id % 32u;
return bool(extractBits(packed_candidates, bit_offset, 1u));
}
#endif
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS
@group(0) @binding(0) var<storage, read> meshlet_cluster_meshlet_ids: array<u32>; // Per cluster
@group(0) @binding(1) var<storage, read> meshlets: array<Meshlet>; // Per meshlet
@group(0) @binding(2) var<storage, read> meshlet_indices: array<u32>; // Many per meshlet
@group(0) @binding(3) var<storage, read> meshlet_vertex_positions: array<u32>; // Many per meshlet
@group(0) @binding(4) var<storage, read> meshlet_cluster_instance_ids: array<u32>; // Per cluster
@group(0) @binding(5) var<storage, read> meshlet_instance_uniforms: array<Mesh>; // Per entity instance
@group(0) @binding(6) var<storage, read> meshlet_raster_clusters: array<u32>; // Single object shared between all workgroups
@group(0) @binding(7) var<storage, read> meshlet_software_raster_cluster_count: u32;
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
@group(0) @binding(8) var meshlet_visibility_buffer: texture_storage_2d<r64uint, atomic>;
#else
@group(0) @binding(8) var meshlet_visibility_buffer: texture_storage_2d<r32uint, atomic>;
#endif
@group(0) @binding(9) var<uniform> view: View;
// TODO: Load only twice, instead of 3x in cases where you load 3 indices per thread?
fn get_meshlet_vertex_id(index_id: u32) -> u32 {
let packed_index = meshlet_indices[index_id / 4u];
let bit_offset = (index_id % 4u) * 8u;
return extractBits(packed_index, bit_offset, 8u);
}
fn get_meshlet_vertex_position(meshlet: ptr<function, Meshlet>, vertex_id: u32) -> vec3<f32> {
// Get bitstream start for the vertex
let unpacked = unpack4xU8((*meshlet).packed_b);
let bits_per_channel = unpacked.xyz;
let bits_per_vertex = bits_per_channel.x + bits_per_channel.y + bits_per_channel.z;
var start_bit = (*meshlet).start_vertex_position_bit + (vertex_id * bits_per_vertex);
// Read each vertex channel from the bitstream
var vertex_position_packed = vec3(0u);
for (var i = 0u; i < 3u; i++) {
let lower_word_index = start_bit / 32u;
let lower_word_bit_offset = start_bit & 31u;
var next_32_bits = meshlet_vertex_positions[lower_word_index] >> lower_word_bit_offset;
if lower_word_bit_offset + bits_per_channel[i] > 32u {
next_32_bits |= meshlet_vertex_positions[lower_word_index + 1u] << (32u - lower_word_bit_offset);
}
vertex_position_packed[i] = extractBits(next_32_bits, 0u, bits_per_channel[i]);
start_bit += bits_per_channel[i];
}
// Remap [0, range_max - range_min] vec3<u32> to [range_min, range_max] vec3<f32>
var vertex_position = vec3<f32>(vertex_position_packed) + vec3(
(*meshlet).min_vertex_position_channel_x,
(*meshlet).min_vertex_position_channel_y,
(*meshlet).min_vertex_position_channel_z,
);
// Reverse vertex quantization
let vertex_position_quantization_factor = unpacked.w;
vertex_position /= f32(1u << vertex_position_quantization_factor) * CENTIMETERS_PER_METER;
return vertex_position;
}
#endif
#ifdef MESHLET_MESH_MATERIAL_PASS
@group(1) @binding(0) var meshlet_visibility_buffer: texture_storage_2d<r64uint, read>;
@group(1) @binding(1) var<storage, read> meshlet_cluster_meshlet_ids: array<u32>; // Per cluster
@group(1) @binding(2) var<storage, read> meshlets: array<Meshlet>; // Per meshlet
@group(1) @binding(3) var<storage, read> meshlet_indices: array<u32>; // Many per meshlet
@group(1) @binding(4) var<storage, read> meshlet_vertex_positions: array<u32>; // Many per meshlet
@group(1) @binding(5) var<storage, read> meshlet_vertex_normals: array<u32>; // Many per meshlet
@group(1) @binding(6) var<storage, read> meshlet_vertex_uvs: array<vec2<f32>>; // Many per meshlet
@group(1) @binding(7) var<storage, read> meshlet_cluster_instance_ids: array<u32>; // Per cluster
@group(1) @binding(8) var<storage, read> meshlet_instance_uniforms: array<Mesh>; // Per entity instance
// TODO: Load only twice, instead of 3x in cases where you load 3 indices per thread?
fn get_meshlet_vertex_id(index_id: u32) -> u32 {
let packed_index = meshlet_indices[index_id / 4u];
let bit_offset = (index_id % 4u) * 8u;
return extractBits(packed_index, bit_offset, 8u);
}
fn get_meshlet_vertex_position(meshlet: ptr<function, Meshlet>, vertex_id: u32) -> vec3<f32> {
// Get bitstream start for the vertex
let unpacked = unpack4xU8((*meshlet).packed_b);
let bits_per_channel = unpacked.xyz;
let bits_per_vertex = bits_per_channel.x + bits_per_channel.y + bits_per_channel.z;
var start_bit = (*meshlet).start_vertex_position_bit + (vertex_id * bits_per_vertex);
// Read each vertex channel from the bitstream
var vertex_position_packed = vec3(0u);
for (var i = 0u; i < 3u; i++) {
let lower_word_index = start_bit / 32u;
let lower_word_bit_offset = start_bit & 31u;
var next_32_bits = meshlet_vertex_positions[lower_word_index] >> lower_word_bit_offset;
if lower_word_bit_offset + bits_per_channel[i] > 32u {
next_32_bits |= meshlet_vertex_positions[lower_word_index + 1u] << (32u - lower_word_bit_offset);
}
vertex_position_packed[i] = extractBits(next_32_bits, 0u, bits_per_channel[i]);
start_bit += bits_per_channel[i];
}
// Remap [0, range_max - range_min] vec3<u32> to [range_min, range_max] vec3<f32>
var vertex_position = vec3<f32>(vertex_position_packed) + vec3(
(*meshlet).min_vertex_position_channel_x,
(*meshlet).min_vertex_position_channel_y,
(*meshlet).min_vertex_position_channel_z,
);
// Reverse vertex quantization
let vertex_position_quantization_factor = unpacked.w;
vertex_position /= f32(1u << vertex_position_quantization_factor) * CENTIMETERS_PER_METER;
return vertex_position;
}
fn get_meshlet_vertex_normal(meshlet: ptr<function, Meshlet>, vertex_id: u32) -> vec3<f32> {
let packed_normal = meshlet_vertex_normals[(*meshlet).start_vertex_attribute_id + vertex_id];
return octahedral_decode_signed(unpack2x16snorm(packed_normal));
}
fn get_meshlet_vertex_uv(meshlet: ptr<function, Meshlet>, vertex_id: u32) -> vec2<f32> {
return meshlet_vertex_uvs[(*meshlet).start_vertex_attribute_id + vertex_id];
}
#endif

View File

@@ -0,0 +1,163 @@
use super::{
asset::{Meshlet, MeshletBoundingSpheres, MeshletSimplificationError},
persistent_buffer::PersistentGpuBuffer,
MeshletMesh,
};
use alloc::sync::Arc;
use bevy_asset::{AssetId, Assets};
use bevy_ecs::{
resource::Resource,
system::{Res, ResMut},
world::{FromWorld, World},
};
use bevy_math::Vec2;
use bevy_platform::collections::HashMap;
use bevy_render::{
render_resource::BufferAddress,
renderer::{RenderDevice, RenderQueue},
};
use core::ops::Range;
/// Manages uploading [`MeshletMesh`] asset data to the GPU.
#[derive(Resource)]
pub struct MeshletMeshManager {
pub vertex_positions: PersistentGpuBuffer<Arc<[u32]>>,
pub vertex_normals: PersistentGpuBuffer<Arc<[u32]>>,
pub vertex_uvs: PersistentGpuBuffer<Arc<[Vec2]>>,
pub indices: PersistentGpuBuffer<Arc<[u8]>>,
pub meshlets: PersistentGpuBuffer<Arc<[Meshlet]>>,
pub meshlet_bounding_spheres: PersistentGpuBuffer<Arc<[MeshletBoundingSpheres]>>,
pub meshlet_simplification_errors: PersistentGpuBuffer<Arc<[MeshletSimplificationError]>>,
meshlet_mesh_slices: HashMap<AssetId<MeshletMesh>, [Range<BufferAddress>; 7]>,
}
impl FromWorld for MeshletMeshManager {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
Self {
vertex_positions: PersistentGpuBuffer::new("meshlet_vertex_positions", render_device),
vertex_normals: PersistentGpuBuffer::new("meshlet_vertex_normals", render_device),
vertex_uvs: PersistentGpuBuffer::new("meshlet_vertex_uvs", render_device),
indices: PersistentGpuBuffer::new("meshlet_indices", render_device),
meshlets: PersistentGpuBuffer::new("meshlets", render_device),
meshlet_bounding_spheres: PersistentGpuBuffer::new(
"meshlet_bounding_spheres",
render_device,
),
meshlet_simplification_errors: PersistentGpuBuffer::new(
"meshlet_simplification_errors",
render_device,
),
meshlet_mesh_slices: HashMap::default(),
}
}
}
impl MeshletMeshManager {
pub fn queue_upload_if_needed(
&mut self,
asset_id: AssetId<MeshletMesh>,
assets: &mut Assets<MeshletMesh>,
) -> Range<u32> {
let queue_meshlet_mesh = |asset_id: &AssetId<MeshletMesh>| {
let meshlet_mesh = assets.remove_untracked(*asset_id).expect(
"MeshletMesh asset was already unloaded but is not registered with MeshletMeshManager",
);
let vertex_positions_slice = self
.vertex_positions
.queue_write(Arc::clone(&meshlet_mesh.vertex_positions), ());
let vertex_normals_slice = self
.vertex_normals
.queue_write(Arc::clone(&meshlet_mesh.vertex_normals), ());
let vertex_uvs_slice = self
.vertex_uvs
.queue_write(Arc::clone(&meshlet_mesh.vertex_uvs), ());
let indices_slice = self
.indices
.queue_write(Arc::clone(&meshlet_mesh.indices), ());
let meshlets_slice = self.meshlets.queue_write(
Arc::clone(&meshlet_mesh.meshlets),
(
vertex_positions_slice.start,
vertex_normals_slice.start,
indices_slice.start,
),
);
let meshlet_bounding_spheres_slice = self
.meshlet_bounding_spheres
.queue_write(Arc::clone(&meshlet_mesh.meshlet_bounding_spheres), ());
let meshlet_simplification_errors_slice = self
.meshlet_simplification_errors
.queue_write(Arc::clone(&meshlet_mesh.meshlet_simplification_errors), ());
[
vertex_positions_slice,
vertex_normals_slice,
vertex_uvs_slice,
indices_slice,
meshlets_slice,
meshlet_bounding_spheres_slice,
meshlet_simplification_errors_slice,
]
};
// If the MeshletMesh asset has not been uploaded to the GPU yet, queue it for uploading
let [_, _, _, _, meshlets_slice, _, _] = self
.meshlet_mesh_slices
.entry(asset_id)
.or_insert_with_key(queue_meshlet_mesh)
.clone();
let meshlets_slice_start = meshlets_slice.start as u32 / size_of::<Meshlet>() as u32;
let meshlets_slice_end = meshlets_slice.end as u32 / size_of::<Meshlet>() as u32;
meshlets_slice_start..meshlets_slice_end
}
pub fn remove(&mut self, asset_id: &AssetId<MeshletMesh>) {
if let Some(
[vertex_positions_slice, vertex_normals_slice, vertex_uvs_slice, indices_slice, meshlets_slice, meshlet_bounding_spheres_slice, meshlet_simplification_errors_slice],
) = self.meshlet_mesh_slices.remove(asset_id)
{
self.vertex_positions
.mark_slice_unused(vertex_positions_slice);
self.vertex_normals.mark_slice_unused(vertex_normals_slice);
self.vertex_uvs.mark_slice_unused(vertex_uvs_slice);
self.indices.mark_slice_unused(indices_slice);
self.meshlets.mark_slice_unused(meshlets_slice);
self.meshlet_bounding_spheres
.mark_slice_unused(meshlet_bounding_spheres_slice);
self.meshlet_simplification_errors
.mark_slice_unused(meshlet_simplification_errors_slice);
}
}
}
/// Upload all newly queued [`MeshletMesh`] asset data to the GPU.
pub fn perform_pending_meshlet_mesh_writes(
mut meshlet_mesh_manager: ResMut<MeshletMeshManager>,
render_queue: Res<RenderQueue>,
render_device: Res<RenderDevice>,
) {
meshlet_mesh_manager
.vertex_positions
.perform_writes(&render_queue, &render_device);
meshlet_mesh_manager
.vertex_normals
.perform_writes(&render_queue, &render_device);
meshlet_mesh_manager
.vertex_uvs
.perform_writes(&render_queue, &render_device);
meshlet_mesh_manager
.indices
.perform_writes(&render_queue, &render_device);
meshlet_mesh_manager
.meshlets
.perform_writes(&render_queue, &render_device);
meshlet_mesh_manager
.meshlet_bounding_spheres
.perform_writes(&render_queue, &render_device);
meshlet_mesh_manager
.meshlet_simplification_errors
.perform_writes(&render_queue, &render_device);
}

View File

@@ -0,0 +1,52 @@
#import bevy_pbr::{
meshlet_visibility_buffer_resolve::resolve_vertex_output,
view_transformations::uv_to_ndc,
prepass_io,
pbr_prepass_functions,
utils::rand_f,
}
@vertex
fn vertex(@builtin(vertex_index) vertex_input: u32) -> @builtin(position) vec4<f32> {
let vertex_index = vertex_input % 3u;
let material_id = vertex_input / 3u;
let material_depth = f32(material_id) / 65535.0;
let uv = vec2<f32>(vec2(vertex_index >> 1u, vertex_index & 1u)) * 2.0;
return vec4(uv_to_ndc(uv), material_depth, 1.0);
}
@fragment
fn fragment(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {
let vertex_output = resolve_vertex_output(frag_coord);
var rng = vertex_output.cluster_id;
let color = vec3(rand_f(&rng), rand_f(&rng), rand_f(&rng));
return vec4(color, 1.0);
}
#ifdef PREPASS_FRAGMENT
@fragment
fn prepass_fragment(@builtin(position) frag_coord: vec4<f32>) -> prepass_io::FragmentOutput {
let vertex_output = resolve_vertex_output(frag_coord);
var out: prepass_io::FragmentOutput;
#ifdef NORMAL_PREPASS
out.normal = vec4(vertex_output.world_normal * 0.5 + vec3(0.5), 1.0);
#endif
#ifdef MOTION_VECTOR_PREPASS
out.motion_vector = vertex_output.motion_vector;
#endif
#ifdef DEFERRED_PREPASS
// There isn't any material info available for this default prepass shader so we are just writing 
// emissive magenta out to the deferred gbuffer to be rendered by the first deferred lighting pass layer.
// This is here so if the default prepass fragment is used for deferred magenta will be rendered, and also
// as an example to show that a user could write to the deferred gbuffer if they were to start from this shader.
out.deferred = vec4(0u, bevy_pbr::rgb9e5::vec3_to_rgb9e5_(vec3(1.0, 0.0, 1.0)), 0u, 0u);
out.deferred_lighting_pass_id = 1u;
#endif
return out;
}
#endif

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

339
vendor/bevy_pbr/src/meshlet/mod.rs vendored Normal file
View File

@@ -0,0 +1,339 @@
//! Render high-poly 3d meshes using an efficient GPU-driven method. See [`MeshletPlugin`] and [`MeshletMesh`] for details.
mod asset;
#[cfg(feature = "meshlet_processor")]
mod from_mesh;
mod instance_manager;
mod material_pipeline_prepare;
mod material_shade_nodes;
mod meshlet_mesh_manager;
mod persistent_buffer;
mod persistent_buffer_impls;
mod pipelines;
mod resource_manager;
mod visibility_buffer_raster_node;
pub mod graph {
use bevy_render::render_graph::RenderLabel;
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
pub enum NodeMeshlet {
VisibilityBufferRasterPass,
Prepass,
DeferredPrepass,
MainOpaquePass,
}
}
pub(crate) use self::{
instance_manager::{queue_material_meshlet_meshes, InstanceManager},
material_pipeline_prepare::{
prepare_material_meshlet_meshes_main_opaque_pass, prepare_material_meshlet_meshes_prepass,
},
};
pub use self::asset::{
MeshletMesh, MeshletMeshLoader, MeshletMeshSaver, MESHLET_MESH_ASSET_VERSION,
};
#[cfg(feature = "meshlet_processor")]
pub use self::from_mesh::{
MeshToMeshletMeshConversionError, MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR,
};
use self::{
graph::NodeMeshlet,
instance_manager::extract_meshlet_mesh_entities,
material_pipeline_prepare::{
MeshletViewMaterialsDeferredGBufferPrepass, MeshletViewMaterialsMainOpaquePass,
MeshletViewMaterialsPrepass,
},
material_shade_nodes::{
MeshletDeferredGBufferPrepassNode, MeshletMainOpaquePass3dNode, MeshletPrepassNode,
},
meshlet_mesh_manager::{perform_pending_meshlet_mesh_writes, MeshletMeshManager},
pipelines::*,
resource_manager::{
prepare_meshlet_per_frame_resources, prepare_meshlet_view_bind_groups, ResourceManager,
},
visibility_buffer_raster_node::MeshletVisibilityBufferRasterPassNode,
};
use crate::{graph::NodePbr, PreviousGlobalTransform};
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, AssetApp, AssetId, Handle};
use bevy_core_pipeline::{
core_3d::graph::{Core3d, Node3d},
prepass::{DeferredPrepass, MotionVectorPrepass, NormalPrepass},
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
query::Has,
reflect::ReflectComponent,
schedule::IntoScheduleConfigs,
system::{Commands, Query},
};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
render_graph::{RenderGraphApp, ViewNodeRunner},
render_resource::Shader,
renderer::RenderDevice,
settings::WgpuFeatures,
view::{self, prepare_view_targets, Msaa, Visibility, VisibilityClass},
ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::components::Transform;
use derive_more::From;
use tracing::error;
const MESHLET_BINDINGS_SHADER_HANDLE: Handle<Shader> =
weak_handle!("d90ac78c-500f-48aa-b488-cc98eb3f6314");
const MESHLET_MESH_MATERIAL_SHADER_HANDLE: Handle<Shader> =
weak_handle!("db8d9001-6ca7-4d00-968a-d5f5b96b89c3");
/// Provides a plugin for rendering large amounts of high-poly 3d meshes using an efficient GPU-driven method. See also [`MeshletMesh`].
///
/// Rendering dense scenes made of high-poly meshes with thousands or millions of triangles is extremely expensive in Bevy's standard renderer.
/// Once meshes are pre-processed into a [`MeshletMesh`], this plugin can render these kinds of scenes very efficiently.
///
/// In comparison to Bevy's standard renderer:
/// * Much more efficient culling. Meshlets can be culled individually, instead of all or nothing culling for entire meshes at a time.
/// Additionally, occlusion culling can eliminate meshlets that would cause overdraw.
/// * Much more efficient batching. All geometry can be rasterized in a single draw.
/// * Scales better with large amounts of dense geometry and overdraw. Bevy's standard renderer will bottleneck sooner.
/// * Near-seamless level of detail (LOD).
/// * Much greater base overhead. Rendering will be slower and use more memory than Bevy's standard renderer
/// with small amounts of geometry and overdraw.
/// * Requires preprocessing meshes. See [`MeshletMesh`] for details.
/// * Limitations on the kinds of materials you can use. See [`MeshletMesh`] for details.
///
/// This plugin requires a fairly recent GPU that supports [`WgpuFeatures::TEXTURE_INT64_ATOMIC`].
///
/// This plugin currently works only on the Vulkan and Metal backends.
///
/// This plugin is not compatible with [`Msaa`]. Any camera rendering a [`MeshletMesh`] must have
/// [`Msaa`] set to [`Msaa::Off`].
///
/// Mixing forward+prepass and deferred rendering for opaque materials is not currently supported when using this plugin.
/// You must use one or the other by setting [`crate::DefaultOpaqueRendererMethod`].
/// Do not override [`crate::Material::opaque_render_method`] for any material when using this plugin.
///
/// ![A render of the Stanford dragon as a `MeshletMesh`](https://raw.githubusercontent.com/bevyengine/bevy/main/crates/bevy_pbr/src/meshlet/meshlet_preview.png)
pub struct MeshletPlugin {
/// The maximum amount of clusters that can be processed at once,
/// used to control the size of a pre-allocated GPU buffer.
///
/// If this number is too low, you'll see rendering artifacts like missing or blinking meshes.
///
/// Each cluster slot costs 4 bytes of VRAM.
///
/// Must not be greater than 2^25.
pub cluster_buffer_slots: u32,
}
impl MeshletPlugin {
/// [`WgpuFeatures`] required for this plugin to function.
pub fn required_wgpu_features() -> WgpuFeatures {
WgpuFeatures::TEXTURE_INT64_ATOMIC
| WgpuFeatures::TEXTURE_ATOMIC
| WgpuFeatures::SHADER_INT64
| WgpuFeatures::SUBGROUP
| WgpuFeatures::DEPTH_CLIP_CONTROL
| WgpuFeatures::PUSH_CONSTANTS
}
}
impl Plugin for MeshletPlugin {
fn build(&self, app: &mut App) {
#[cfg(target_endian = "big")]
compile_error!("MeshletPlugin is only supported on little-endian processors.");
if self.cluster_buffer_slots > 2_u32.pow(25) {
error!("MeshletPlugin::cluster_buffer_slots must not be greater than 2^25.");
std::process::exit(1);
}
load_internal_asset!(
app,
MESHLET_CLEAR_VISIBILITY_BUFFER_SHADER_HANDLE,
"clear_visibility_buffer.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESHLET_BINDINGS_SHADER_HANDLE,
"meshlet_bindings.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
super::MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE,
"visibility_buffer_resolve.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE,
"fill_cluster_buffers.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESHLET_CULLING_SHADER_HANDLE,
"cull_clusters.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE,
"visibility_buffer_software_raster.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE,
"visibility_buffer_hardware_raster.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESHLET_MESH_MATERIAL_SHADER_HANDLE,
"meshlet_mesh_material.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE,
"resolve_render_targets.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
MESHLET_REMAP_1D_TO_2D_DISPATCH_SHADER_HANDLE,
"remap_1d_to_2d_dispatch.wgsl",
Shader::from_wgsl
);
app.init_asset::<MeshletMesh>()
.register_asset_loader(MeshletMeshLoader);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
let render_device = render_app.world().resource::<RenderDevice>().clone();
let features = render_device.features();
if !features.contains(Self::required_wgpu_features()) {
error!(
"MeshletPlugin can't be used. GPU lacks support for required features: {:?}.",
Self::required_wgpu_features().difference(features)
);
std::process::exit(1);
}
render_app
.add_render_graph_node::<MeshletVisibilityBufferRasterPassNode>(
Core3d,
NodeMeshlet::VisibilityBufferRasterPass,
)
.add_render_graph_node::<ViewNodeRunner<MeshletPrepassNode>>(
Core3d,
NodeMeshlet::Prepass,
)
.add_render_graph_node::<ViewNodeRunner<MeshletDeferredGBufferPrepassNode>>(
Core3d,
NodeMeshlet::DeferredPrepass,
)
.add_render_graph_node::<ViewNodeRunner<MeshletMainOpaquePass3dNode>>(
Core3d,
NodeMeshlet::MainOpaquePass,
)
.add_render_graph_edges(
Core3d,
(
NodeMeshlet::VisibilityBufferRasterPass,
NodePbr::EarlyShadowPass,
//
NodeMeshlet::Prepass,
//
NodeMeshlet::DeferredPrepass,
Node3d::EndPrepasses,
//
Node3d::StartMainPass,
NodeMeshlet::MainOpaquePass,
Node3d::MainOpaquePass,
Node3d::EndMainPass,
),
)
.init_resource::<MeshletMeshManager>()
.insert_resource(InstanceManager::new())
.insert_resource(ResourceManager::new(
self.cluster_buffer_slots,
&render_device,
))
.init_resource::<MeshletPipelines>()
.add_systems(ExtractSchedule, extract_meshlet_mesh_entities)
.add_systems(
Render,
(
perform_pending_meshlet_mesh_writes.in_set(RenderSet::PrepareAssets),
configure_meshlet_views
.after(prepare_view_targets)
.in_set(RenderSet::ManageViews),
prepare_meshlet_per_frame_resources.in_set(RenderSet::PrepareResources),
prepare_meshlet_view_bind_groups.in_set(RenderSet::PrepareBindGroups),
),
);
}
}
/// The meshlet mesh equivalent of [`bevy_render::mesh::Mesh3d`].
#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)]
#[reflect(Component, Default, Clone, PartialEq)]
#[require(Transform, PreviousGlobalTransform, Visibility, VisibilityClass)]
#[component(on_add = view::add_visibility_class::<MeshletMesh3d>)]
pub struct MeshletMesh3d(pub Handle<MeshletMesh>);
impl From<MeshletMesh3d> for AssetId<MeshletMesh> {
fn from(mesh: MeshletMesh3d) -> Self {
mesh.id()
}
}
impl From<&MeshletMesh3d> for AssetId<MeshletMesh> {
fn from(mesh: &MeshletMesh3d) -> Self {
mesh.id()
}
}
fn configure_meshlet_views(
mut views_3d: Query<(
Entity,
&Msaa,
Has<NormalPrepass>,
Has<MotionVectorPrepass>,
Has<DeferredPrepass>,
)>,
mut commands: Commands,
) {
for (entity, msaa, normal_prepass, motion_vector_prepass, deferred_prepass) in &mut views_3d {
if *msaa != Msaa::Off {
error!("MeshletPlugin can't be used with MSAA. Add Msaa::Off to your camera to use this plugin.");
std::process::exit(1);
}
if !(normal_prepass || motion_vector_prepass || deferred_prepass) {
commands
.entity(entity)
.insert(MeshletViewMaterialsMainOpaquePass::default());
} else {
// TODO: Should we add both Prepass and DeferredGBufferPrepass materials here, and in other systems/nodes?
commands.entity(entity).insert((
MeshletViewMaterialsMainOpaquePass::default(),
MeshletViewMaterialsPrepass::default(),
MeshletViewMaterialsDeferredGBufferPrepass::default(),
));
}
}
}

View File

@@ -0,0 +1,127 @@
use bevy_render::{
render_resource::{
BindingResource, Buffer, BufferAddress, BufferDescriptor, BufferUsages,
CommandEncoderDescriptor, COPY_BUFFER_ALIGNMENT,
},
renderer::{RenderDevice, RenderQueue},
};
use core::{num::NonZero, ops::Range};
use range_alloc::RangeAllocator;
/// Wrapper for a GPU buffer holding a large amount of data that persists across frames.
pub struct PersistentGpuBuffer<T: PersistentGpuBufferable> {
/// Debug label for the buffer.
label: &'static str,
/// Handle to the GPU buffer.
buffer: Buffer,
/// Tracks free slices of the buffer.
allocation_planner: RangeAllocator<BufferAddress>,
/// Queue of pending writes, and associated metadata.
write_queue: Vec<(T, T::Metadata, Range<BufferAddress>)>,
}
impl<T: PersistentGpuBufferable> PersistentGpuBuffer<T> {
/// Create a new persistent buffer.
pub fn new(label: &'static str, render_device: &RenderDevice) -> Self {
Self {
label,
buffer: render_device.create_buffer(&BufferDescriptor {
label: Some(label),
size: 0,
usage: BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC,
mapped_at_creation: false,
}),
allocation_planner: RangeAllocator::new(0..0),
write_queue: Vec::new(),
}
}
/// Queue an item of type T to be added to the buffer, returning the byte range within the buffer that it will be located at.
pub fn queue_write(&mut self, data: T, metadata: T::Metadata) -> Range<BufferAddress> {
let data_size = data.size_in_bytes() as u64;
debug_assert!(data_size % COPY_BUFFER_ALIGNMENT == 0);
if let Ok(buffer_slice) = self.allocation_planner.allocate_range(data_size) {
self.write_queue
.push((data, metadata, buffer_slice.clone()));
return buffer_slice;
}
let buffer_size = self.allocation_planner.initial_range();
let double_buffer_size = (buffer_size.end - buffer_size.start) * 2;
let new_size = double_buffer_size.max(data_size);
self.allocation_planner.grow_to(buffer_size.end + new_size);
let buffer_slice = self.allocation_planner.allocate_range(data_size).unwrap();
self.write_queue
.push((data, metadata, buffer_slice.clone()));
buffer_slice
}
/// Upload all pending data to the GPU buffer.
pub fn perform_writes(&mut self, render_queue: &RenderQueue, render_device: &RenderDevice) {
if self.allocation_planner.initial_range().end > self.buffer.size() {
self.expand_buffer(render_device, render_queue);
}
let queue_count = self.write_queue.len();
for (data, metadata, buffer_slice) in self.write_queue.drain(..) {
let buffer_slice_size =
NonZero::<u64>::new(buffer_slice.end - buffer_slice.start).unwrap();
let mut buffer_view = render_queue
.write_buffer_with(&self.buffer, buffer_slice.start, buffer_slice_size)
.unwrap();
data.write_bytes_le(metadata, &mut buffer_view);
}
let queue_saturation = queue_count as f32 / self.write_queue.capacity() as f32;
if queue_saturation < 0.3 {
self.write_queue = Vec::new();
}
}
/// Mark a section of the GPU buffer as no longer needed.
pub fn mark_slice_unused(&mut self, buffer_slice: Range<BufferAddress>) {
self.allocation_planner.free_range(buffer_slice);
}
pub fn binding(&self) -> BindingResource<'_> {
self.buffer.as_entire_binding()
}
/// Expand the buffer by creating a new buffer and copying old data over.
fn expand_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) {
let size = self.allocation_planner.initial_range();
let new_buffer = render_device.create_buffer(&BufferDescriptor {
label: Some(self.label),
size: size.end - size.start,
usage: BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("persistent_gpu_buffer_expand"),
});
command_encoder.copy_buffer_to_buffer(&self.buffer, 0, &new_buffer, 0, self.buffer.size());
render_queue.submit([command_encoder.finish()]);
self.buffer = new_buffer;
}
}
/// A trait representing data that can be written to a [`PersistentGpuBuffer`].
pub trait PersistentGpuBufferable {
/// Additional metadata associated with each item, made available during `write_bytes_le`.
type Metadata;
/// The size in bytes of `self`. This will determine the size of the buffer passed into
/// `write_bytes_le`.
///
/// All data written must be in a multiple of `wgpu::COPY_BUFFER_ALIGNMENT` bytes. Failure to do so will
/// result in a panic when using [`PersistentGpuBuffer`].
fn size_in_bytes(&self) -> usize;
/// Convert `self` + `metadata` into bytes (little-endian), and write to the provided buffer slice.
/// Any bytes not written to in the slice will be zeroed out when uploaded to the GPU.
fn write_bytes_le(&self, metadata: Self::Metadata, buffer_slice: &mut [u8]);
}

View File

@@ -0,0 +1,98 @@
use super::{
asset::{Meshlet, MeshletBoundingSpheres, MeshletSimplificationError},
persistent_buffer::PersistentGpuBufferable,
};
use alloc::sync::Arc;
use bevy_math::Vec2;
impl PersistentGpuBufferable for Arc<[Meshlet]> {
type Metadata = (u64, u64, u64);
fn size_in_bytes(&self) -> usize {
self.len() * size_of::<Meshlet>()
}
fn write_bytes_le(
&self,
(vertex_position_offset, vertex_attribute_offset, index_offset): Self::Metadata,
buffer_slice: &mut [u8],
) {
let vertex_position_offset = (vertex_position_offset * 8) as u32;
let vertex_attribute_offset = (vertex_attribute_offset as usize / size_of::<u32>()) as u32;
let index_offset = index_offset as u32;
for (i, meshlet) in self.iter().enumerate() {
let size = size_of::<Meshlet>();
let i = i * size;
let bytes = bytemuck::cast::<_, [u8; size_of::<Meshlet>()]>(Meshlet {
start_vertex_position_bit: meshlet.start_vertex_position_bit
+ vertex_position_offset,
start_vertex_attribute_id: meshlet.start_vertex_attribute_id
+ vertex_attribute_offset,
start_index_id: meshlet.start_index_id + index_offset,
..*meshlet
});
buffer_slice[i..(i + size)].clone_from_slice(&bytes);
}
}
}
impl PersistentGpuBufferable for Arc<[u8]> {
type Metadata = ();
fn size_in_bytes(&self) -> usize {
self.len()
}
fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) {
buffer_slice.clone_from_slice(self);
}
}
impl PersistentGpuBufferable for Arc<[u32]> {
type Metadata = ();
fn size_in_bytes(&self) -> usize {
self.len() * size_of::<u32>()
}
fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) {
buffer_slice.clone_from_slice(bytemuck::cast_slice(self));
}
}
impl PersistentGpuBufferable for Arc<[Vec2]> {
type Metadata = ();
fn size_in_bytes(&self) -> usize {
self.len() * size_of::<Vec2>()
}
fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) {
buffer_slice.clone_from_slice(bytemuck::cast_slice(self));
}
}
impl PersistentGpuBufferable for Arc<[MeshletBoundingSpheres]> {
type Metadata = ();
fn size_in_bytes(&self) -> usize {
self.len() * size_of::<MeshletBoundingSpheres>()
}
fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) {
buffer_slice.clone_from_slice(bytemuck::cast_slice(self));
}
}
impl PersistentGpuBufferable for Arc<[MeshletSimplificationError]> {
type Metadata = ();
fn size_in_bytes(&self) -> usize {
self.len() * size_of::<MeshletSimplificationError>()
}
fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8]) {
buffer_slice.clone_from_slice(bytemuck::cast_slice(self));
}
}

543
vendor/bevy_pbr/src/meshlet/pipelines.rs vendored Normal file
View File

@@ -0,0 +1,543 @@
use super::resource_manager::ResourceManager;
use bevy_asset::{weak_handle, Handle};
use bevy_core_pipeline::{
core_3d::CORE_3D_DEPTH_FORMAT, experimental::mip_generation::DOWNSAMPLE_DEPTH_SHADER_HANDLE,
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
};
use bevy_ecs::{
resource::Resource,
world::{FromWorld, World},
};
use bevy_render::render_resource::*;
pub const MESHLET_CLEAR_VISIBILITY_BUFFER_SHADER_HANDLE: Handle<Shader> =
weak_handle!("a4bf48e4-5605-4d1c-987e-29c7b1ec95dc");
pub const MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE: Handle<Shader> =
weak_handle!("80ccea4a-8234-4ee0-af74-77b3cad503cf");
pub const MESHLET_CULLING_SHADER_HANDLE: Handle<Shader> =
weak_handle!("d71c5879-97fa-49d1-943e-ed9162fe8adb");
pub const MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE: Handle<Shader> =
weak_handle!("68cc6826-8321-43d1-93d5-4f61f0456c13");
pub const MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE: Handle<Shader> =
weak_handle!("4b4e3020-748f-4baf-b011-87d9d2a12796");
pub const MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE: Handle<Shader> =
weak_handle!("c218ce17-cf59-4268-8898-13ecf384f133");
pub const MESHLET_REMAP_1D_TO_2D_DISPATCH_SHADER_HANDLE: Handle<Shader> =
weak_handle!("f5b7edfc-2eac-4407-8f5c-1265d4d795c2");
#[derive(Resource)]
pub struct MeshletPipelines {
fill_cluster_buffers: CachedComputePipelineId,
clear_visibility_buffer: CachedComputePipelineId,
clear_visibility_buffer_shadow_view: CachedComputePipelineId,
cull_first: CachedComputePipelineId,
cull_second: CachedComputePipelineId,
downsample_depth_first: CachedComputePipelineId,
downsample_depth_second: CachedComputePipelineId,
downsample_depth_first_shadow_view: CachedComputePipelineId,
downsample_depth_second_shadow_view: CachedComputePipelineId,
visibility_buffer_software_raster: CachedComputePipelineId,
visibility_buffer_software_raster_shadow_view: CachedComputePipelineId,
visibility_buffer_hardware_raster: CachedRenderPipelineId,
visibility_buffer_hardware_raster_shadow_view: CachedRenderPipelineId,
visibility_buffer_hardware_raster_shadow_view_unclipped: CachedRenderPipelineId,
resolve_depth: CachedRenderPipelineId,
resolve_depth_shadow_view: CachedRenderPipelineId,
resolve_material_depth: CachedRenderPipelineId,
remap_1d_to_2d_dispatch: Option<CachedComputePipelineId>,
}
impl FromWorld for MeshletPipelines {
fn from_world(world: &mut World) -> Self {
let resource_manager = world.resource::<ResourceManager>();
let fill_cluster_buffers_bind_group_layout = resource_manager
.fill_cluster_buffers_bind_group_layout
.clone();
let clear_visibility_buffer_bind_group_layout = resource_manager
.clear_visibility_buffer_bind_group_layout
.clone();
let clear_visibility_buffer_shadow_view_bind_group_layout = resource_manager
.clear_visibility_buffer_shadow_view_bind_group_layout
.clone();
let cull_layout = resource_manager.culling_bind_group_layout.clone();
let downsample_depth_layout = resource_manager.downsample_depth_bind_group_layout.clone();
let downsample_depth_shadow_view_layout = resource_manager
.downsample_depth_shadow_view_bind_group_layout
.clone();
let visibility_buffer_raster_layout = resource_manager
.visibility_buffer_raster_bind_group_layout
.clone();
let visibility_buffer_raster_shadow_view_layout = resource_manager
.visibility_buffer_raster_shadow_view_bind_group_layout
.clone();
let resolve_depth_layout = resource_manager.resolve_depth_bind_group_layout.clone();
let resolve_depth_shadow_view_layout = resource_manager
.resolve_depth_shadow_view_bind_group_layout
.clone();
let resolve_material_depth_layout = resource_manager
.resolve_material_depth_bind_group_layout
.clone();
let remap_1d_to_2d_dispatch_layout = resource_manager
.remap_1d_to_2d_dispatch_bind_group_layout
.clone();
let pipeline_cache = world.resource_mut::<PipelineCache>();
Self {
fill_cluster_buffers: pipeline_cache.queue_compute_pipeline(
ComputePipelineDescriptor {
label: Some("meshlet_fill_cluster_buffers_pipeline".into()),
layout: vec![fill_cluster_buffers_bind_group_layout],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::COMPUTE,
range: 0..4,
}],
shader: MESHLET_FILL_CLUSTER_BUFFERS_SHADER_HANDLE,
shader_defs: vec!["MESHLET_FILL_CLUSTER_BUFFERS_PASS".into()],
entry_point: "fill_cluster_buffers".into(),
zero_initialize_workgroup_memory: false,
},
),
clear_visibility_buffer: pipeline_cache.queue_compute_pipeline(
ComputePipelineDescriptor {
label: Some("meshlet_clear_visibility_buffer_pipeline".into()),
layout: vec![clear_visibility_buffer_bind_group_layout],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::COMPUTE,
range: 0..8,
}],
shader: MESHLET_CLEAR_VISIBILITY_BUFFER_SHADER_HANDLE,
shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()],
entry_point: "clear_visibility_buffer".into(),
zero_initialize_workgroup_memory: false,
},
),
clear_visibility_buffer_shadow_view: pipeline_cache.queue_compute_pipeline(
ComputePipelineDescriptor {
label: Some("meshlet_clear_visibility_buffer_shadow_view_pipeline".into()),
layout: vec![clear_visibility_buffer_shadow_view_bind_group_layout],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::COMPUTE,
range: 0..8,
}],
shader: MESHLET_CLEAR_VISIBILITY_BUFFER_SHADER_HANDLE,
shader_defs: vec![],
entry_point: "clear_visibility_buffer".into(),
zero_initialize_workgroup_memory: false,
},
),
cull_first: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
label: Some("meshlet_culling_first_pipeline".into()),
layout: vec![cull_layout.clone()],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::COMPUTE,
range: 0..8,
}],
shader: MESHLET_CULLING_SHADER_HANDLE,
shader_defs: vec![
"MESHLET_CULLING_PASS".into(),
"MESHLET_FIRST_CULLING_PASS".into(),
],
entry_point: "cull_clusters".into(),
zero_initialize_workgroup_memory: false,
}),
cull_second: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
label: Some("meshlet_culling_second_pipeline".into()),
layout: vec![cull_layout],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::COMPUTE,
range: 0..8,
}],
shader: MESHLET_CULLING_SHADER_HANDLE,
shader_defs: vec![
"MESHLET_CULLING_PASS".into(),
"MESHLET_SECOND_CULLING_PASS".into(),
],
entry_point: "cull_clusters".into(),
zero_initialize_workgroup_memory: false,
}),
downsample_depth_first: pipeline_cache.queue_compute_pipeline(
ComputePipelineDescriptor {
label: Some("meshlet_downsample_depth_first_pipeline".into()),
layout: vec![downsample_depth_layout.clone()],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::COMPUTE,
range: 0..4,
}],
shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE,
shader_defs: vec![
"MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(),
"MESHLET".into(),
],
entry_point: "downsample_depth_first".into(),
zero_initialize_workgroup_memory: false,
},
),
downsample_depth_second: pipeline_cache.queue_compute_pipeline(
ComputePipelineDescriptor {
label: Some("meshlet_downsample_depth_second_pipeline".into()),
layout: vec![downsample_depth_layout.clone()],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::COMPUTE,
range: 0..4,
}],
shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE,
shader_defs: vec![
"MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(),
"MESHLET".into(),
],
entry_point: "downsample_depth_second".into(),
zero_initialize_workgroup_memory: false,
},
),
downsample_depth_first_shadow_view: pipeline_cache.queue_compute_pipeline(
ComputePipelineDescriptor {
label: Some("meshlet_downsample_depth_first_pipeline".into()),
layout: vec![downsample_depth_shadow_view_layout.clone()],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::COMPUTE,
range: 0..4,
}],
shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE,
shader_defs: vec!["MESHLET".into()],
entry_point: "downsample_depth_first".into(),
zero_initialize_workgroup_memory: false,
},
),
downsample_depth_second_shadow_view: pipeline_cache.queue_compute_pipeline(
ComputePipelineDescriptor {
label: Some("meshlet_downsample_depth_second_pipeline".into()),
layout: vec![downsample_depth_shadow_view_layout],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::COMPUTE,
range: 0..4,
}],
shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE,
shader_defs: vec!["MESHLET".into()],
entry_point: "downsample_depth_second".into(),
zero_initialize_workgroup_memory: false,
},
),
visibility_buffer_software_raster: pipeline_cache.queue_compute_pipeline(
ComputePipelineDescriptor {
label: Some("meshlet_visibility_buffer_software_raster_pipeline".into()),
layout: vec![visibility_buffer_raster_layout.clone()],
push_constant_ranges: vec![],
shader: MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE,
shader_defs: vec![
"MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(),
"MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(),
if remap_1d_to_2d_dispatch_layout.is_some() {
"MESHLET_2D_DISPATCH"
} else {
""
}
.into(),
],
entry_point: "rasterize_cluster".into(),
zero_initialize_workgroup_memory: false,
},
),
visibility_buffer_software_raster_shadow_view: pipeline_cache.queue_compute_pipeline(
ComputePipelineDescriptor {
label: Some(
"meshlet_visibility_buffer_software_raster_shadow_view_pipeline".into(),
),
layout: vec![visibility_buffer_raster_shadow_view_layout.clone()],
push_constant_ranges: vec![],
shader: MESHLET_VISIBILITY_BUFFER_SOFTWARE_RASTER_SHADER_HANDLE,
shader_defs: vec![
"MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(),
if remap_1d_to_2d_dispatch_layout.is_some() {
"MESHLET_2D_DISPATCH"
} else {
""
}
.into(),
],
entry_point: "rasterize_cluster".into(),
zero_initialize_workgroup_memory: false,
},
),
visibility_buffer_hardware_raster: pipeline_cache.queue_render_pipeline(
RenderPipelineDescriptor {
label: Some("meshlet_visibility_buffer_hardware_raster_pipeline".into()),
layout: vec![visibility_buffer_raster_layout.clone()],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::VERTEX,
range: 0..4,
}],
vertex: VertexState {
shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE,
shader_defs: vec![
"MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(),
"MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(),
],
entry_point: "vertex".into(),
buffers: vec![],
},
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: FrontFace::Ccw,
cull_mode: Some(Face::Back),
unclipped_depth: false,
polygon_mode: PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: MultisampleState::default(),
fragment: Some(FragmentState {
shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE,
shader_defs: vec![
"MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(),
"MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(),
],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::R8Uint,
blend: None,
write_mask: ColorWrites::empty(),
})],
}),
zero_initialize_workgroup_memory: false,
},
),
visibility_buffer_hardware_raster_shadow_view: pipeline_cache.queue_render_pipeline(
RenderPipelineDescriptor {
label: Some(
"meshlet_visibility_buffer_hardware_raster_shadow_view_pipeline".into(),
),
layout: vec![visibility_buffer_raster_shadow_view_layout.clone()],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::VERTEX,
range: 0..4,
}],
vertex: VertexState {
shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE,
shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()],
entry_point: "vertex".into(),
buffers: vec![],
},
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: FrontFace::Ccw,
cull_mode: Some(Face::Back),
unclipped_depth: false,
polygon_mode: PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: MultisampleState::default(),
fragment: Some(FragmentState {
shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE,
shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::R8Uint,
blend: None,
write_mask: ColorWrites::empty(),
})],
}),
zero_initialize_workgroup_memory: false,
},
),
visibility_buffer_hardware_raster_shadow_view_unclipped: pipeline_cache
.queue_render_pipeline(RenderPipelineDescriptor {
label: Some(
"meshlet_visibility_buffer_hardware_raster_shadow_view_unclipped_pipeline"
.into(),
),
layout: vec![visibility_buffer_raster_shadow_view_layout],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::VERTEX,
range: 0..4,
}],
vertex: VertexState {
shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE,
shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()],
entry_point: "vertex".into(),
buffers: vec![],
},
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: FrontFace::Ccw,
cull_mode: Some(Face::Back),
unclipped_depth: true,
polygon_mode: PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: MultisampleState::default(),
fragment: Some(FragmentState {
shader: MESHLET_VISIBILITY_BUFFER_HARDWARE_RASTER_SHADER_HANDLE,
shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::R8Uint,
blend: None,
write_mask: ColorWrites::empty(),
})],
}),
zero_initialize_workgroup_memory: false,
}),
resolve_depth: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor {
label: Some("meshlet_resolve_depth_pipeline".into()),
layout: vec![resolve_depth_layout],
push_constant_ranges: vec![],
vertex: fullscreen_shader_vertex_state(),
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: CORE_3D_DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: CompareFunction::Always,
stencil: StencilState::default(),
bias: DepthBiasState::default(),
}),
multisample: MultisampleState::default(),
fragment: Some(FragmentState {
shader: MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE,
shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()],
entry_point: "resolve_depth".into(),
targets: vec![],
}),
zero_initialize_workgroup_memory: false,
}),
resolve_depth_shadow_view: pipeline_cache.queue_render_pipeline(
RenderPipelineDescriptor {
label: Some("meshlet_resolve_depth_pipeline".into()),
layout: vec![resolve_depth_shadow_view_layout],
push_constant_ranges: vec![],
vertex: fullscreen_shader_vertex_state(),
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: CORE_3D_DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: CompareFunction::Always,
stencil: StencilState::default(),
bias: DepthBiasState::default(),
}),
multisample: MultisampleState::default(),
fragment: Some(FragmentState {
shader: MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE,
shader_defs: vec![],
entry_point: "resolve_depth".into(),
targets: vec![],
}),
zero_initialize_workgroup_memory: false,
},
),
resolve_material_depth: pipeline_cache.queue_render_pipeline(
RenderPipelineDescriptor {
label: Some("meshlet_resolve_material_depth_pipeline".into()),
layout: vec![resolve_material_depth_layout],
push_constant_ranges: vec![],
vertex: fullscreen_shader_vertex_state(),
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Depth16Unorm,
depth_write_enabled: true,
depth_compare: CompareFunction::Always,
stencil: StencilState::default(),
bias: DepthBiasState::default(),
}),
multisample: MultisampleState::default(),
fragment: Some(FragmentState {
shader: MESHLET_RESOLVE_RENDER_TARGETS_SHADER_HANDLE,
shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()],
entry_point: "resolve_material_depth".into(),
targets: vec![],
}),
zero_initialize_workgroup_memory: false,
},
),
remap_1d_to_2d_dispatch: remap_1d_to_2d_dispatch_layout.map(|layout| {
pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
label: Some("meshlet_remap_1d_to_2d_dispatch_pipeline".into()),
layout: vec![layout],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::COMPUTE,
range: 0..4,
}],
shader: MESHLET_REMAP_1D_TO_2D_DISPATCH_SHADER_HANDLE,
shader_defs: vec![],
entry_point: "remap_dispatch".into(),
zero_initialize_workgroup_memory: false,
})
}),
}
}
}
impl MeshletPipelines {
pub fn get(
world: &World,
) -> Option<(
&ComputePipeline,
&ComputePipeline,
&ComputePipeline,
&ComputePipeline,
&ComputePipeline,
&ComputePipeline,
&ComputePipeline,
&ComputePipeline,
&ComputePipeline,
&ComputePipeline,
&ComputePipeline,
&RenderPipeline,
&RenderPipeline,
&RenderPipeline,
&RenderPipeline,
&RenderPipeline,
&RenderPipeline,
Option<&ComputePipeline>,
)> {
let pipeline_cache = world.get_resource::<PipelineCache>()?;
let pipeline = world.get_resource::<Self>()?;
Some((
pipeline_cache.get_compute_pipeline(pipeline.fill_cluster_buffers)?,
pipeline_cache.get_compute_pipeline(pipeline.clear_visibility_buffer)?,
pipeline_cache.get_compute_pipeline(pipeline.clear_visibility_buffer_shadow_view)?,
pipeline_cache.get_compute_pipeline(pipeline.cull_first)?,
pipeline_cache.get_compute_pipeline(pipeline.cull_second)?,
pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_first)?,
pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_second)?,
pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_first_shadow_view)?,
pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_second_shadow_view)?,
pipeline_cache.get_compute_pipeline(pipeline.visibility_buffer_software_raster)?,
pipeline_cache
.get_compute_pipeline(pipeline.visibility_buffer_software_raster_shadow_view)?,
pipeline_cache.get_render_pipeline(pipeline.visibility_buffer_hardware_raster)?,
pipeline_cache
.get_render_pipeline(pipeline.visibility_buffer_hardware_raster_shadow_view)?,
pipeline_cache.get_render_pipeline(
pipeline.visibility_buffer_hardware_raster_shadow_view_unclipped,
)?,
pipeline_cache.get_render_pipeline(pipeline.resolve_depth)?,
pipeline_cache.get_render_pipeline(pipeline.resolve_depth_shadow_view)?,
pipeline_cache.get_render_pipeline(pipeline.resolve_material_depth)?,
match pipeline.remap_1d_to_2d_dispatch {
Some(id) => Some(pipeline_cache.get_compute_pipeline(id)?),
None => None,
},
))
}
}

View File

@@ -0,0 +1,23 @@
/// Remaps an indirect 1d to 2d dispatch for devices with low dispatch size limit.
struct DispatchIndirectArgs {
x: u32,
y: u32,
z: u32,
}
@group(0) @binding(0) var<storage, read_write> meshlet_software_raster_indirect_args: DispatchIndirectArgs;
@group(0) @binding(1) var<storage, read_write> meshlet_software_raster_cluster_count: u32;
var<push_constant> max_compute_workgroups_per_dimension: u32;
@compute
@workgroup_size(1, 1, 1)
fn remap_dispatch() {
meshlet_software_raster_cluster_count = meshlet_software_raster_indirect_args.x;
if meshlet_software_raster_cluster_count > max_compute_workgroups_per_dimension {
let n = u32(ceil(sqrt(f32(meshlet_software_raster_cluster_count))));
meshlet_software_raster_indirect_args.x = n;
meshlet_software_raster_indirect_args.y = n;
}
}

View File

@@ -0,0 +1,40 @@
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
@group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d<r64uint, read>;
#else
@group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d<r32uint, read>;
#endif
@group(0) @binding(1) var<storage, read> meshlet_cluster_instance_ids: array<u32>; // Per cluster
@group(0) @binding(2) var<storage, read> meshlet_instance_material_ids: array<u32>; // Per entity instance
/// This pass writes out the depth texture.
@fragment
fn resolve_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f32 {
let visibility = textureLoad(meshlet_visibility_buffer, vec2<u32>(in.position.xy)).r;
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
let depth = u32(visibility >> 32u);
#else
let depth = visibility;
#endif
if depth == 0u { discard; }
return bitcast<f32>(depth);
}
/// This pass writes out the material depth texture.
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
@fragment
fn resolve_material_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f32 {
let visibility = textureLoad(meshlet_visibility_buffer, vec2<u32>(in.position.xy)).r;
let depth = visibility >> 32u;
if depth == 0lu { discard; }
let cluster_id = u32(visibility) >> 7u;
let instance_id = meshlet_cluster_instance_ids[cluster_id];
let material_id = meshlet_instance_material_ids[instance_id];
return f32(material_id) / 65535.0;
}
#endif

View File

@@ -0,0 +1,862 @@
use super::{instance_manager::InstanceManager, meshlet_mesh_manager::MeshletMeshManager};
use crate::ShadowView;
use alloc::sync::Arc;
use bevy_core_pipeline::{
core_3d::Camera3d,
experimental::mip_generation::{self, ViewDepthPyramid},
prepass::{PreviousViewData, PreviousViewUniforms},
};
use bevy_ecs::{
component::Component,
entity::{Entity, EntityHashMap},
query::AnyOf,
resource::Resource,
system::{Commands, Query, Res, ResMut},
};
use bevy_math::{UVec2, Vec4Swizzles};
use bevy_render::{
render_resource::*,
renderer::{RenderDevice, RenderQueue},
texture::{CachedTexture, TextureCache},
view::{ExtractedView, RenderLayers, ViewUniform, ViewUniforms},
};
use binding_types::*;
use core::{iter, sync::atomic::AtomicBool};
use encase::internal::WriteInto;
/// Manages per-view and per-cluster GPU resources for [`super::MeshletPlugin`].
#[derive(Resource)]
pub struct ResourceManager {
/// Intermediate buffer of cluster IDs for use with rasterizing the visibility buffer
visibility_buffer_raster_clusters: Buffer,
/// Intermediate buffer of count of clusters to software rasterize
software_raster_cluster_count: Buffer,
/// Rightmost slot index of [`Self::visibility_buffer_raster_clusters`]
raster_cluster_rightmost_slot: u32,
/// Per-cluster instance ID
cluster_instance_ids: Option<Buffer>,
/// Per-cluster meshlet ID
cluster_meshlet_ids: Option<Buffer>,
/// Per-cluster bitmask of whether or not it's a candidate for the second raster pass
second_pass_candidates_buffer: Option<Buffer>,
/// Sampler for a depth pyramid
depth_pyramid_sampler: Sampler,
/// Dummy texture view for binding depth pyramids with less than the maximum amount of mips
depth_pyramid_dummy_texture: TextureView,
// TODO
previous_depth_pyramids: EntityHashMap<TextureView>,
// Bind group layouts
pub fill_cluster_buffers_bind_group_layout: BindGroupLayout,
pub clear_visibility_buffer_bind_group_layout: BindGroupLayout,
pub clear_visibility_buffer_shadow_view_bind_group_layout: BindGroupLayout,
pub culling_bind_group_layout: BindGroupLayout,
pub visibility_buffer_raster_bind_group_layout: BindGroupLayout,
pub visibility_buffer_raster_shadow_view_bind_group_layout: BindGroupLayout,
pub downsample_depth_bind_group_layout: BindGroupLayout,
pub downsample_depth_shadow_view_bind_group_layout: BindGroupLayout,
pub resolve_depth_bind_group_layout: BindGroupLayout,
pub resolve_depth_shadow_view_bind_group_layout: BindGroupLayout,
pub resolve_material_depth_bind_group_layout: BindGroupLayout,
pub material_shade_bind_group_layout: BindGroupLayout,
pub remap_1d_to_2d_dispatch_bind_group_layout: Option<BindGroupLayout>,
}
impl ResourceManager {
pub fn new(cluster_buffer_slots: u32, render_device: &RenderDevice) -> Self {
let needs_dispatch_remap =
cluster_buffer_slots > render_device.limits().max_compute_workgroups_per_dimension;
Self {
visibility_buffer_raster_clusters: render_device.create_buffer(&BufferDescriptor {
label: Some("meshlet_visibility_buffer_raster_clusters"),
size: cluster_buffer_slots as u64 * size_of::<u32>() as u64,
usage: BufferUsages::STORAGE,
mapped_at_creation: false,
}),
software_raster_cluster_count: render_device.create_buffer(&BufferDescriptor {
label: Some("meshlet_software_raster_cluster_count"),
size: size_of::<u32>() as u64,
usage: BufferUsages::STORAGE,
mapped_at_creation: false,
}),
raster_cluster_rightmost_slot: cluster_buffer_slots - 1,
cluster_instance_ids: None,
cluster_meshlet_ids: None,
second_pass_candidates_buffer: None,
depth_pyramid_sampler: render_device.create_sampler(&SamplerDescriptor {
label: Some("meshlet_depth_pyramid_sampler"),
..SamplerDescriptor::default()
}),
depth_pyramid_dummy_texture: mip_generation::create_depth_pyramid_dummy_texture(
render_device,
"meshlet_depth_pyramid_dummy_texture",
"meshlet_depth_pyramid_dummy_texture_view",
),
previous_depth_pyramids: EntityHashMap::default(),
// TODO: Buffer min sizes
fill_cluster_buffers_bind_group_layout: render_device.create_bind_group_layout(
"meshlet_fill_cluster_buffers_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_sized(false, None),
storage_buffer_sized(false, None),
storage_buffer_sized(false, None),
),
),
),
clear_visibility_buffer_bind_group_layout: render_device.create_bind_group_layout(
"meshlet_clear_visibility_buffer_bind_group_layout",
&BindGroupLayoutEntries::single(
ShaderStages::COMPUTE,
texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::WriteOnly),
),
),
clear_visibility_buffer_shadow_view_bind_group_layout: render_device
.create_bind_group_layout(
"meshlet_clear_visibility_buffer_shadow_view_bind_group_layout",
&BindGroupLayoutEntries::single(
ShaderStages::COMPUTE,
texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly),
),
),
culling_bind_group_layout: render_device.create_bind_group_layout(
"meshlet_culling_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_sized(false, None),
storage_buffer_sized(false, None),
storage_buffer_sized(false, None),
storage_buffer_sized(false, None),
texture_2d(TextureSampleType::Float { filterable: false }),
uniform_buffer::<ViewUniform>(true),
uniform_buffer::<PreviousViewData>(true),
),
),
),
downsample_depth_bind_group_layout: render_device.create_bind_group_layout(
"meshlet_downsample_depth_bind_group_layout",
&BindGroupLayoutEntries::sequential(ShaderStages::COMPUTE, {
let write_only_r32float = || {
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly)
};
(
texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::ReadOnly),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
texture_storage_2d(
TextureFormat::R32Float,
StorageTextureAccess::ReadWrite,
),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
sampler(SamplerBindingType::NonFiltering),
)
}),
),
downsample_depth_shadow_view_bind_group_layout: render_device.create_bind_group_layout(
"meshlet_downsample_depth_shadow_view_bind_group_layout",
&BindGroupLayoutEntries::sequential(ShaderStages::COMPUTE, {
let write_only_r32float = || {
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly)
};
(
texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::ReadOnly),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
texture_storage_2d(
TextureFormat::R32Float,
StorageTextureAccess::ReadWrite,
),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
write_only_r32float(),
sampler(SamplerBindingType::NonFiltering),
)
}),
),
visibility_buffer_raster_bind_group_layout: render_device.create_bind_group_layout(
"meshlet_visibility_buffer_raster_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::all(),
(
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::Atomic),
uniform_buffer::<ViewUniform>(true),
),
),
),
visibility_buffer_raster_shadow_view_bind_group_layout: render_device
.create_bind_group_layout(
"meshlet_visibility_buffer_raster_shadow_view_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::all(),
(
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
texture_storage_2d(
TextureFormat::R32Uint,
StorageTextureAccess::Atomic,
),
uniform_buffer::<ViewUniform>(true),
),
),
),
resolve_depth_bind_group_layout: render_device.create_bind_group_layout(
"meshlet_resolve_depth_bind_group_layout",
&BindGroupLayoutEntries::single(
ShaderStages::FRAGMENT,
texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::ReadOnly),
),
),
resolve_depth_shadow_view_bind_group_layout: render_device.create_bind_group_layout(
"meshlet_resolve_depth_shadow_view_bind_group_layout",
&BindGroupLayoutEntries::single(
ShaderStages::FRAGMENT,
texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::ReadOnly),
),
),
resolve_material_depth_bind_group_layout: render_device.create_bind_group_layout(
"meshlet_resolve_material_depth_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::ReadOnly),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
),
),
),
material_shade_bind_group_layout: render_device.create_bind_group_layout(
"meshlet_mesh_material_shade_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::ReadOnly),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
storage_buffer_read_only_sized(false, None),
),
),
),
remap_1d_to_2d_dispatch_bind_group_layout: needs_dispatch_remap.then(|| {
render_device.create_bind_group_layout(
"meshlet_remap_1d_to_2d_dispatch_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
storage_buffer_sized(false, None),
storage_buffer_sized(false, None),
),
),
)
}),
}
}
}
// ------------ TODO: Everything under here needs to be rewritten and cached ------------
#[derive(Component)]
pub struct MeshletViewResources {
pub scene_instance_count: u32,
pub scene_cluster_count: u32,
pub second_pass_candidates_buffer: Buffer,
instance_visibility: Buffer,
pub dummy_render_target: CachedTexture,
pub visibility_buffer: CachedTexture,
pub visibility_buffer_software_raster_indirect_args_first: Buffer,
pub visibility_buffer_software_raster_indirect_args_second: Buffer,
pub visibility_buffer_hardware_raster_indirect_args_first: Buffer,
pub visibility_buffer_hardware_raster_indirect_args_second: Buffer,
pub depth_pyramid: ViewDepthPyramid,
previous_depth_pyramid: TextureView,
pub material_depth: Option<CachedTexture>,
pub view_size: UVec2,
pub raster_cluster_rightmost_slot: u32,
not_shadow_view: bool,
}
#[derive(Component)]
pub struct MeshletViewBindGroups {
pub first_node: Arc<AtomicBool>,
pub fill_cluster_buffers: BindGroup,
pub clear_visibility_buffer: BindGroup,
pub culling_first: BindGroup,
pub culling_second: BindGroup,
pub downsample_depth: BindGroup,
pub visibility_buffer_raster: BindGroup,
pub resolve_depth: BindGroup,
pub resolve_material_depth: Option<BindGroup>,
pub material_shade: Option<BindGroup>,
pub remap_1d_to_2d_dispatch: Option<(BindGroup, BindGroup)>,
}
// TODO: Try using Queue::write_buffer_with() in queue_meshlet_mesh_upload() to reduce copies
fn upload_storage_buffer<T: ShaderSize + bytemuck::NoUninit>(
buffer: &mut StorageBuffer<Vec<T>>,
render_device: &RenderDevice,
render_queue: &RenderQueue,
) where
Vec<T>: WriteInto,
{
let inner = buffer.buffer();
let capacity = inner.map_or(0, |b| b.size());
let size = buffer.get().size().get() as BufferAddress;
if capacity >= size {
let inner = inner.unwrap();
let bytes = bytemuck::must_cast_slice(buffer.get().as_slice());
render_queue.write_buffer(inner, 0, bytes);
} else {
buffer.write_buffer(render_device, render_queue);
}
}
// TODO: Cache things per-view and skip running this system / optimize this system
pub fn prepare_meshlet_per_frame_resources(
mut resource_manager: ResMut<ResourceManager>,
mut instance_manager: ResMut<InstanceManager>,
views: Query<(
Entity,
&ExtractedView,
Option<&RenderLayers>,
AnyOf<(&Camera3d, &ShadowView)>,
)>,
mut texture_cache: ResMut<TextureCache>,
render_queue: Res<RenderQueue>,
render_device: Res<RenderDevice>,
mut commands: Commands,
) {
if instance_manager.scene_cluster_count == 0 {
return;
}
let instance_manager = instance_manager.as_mut();
// TODO: Move this and the submit to a separate system and remove pub from the fields
instance_manager
.instance_uniforms
.write_buffer(&render_device, &render_queue);
upload_storage_buffer(
&mut instance_manager.instance_material_ids,
&render_device,
&render_queue,
);
upload_storage_buffer(
&mut instance_manager.instance_meshlet_counts,
&render_device,
&render_queue,
);
upload_storage_buffer(
&mut instance_manager.instance_meshlet_slice_starts,
&render_device,
&render_queue,
);
let needed_buffer_size = 4 * instance_manager.scene_cluster_count as u64;
match &mut resource_manager.cluster_instance_ids {
Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(),
slot => {
let buffer = render_device.create_buffer(&BufferDescriptor {
label: Some("meshlet_cluster_instance_ids"),
size: needed_buffer_size,
usage: BufferUsages::STORAGE,
mapped_at_creation: false,
});
*slot = Some(buffer.clone());
buffer
}
};
match &mut resource_manager.cluster_meshlet_ids {
Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(),
slot => {
let buffer = render_device.create_buffer(&BufferDescriptor {
label: Some("meshlet_cluster_meshlet_ids"),
size: needed_buffer_size,
usage: BufferUsages::STORAGE,
mapped_at_creation: false,
});
*slot = Some(buffer.clone());
buffer
}
};
let needed_buffer_size =
instance_manager.scene_cluster_count.div_ceil(u32::BITS) as u64 * size_of::<u32>() as u64;
for (view_entity, view, render_layers, (_, shadow_view)) in &views {
let not_shadow_view = shadow_view.is_none();
let instance_visibility = instance_manager
.view_instance_visibility
.entry(view_entity)
.or_insert_with(|| {
let mut buffer = StorageBuffer::default();
buffer.set_label(Some("meshlet_view_instance_visibility"));
buffer
});
for (instance_index, (_, layers, not_shadow_caster)) in
instance_manager.instances.iter().enumerate()
{
// If either the layers don't match the view's layers or this is a shadow view
// and the instance is not a shadow caster, hide the instance for this view
if !render_layers
.unwrap_or(&RenderLayers::default())
.intersects(layers)
|| (shadow_view.is_some() && *not_shadow_caster)
{
let vec = instance_visibility.get_mut();
let index = instance_index / 32;
let bit = instance_index - index * 32;
if vec.len() <= index {
vec.extend(iter::repeat_n(0, index - vec.len() + 1));
}
vec[index] |= 1 << bit;
}
}
upload_storage_buffer(instance_visibility, &render_device, &render_queue);
let instance_visibility = instance_visibility.buffer().unwrap().clone();
let second_pass_candidates_buffer =
match &mut resource_manager.second_pass_candidates_buffer {
Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(),
slot => {
let buffer = render_device.create_buffer(&BufferDescriptor {
label: Some("meshlet_second_pass_candidates"),
size: needed_buffer_size,
usage: BufferUsages::STORAGE | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
*slot = Some(buffer.clone());
buffer
}
};
// TODO: Remove this once wgpu allows render passes with no attachments
let dummy_render_target = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("meshlet_dummy_render_target"),
size: Extent3d {
width: view.viewport.z,
height: view.viewport.w,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::R8Uint,
usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
},
);
let visibility_buffer = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("meshlet_visibility_buffer"),
size: Extent3d {
width: view.viewport.z,
height: view.viewport.w,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: if not_shadow_view {
TextureFormat::R64Uint
} else {
TextureFormat::R32Uint
},
usage: TextureUsages::STORAGE_ATOMIC | TextureUsages::STORAGE_BINDING,
view_formats: &[],
},
);
let visibility_buffer_software_raster_indirect_args_first = render_device
.create_buffer_with_data(&BufferInitDescriptor {
label: Some("meshlet_visibility_buffer_software_raster_indirect_args_first"),
contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(),
usage: BufferUsages::STORAGE | BufferUsages::INDIRECT,
});
let visibility_buffer_software_raster_indirect_args_second = render_device
.create_buffer_with_data(&BufferInitDescriptor {
label: Some("visibility_buffer_software_raster_indirect_args_second"),
contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(),
usage: BufferUsages::STORAGE | BufferUsages::INDIRECT,
});
let visibility_buffer_hardware_raster_indirect_args_first = render_device
.create_buffer_with_data(&BufferInitDescriptor {
label: Some("meshlet_visibility_buffer_hardware_raster_indirect_args_first"),
contents: DrawIndirectArgs {
vertex_count: 128 * 3,
instance_count: 0,
first_vertex: 0,
first_instance: 0,
}
.as_bytes(),
usage: BufferUsages::STORAGE | BufferUsages::INDIRECT,
});
let visibility_buffer_hardware_raster_indirect_args_second = render_device
.create_buffer_with_data(&BufferInitDescriptor {
label: Some("visibility_buffer_hardware_raster_indirect_args_second"),
contents: DrawIndirectArgs {
vertex_count: 128 * 3,
instance_count: 0,
first_vertex: 0,
first_instance: 0,
}
.as_bytes(),
usage: BufferUsages::STORAGE | BufferUsages::INDIRECT,
});
let depth_pyramid = ViewDepthPyramid::new(
&render_device,
&mut texture_cache,
&resource_manager.depth_pyramid_dummy_texture,
view.viewport.zw(),
"meshlet_depth_pyramid",
"meshlet_depth_pyramid_texture_view",
);
let previous_depth_pyramid =
match resource_manager.previous_depth_pyramids.get(&view_entity) {
Some(texture_view) => texture_view.clone(),
None => depth_pyramid.all_mips.clone(),
};
resource_manager
.previous_depth_pyramids
.insert(view_entity, depth_pyramid.all_mips.clone());
let material_depth = TextureDescriptor {
label: Some("meshlet_material_depth"),
size: Extent3d {
width: view.viewport.z,
height: view.viewport.w,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Depth16Unorm,
usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
};
commands.entity(view_entity).insert(MeshletViewResources {
scene_instance_count: instance_manager.scene_instance_count,
scene_cluster_count: instance_manager.scene_cluster_count,
second_pass_candidates_buffer,
instance_visibility,
dummy_render_target,
visibility_buffer,
visibility_buffer_software_raster_indirect_args_first,
visibility_buffer_software_raster_indirect_args_second,
visibility_buffer_hardware_raster_indirect_args_first,
visibility_buffer_hardware_raster_indirect_args_second,
depth_pyramid,
previous_depth_pyramid,
material_depth: not_shadow_view
.then(|| texture_cache.get(&render_device, material_depth)),
view_size: view.viewport.zw(),
raster_cluster_rightmost_slot: resource_manager.raster_cluster_rightmost_slot,
not_shadow_view,
});
}
}
pub fn prepare_meshlet_view_bind_groups(
meshlet_mesh_manager: Res<MeshletMeshManager>,
resource_manager: Res<ResourceManager>,
instance_manager: Res<InstanceManager>,
views: Query<(Entity, &MeshletViewResources)>,
view_uniforms: Res<ViewUniforms>,
previous_view_uniforms: Res<PreviousViewUniforms>,
render_device: Res<RenderDevice>,
mut commands: Commands,
) {
let (
Some(cluster_instance_ids),
Some(cluster_meshlet_ids),
Some(view_uniforms),
Some(previous_view_uniforms),
) = (
resource_manager.cluster_instance_ids.as_ref(),
resource_manager.cluster_meshlet_ids.as_ref(),
view_uniforms.uniforms.binding(),
previous_view_uniforms.uniforms.binding(),
)
else {
return;
};
let first_node = Arc::new(AtomicBool::new(true));
let fill_cluster_buffers_global_cluster_count =
render_device.create_buffer(&BufferDescriptor {
label: Some("meshlet_fill_cluster_buffers_global_cluster_count"),
size: 4,
usage: BufferUsages::STORAGE,
mapped_at_creation: false,
});
// TODO: Some of these bind groups can be reused across multiple views
for (view_entity, view_resources) in &views {
let entries = BindGroupEntries::sequential((
instance_manager.instance_meshlet_counts.binding().unwrap(),
instance_manager
.instance_meshlet_slice_starts
.binding()
.unwrap(),
cluster_instance_ids.as_entire_binding(),
cluster_meshlet_ids.as_entire_binding(),
fill_cluster_buffers_global_cluster_count.as_entire_binding(),
));
let fill_cluster_buffers = render_device.create_bind_group(
"meshlet_fill_cluster_buffers",
&resource_manager.fill_cluster_buffers_bind_group_layout,
&entries,
);
let clear_visibility_buffer = render_device.create_bind_group(
"meshlet_clear_visibility_buffer_bind_group",
if view_resources.not_shadow_view {
&resource_manager.clear_visibility_buffer_bind_group_layout
} else {
&resource_manager.clear_visibility_buffer_shadow_view_bind_group_layout
},
&BindGroupEntries::single(&view_resources.visibility_buffer.default_view),
);
let entries = BindGroupEntries::sequential((
cluster_meshlet_ids.as_entire_binding(),
meshlet_mesh_manager.meshlet_bounding_spheres.binding(),
meshlet_mesh_manager.meshlet_simplification_errors.binding(),
cluster_instance_ids.as_entire_binding(),
instance_manager.instance_uniforms.binding().unwrap(),
view_resources.instance_visibility.as_entire_binding(),
view_resources
.second_pass_candidates_buffer
.as_entire_binding(),
view_resources
.visibility_buffer_software_raster_indirect_args_first
.as_entire_binding(),
view_resources
.visibility_buffer_hardware_raster_indirect_args_first
.as_entire_binding(),
resource_manager
.visibility_buffer_raster_clusters
.as_entire_binding(),
&view_resources.previous_depth_pyramid,
view_uniforms.clone(),
previous_view_uniforms.clone(),
));
let culling_first = render_device.create_bind_group(
"meshlet_culling_first_bind_group",
&resource_manager.culling_bind_group_layout,
&entries,
);
let entries = BindGroupEntries::sequential((
cluster_meshlet_ids.as_entire_binding(),
meshlet_mesh_manager.meshlet_bounding_spheres.binding(),
meshlet_mesh_manager.meshlet_simplification_errors.binding(),
cluster_instance_ids.as_entire_binding(),
instance_manager.instance_uniforms.binding().unwrap(),
view_resources.instance_visibility.as_entire_binding(),
view_resources
.second_pass_candidates_buffer
.as_entire_binding(),
view_resources
.visibility_buffer_software_raster_indirect_args_second
.as_entire_binding(),
view_resources
.visibility_buffer_hardware_raster_indirect_args_second
.as_entire_binding(),
resource_manager
.visibility_buffer_raster_clusters
.as_entire_binding(),
&view_resources.depth_pyramid.all_mips,
view_uniforms.clone(),
previous_view_uniforms.clone(),
));
let culling_second = render_device.create_bind_group(
"meshlet_culling_second_bind_group",
&resource_manager.culling_bind_group_layout,
&entries,
);
let downsample_depth = view_resources.depth_pyramid.create_bind_group(
&render_device,
"meshlet_downsample_depth_bind_group",
if view_resources.not_shadow_view {
&resource_manager.downsample_depth_bind_group_layout
} else {
&resource_manager.downsample_depth_shadow_view_bind_group_layout
},
&view_resources.visibility_buffer.default_view,
&resource_manager.depth_pyramid_sampler,
);
let entries = BindGroupEntries::sequential((
cluster_meshlet_ids.as_entire_binding(),
meshlet_mesh_manager.meshlets.binding(),
meshlet_mesh_manager.indices.binding(),
meshlet_mesh_manager.vertex_positions.binding(),
cluster_instance_ids.as_entire_binding(),
instance_manager.instance_uniforms.binding().unwrap(),
resource_manager
.visibility_buffer_raster_clusters
.as_entire_binding(),
resource_manager
.software_raster_cluster_count
.as_entire_binding(),
&view_resources.visibility_buffer.default_view,
view_uniforms.clone(),
));
let visibility_buffer_raster = render_device.create_bind_group(
"meshlet_visibility_raster_buffer_bind_group",
if view_resources.not_shadow_view {
&resource_manager.visibility_buffer_raster_bind_group_layout
} else {
&resource_manager.visibility_buffer_raster_shadow_view_bind_group_layout
},
&entries,
);
let resolve_depth = render_device.create_bind_group(
"meshlet_resolve_depth_bind_group",
if view_resources.not_shadow_view {
&resource_manager.resolve_depth_bind_group_layout
} else {
&resource_manager.resolve_depth_shadow_view_bind_group_layout
},
&BindGroupEntries::single(&view_resources.visibility_buffer.default_view),
);
let resolve_material_depth = view_resources.material_depth.as_ref().map(|_| {
let entries = BindGroupEntries::sequential((
&view_resources.visibility_buffer.default_view,
cluster_instance_ids.as_entire_binding(),
instance_manager.instance_material_ids.binding().unwrap(),
));
render_device.create_bind_group(
"meshlet_resolve_material_depth_bind_group",
&resource_manager.resolve_material_depth_bind_group_layout,
&entries,
)
});
let material_shade = view_resources.material_depth.as_ref().map(|_| {
let entries = BindGroupEntries::sequential((
&view_resources.visibility_buffer.default_view,
cluster_meshlet_ids.as_entire_binding(),
meshlet_mesh_manager.meshlets.binding(),
meshlet_mesh_manager.indices.binding(),
meshlet_mesh_manager.vertex_positions.binding(),
meshlet_mesh_manager.vertex_normals.binding(),
meshlet_mesh_manager.vertex_uvs.binding(),
cluster_instance_ids.as_entire_binding(),
instance_manager.instance_uniforms.binding().unwrap(),
));
render_device.create_bind_group(
"meshlet_mesh_material_shade_bind_group",
&resource_manager.material_shade_bind_group_layout,
&entries,
)
});
let remap_1d_to_2d_dispatch = resource_manager
.remap_1d_to_2d_dispatch_bind_group_layout
.as_ref()
.map(|layout| {
(
render_device.create_bind_group(
"meshlet_remap_1d_to_2d_dispatch_first_bind_group",
layout,
&BindGroupEntries::sequential((
view_resources
.visibility_buffer_software_raster_indirect_args_first
.as_entire_binding(),
resource_manager
.software_raster_cluster_count
.as_entire_binding(),
)),
),
render_device.create_bind_group(
"meshlet_remap_1d_to_2d_dispatch_second_bind_group",
layout,
&BindGroupEntries::sequential((
view_resources
.visibility_buffer_software_raster_indirect_args_second
.as_entire_binding(),
resource_manager
.software_raster_cluster_count
.as_entire_binding(),
)),
),
)
});
commands.entity(view_entity).insert(MeshletViewBindGroups {
first_node: Arc::clone(&first_node),
fill_cluster_buffers,
clear_visibility_buffer,
culling_first,
culling_second,
downsample_depth,
visibility_buffer_raster,
resolve_depth,
resolve_material_depth,
material_shade,
remap_1d_to_2d_dispatch,
});
}
}

View File

@@ -0,0 +1,78 @@
#import bevy_pbr::{
meshlet_bindings::{
meshlet_cluster_meshlet_ids,
meshlets,
meshlet_cluster_instance_ids,
meshlet_instance_uniforms,
meshlet_raster_clusters,
meshlet_visibility_buffer,
view,
get_meshlet_triangle_count,
get_meshlet_vertex_id,
get_meshlet_vertex_position,
},
mesh_functions::mesh_position_local_to_world,
}
#import bevy_render::maths::affine3_to_square
var<push_constant> meshlet_raster_cluster_rightmost_slot: u32;
/// Vertex/fragment shader for rasterizing large clusters into a visibility buffer.
struct VertexOutput {
@builtin(position) position: vec4<f32>,
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
@location(0) @interpolate(flat) packed_ids: u32,
#endif
}
@vertex
fn vertex(@builtin(instance_index) instance_index: u32, @builtin(vertex_index) vertex_index: u32) -> VertexOutput {
let cluster_id = meshlet_raster_clusters[meshlet_raster_cluster_rightmost_slot - instance_index];
let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id];
var meshlet = meshlets[meshlet_id];
let triangle_id = vertex_index / 3u;
if triangle_id >= get_meshlet_triangle_count(&meshlet) { return dummy_vertex(); }
let index_id = (triangle_id * 3u) + (vertex_index % 3u);
let vertex_id = get_meshlet_vertex_id(meshlet.start_index_id + index_id);
let instance_id = meshlet_cluster_instance_ids[cluster_id];
let instance_uniform = meshlet_instance_uniforms[instance_id];
let vertex_position = get_meshlet_vertex_position(&meshlet, vertex_id);
let world_from_local = affine3_to_square(instance_uniform.world_from_local);
let world_position = mesh_position_local_to_world(world_from_local, vec4(vertex_position, 1.0));
let clip_position = view.clip_from_world * vec4(world_position.xyz, 1.0);
return VertexOutput(
clip_position,
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
(cluster_id << 7u) | triangle_id,
#endif
);
}
@fragment
fn fragment(vertex_output: VertexOutput) {
let depth = bitcast<u32>(vertex_output.position.z);
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
let visibility = (u64(depth) << 32u) | u64(vertex_output.packed_ids);
#else
let visibility = depth;
#endif
textureAtomicMax(meshlet_visibility_buffer, vec2<u32>(vertex_output.position.xy), visibility);
}
fn dummy_vertex() -> VertexOutput {
return VertexOutput(
vec4(divide(0.0, 0.0)), // NaN vertex position
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
0u,
#endif
);
}
// Naga doesn't allow divide by zero literals, but this lets us work around it
fn divide(a: f32, b: f32) -> f32 {
return a / b;
}

View File

@@ -0,0 +1,567 @@
use super::{
pipelines::MeshletPipelines,
resource_manager::{MeshletViewBindGroups, MeshletViewResources},
};
use crate::{LightEntity, ShadowView, ViewLightEntities};
use bevy_color::LinearRgba;
use bevy_core_pipeline::prepass::PreviousViewUniformOffset;
use bevy_ecs::{
query::QueryState,
world::{FromWorld, World},
};
use bevy_math::{ops, UVec2};
use bevy_render::{
camera::ExtractedCamera,
render_graph::{Node, NodeRunError, RenderGraphContext},
render_resource::*,
renderer::RenderContext,
view::{ViewDepthTexture, ViewUniformOffset},
};
use core::sync::atomic::Ordering;
/// Rasterize meshlets into a depth buffer, and optional visibility buffer + material depth buffer for shading passes.
pub struct MeshletVisibilityBufferRasterPassNode {
main_view_query: QueryState<(
&'static ExtractedCamera,
&'static ViewDepthTexture,
&'static ViewUniformOffset,
&'static PreviousViewUniformOffset,
&'static MeshletViewBindGroups,
&'static MeshletViewResources,
&'static ViewLightEntities,
)>,
view_light_query: QueryState<(
&'static ShadowView,
&'static LightEntity,
&'static ViewUniformOffset,
&'static PreviousViewUniformOffset,
&'static MeshletViewBindGroups,
&'static MeshletViewResources,
)>,
}
impl FromWorld for MeshletVisibilityBufferRasterPassNode {
fn from_world(world: &mut World) -> Self {
Self {
main_view_query: QueryState::new(world),
view_light_query: QueryState::new(world),
}
}
}
impl Node for MeshletVisibilityBufferRasterPassNode {
fn update(&mut self, world: &mut World) {
self.main_view_query.update_archetypes(world);
self.view_light_query.update_archetypes(world);
}
// TODO: Reuse compute/render passes between logical passes where possible, as they're expensive
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
world: &World,
) -> Result<(), NodeRunError> {
let Ok((
camera,
view_depth,
view_offset,
previous_view_offset,
meshlet_view_bind_groups,
meshlet_view_resources,
lights,
)) = self.main_view_query.get_manual(world, graph.view_entity())
else {
return Ok(());
};
let Some((
fill_cluster_buffers_pipeline,
clear_visibility_buffer_pipeline,
clear_visibility_buffer_shadow_view_pipeline,
culling_first_pipeline,
culling_second_pipeline,
downsample_depth_first_pipeline,
downsample_depth_second_pipeline,
downsample_depth_first_shadow_view_pipeline,
downsample_depth_second_shadow_view_pipeline,
visibility_buffer_software_raster_pipeline,
visibility_buffer_software_raster_shadow_view_pipeline,
visibility_buffer_hardware_raster_pipeline,
visibility_buffer_hardware_raster_shadow_view_pipeline,
visibility_buffer_hardware_raster_shadow_view_unclipped_pipeline,
resolve_depth_pipeline,
resolve_depth_shadow_view_pipeline,
resolve_material_depth_pipeline,
remap_1d_to_2d_dispatch_pipeline,
)) = MeshletPipelines::get(world)
else {
return Ok(());
};
let first_node = meshlet_view_bind_groups
.first_node
.fetch_and(false, Ordering::SeqCst);
let div_ceil = meshlet_view_resources.scene_cluster_count.div_ceil(128);
let thread_per_cluster_workgroups = ops::cbrt(div_ceil as f32).ceil() as u32;
render_context
.command_encoder()
.push_debug_group("meshlet_visibility_buffer_raster");
if first_node {
fill_cluster_buffers_pass(
render_context,
&meshlet_view_bind_groups.fill_cluster_buffers,
fill_cluster_buffers_pipeline,
meshlet_view_resources.scene_instance_count,
);
}
clear_visibility_buffer_pass(
render_context,
&meshlet_view_bind_groups.clear_visibility_buffer,
clear_visibility_buffer_pipeline,
meshlet_view_resources.view_size,
);
render_context.command_encoder().clear_buffer(
&meshlet_view_resources.second_pass_candidates_buffer,
0,
None,
);
cull_pass(
"culling_first",
render_context,
&meshlet_view_bind_groups.culling_first,
view_offset,
previous_view_offset,
culling_first_pipeline,
thread_per_cluster_workgroups,
meshlet_view_resources.scene_cluster_count,
meshlet_view_resources.raster_cluster_rightmost_slot,
meshlet_view_bind_groups
.remap_1d_to_2d_dispatch
.as_ref()
.map(|(bg1, _)| bg1),
remap_1d_to_2d_dispatch_pipeline,
);
raster_pass(
true,
render_context,
&meshlet_view_resources.visibility_buffer_software_raster_indirect_args_first,
&meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_first,
&meshlet_view_resources.dummy_render_target.default_view,
meshlet_view_bind_groups,
view_offset,
visibility_buffer_software_raster_pipeline,
visibility_buffer_hardware_raster_pipeline,
Some(camera),
meshlet_view_resources.raster_cluster_rightmost_slot,
);
meshlet_view_resources.depth_pyramid.downsample_depth(
"downsample_depth",
render_context,
meshlet_view_resources.view_size,
&meshlet_view_bind_groups.downsample_depth,
downsample_depth_first_pipeline,
downsample_depth_second_pipeline,
);
cull_pass(
"culling_second",
render_context,
&meshlet_view_bind_groups.culling_second,
view_offset,
previous_view_offset,
culling_second_pipeline,
thread_per_cluster_workgroups,
meshlet_view_resources.scene_cluster_count,
meshlet_view_resources.raster_cluster_rightmost_slot,
meshlet_view_bind_groups
.remap_1d_to_2d_dispatch
.as_ref()
.map(|(_, bg2)| bg2),
remap_1d_to_2d_dispatch_pipeline,
);
raster_pass(
false,
render_context,
&meshlet_view_resources.visibility_buffer_software_raster_indirect_args_second,
&meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_second,
&meshlet_view_resources.dummy_render_target.default_view,
meshlet_view_bind_groups,
view_offset,
visibility_buffer_software_raster_pipeline,
visibility_buffer_hardware_raster_pipeline,
Some(camera),
meshlet_view_resources.raster_cluster_rightmost_slot,
);
resolve_depth(
render_context,
view_depth.get_attachment(StoreOp::Store),
meshlet_view_bind_groups,
resolve_depth_pipeline,
camera,
);
resolve_material_depth(
render_context,
meshlet_view_resources,
meshlet_view_bind_groups,
resolve_material_depth_pipeline,
camera,
);
meshlet_view_resources.depth_pyramid.downsample_depth(
"downsample_depth",
render_context,
meshlet_view_resources.view_size,
&meshlet_view_bind_groups.downsample_depth,
downsample_depth_first_pipeline,
downsample_depth_second_pipeline,
);
render_context.command_encoder().pop_debug_group();
for light_entity in &lights.lights {
let Ok((
shadow_view,
light_type,
view_offset,
previous_view_offset,
meshlet_view_bind_groups,
meshlet_view_resources,
)) = self.view_light_query.get_manual(world, *light_entity)
else {
continue;
};
let shadow_visibility_buffer_hardware_raster_pipeline =
if let LightEntity::Directional { .. } = light_type {
visibility_buffer_hardware_raster_shadow_view_unclipped_pipeline
} else {
visibility_buffer_hardware_raster_shadow_view_pipeline
};
render_context.command_encoder().push_debug_group(&format!(
"meshlet_visibility_buffer_raster: {}",
shadow_view.pass_name
));
clear_visibility_buffer_pass(
render_context,
&meshlet_view_bind_groups.clear_visibility_buffer,
clear_visibility_buffer_shadow_view_pipeline,
meshlet_view_resources.view_size,
);
render_context.command_encoder().clear_buffer(
&meshlet_view_resources.second_pass_candidates_buffer,
0,
None,
);
cull_pass(
"culling_first",
render_context,
&meshlet_view_bind_groups.culling_first,
view_offset,
previous_view_offset,
culling_first_pipeline,
thread_per_cluster_workgroups,
meshlet_view_resources.scene_cluster_count,
meshlet_view_resources.raster_cluster_rightmost_slot,
meshlet_view_bind_groups
.remap_1d_to_2d_dispatch
.as_ref()
.map(|(bg1, _)| bg1),
remap_1d_to_2d_dispatch_pipeline,
);
raster_pass(
true,
render_context,
&meshlet_view_resources.visibility_buffer_software_raster_indirect_args_first,
&meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_first,
&meshlet_view_resources.dummy_render_target.default_view,
meshlet_view_bind_groups,
view_offset,
visibility_buffer_software_raster_shadow_view_pipeline,
shadow_visibility_buffer_hardware_raster_pipeline,
None,
meshlet_view_resources.raster_cluster_rightmost_slot,
);
meshlet_view_resources.depth_pyramid.downsample_depth(
"downsample_depth",
render_context,
meshlet_view_resources.view_size,
&meshlet_view_bind_groups.downsample_depth,
downsample_depth_first_shadow_view_pipeline,
downsample_depth_second_shadow_view_pipeline,
);
cull_pass(
"culling_second",
render_context,
&meshlet_view_bind_groups.culling_second,
view_offset,
previous_view_offset,
culling_second_pipeline,
thread_per_cluster_workgroups,
meshlet_view_resources.scene_cluster_count,
meshlet_view_resources.raster_cluster_rightmost_slot,
meshlet_view_bind_groups
.remap_1d_to_2d_dispatch
.as_ref()
.map(|(_, bg2)| bg2),
remap_1d_to_2d_dispatch_pipeline,
);
raster_pass(
false,
render_context,
&meshlet_view_resources.visibility_buffer_software_raster_indirect_args_second,
&meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args_second,
&meshlet_view_resources.dummy_render_target.default_view,
meshlet_view_bind_groups,
view_offset,
visibility_buffer_software_raster_shadow_view_pipeline,
shadow_visibility_buffer_hardware_raster_pipeline,
None,
meshlet_view_resources.raster_cluster_rightmost_slot,
);
resolve_depth(
render_context,
shadow_view.depth_attachment.get_attachment(StoreOp::Store),
meshlet_view_bind_groups,
resolve_depth_shadow_view_pipeline,
camera,
);
meshlet_view_resources.depth_pyramid.downsample_depth(
"downsample_depth",
render_context,
meshlet_view_resources.view_size,
&meshlet_view_bind_groups.downsample_depth,
downsample_depth_first_shadow_view_pipeline,
downsample_depth_second_shadow_view_pipeline,
);
render_context.command_encoder().pop_debug_group();
}
Ok(())
}
}
fn fill_cluster_buffers_pass(
render_context: &mut RenderContext,
fill_cluster_buffers_bind_group: &BindGroup,
fill_cluster_buffers_pass_pipeline: &ComputePipeline,
scene_instance_count: u32,
) {
let mut fill_cluster_buffers_pass_workgroups_x = scene_instance_count;
let mut fill_cluster_buffers_pass_workgroups_y = 1;
if scene_instance_count
> render_context
.render_device()
.limits()
.max_compute_workgroups_per_dimension
{
fill_cluster_buffers_pass_workgroups_x = (scene_instance_count as f32).sqrt().ceil() as u32;
fill_cluster_buffers_pass_workgroups_y = fill_cluster_buffers_pass_workgroups_x;
}
let command_encoder = render_context.command_encoder();
let mut fill_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor {
label: Some("fill_cluster_buffers"),
timestamp_writes: None,
});
fill_pass.set_pipeline(fill_cluster_buffers_pass_pipeline);
fill_pass.set_push_constants(0, &scene_instance_count.to_le_bytes());
fill_pass.set_bind_group(0, fill_cluster_buffers_bind_group, &[]);
fill_pass.dispatch_workgroups(
fill_cluster_buffers_pass_workgroups_x,
fill_cluster_buffers_pass_workgroups_y,
1,
);
}
// TODO: Replace this with vkCmdClearColorImage once wgpu supports it
fn clear_visibility_buffer_pass(
render_context: &mut RenderContext,
clear_visibility_buffer_bind_group: &BindGroup,
clear_visibility_buffer_pipeline: &ComputePipeline,
view_size: UVec2,
) {
let command_encoder = render_context.command_encoder();
let mut clear_visibility_buffer_pass =
command_encoder.begin_compute_pass(&ComputePassDescriptor {
label: Some("clear_visibility_buffer"),
timestamp_writes: None,
});
clear_visibility_buffer_pass.set_pipeline(clear_visibility_buffer_pipeline);
clear_visibility_buffer_pass.set_push_constants(0, bytemuck::bytes_of(&view_size));
clear_visibility_buffer_pass.set_bind_group(0, clear_visibility_buffer_bind_group, &[]);
clear_visibility_buffer_pass.dispatch_workgroups(
view_size.x.div_ceil(16),
view_size.y.div_ceil(16),
1,
);
}
fn cull_pass(
label: &'static str,
render_context: &mut RenderContext,
culling_bind_group: &BindGroup,
view_offset: &ViewUniformOffset,
previous_view_offset: &PreviousViewUniformOffset,
culling_pipeline: &ComputePipeline,
culling_workgroups: u32,
scene_cluster_count: u32,
raster_cluster_rightmost_slot: u32,
remap_1d_to_2d_dispatch_bind_group: Option<&BindGroup>,
remap_1d_to_2d_dispatch_pipeline: Option<&ComputePipeline>,
) {
let max_compute_workgroups_per_dimension = render_context
.render_device()
.limits()
.max_compute_workgroups_per_dimension;
let command_encoder = render_context.command_encoder();
let mut cull_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor {
label: Some(label),
timestamp_writes: None,
});
cull_pass.set_pipeline(culling_pipeline);
cull_pass.set_push_constants(
0,
bytemuck::cast_slice(&[scene_cluster_count, raster_cluster_rightmost_slot]),
);
cull_pass.set_bind_group(
0,
culling_bind_group,
&[view_offset.offset, previous_view_offset.offset],
);
cull_pass.dispatch_workgroups(culling_workgroups, culling_workgroups, culling_workgroups);
if let (Some(remap_1d_to_2d_dispatch_pipeline), Some(remap_1d_to_2d_dispatch_bind_group)) = (
remap_1d_to_2d_dispatch_pipeline,
remap_1d_to_2d_dispatch_bind_group,
) {
cull_pass.set_pipeline(remap_1d_to_2d_dispatch_pipeline);
cull_pass.set_push_constants(0, &max_compute_workgroups_per_dimension.to_be_bytes());
cull_pass.set_bind_group(0, remap_1d_to_2d_dispatch_bind_group, &[]);
cull_pass.dispatch_workgroups(1, 1, 1);
}
}
fn raster_pass(
first_pass: bool,
render_context: &mut RenderContext,
visibility_buffer_hardware_software_indirect_args: &Buffer,
visibility_buffer_hardware_raster_indirect_args: &Buffer,
dummy_render_target: &TextureView,
meshlet_view_bind_groups: &MeshletViewBindGroups,
view_offset: &ViewUniformOffset,
visibility_buffer_hardware_software_pipeline: &ComputePipeline,
visibility_buffer_hardware_raster_pipeline: &RenderPipeline,
camera: Option<&ExtractedCamera>,
raster_cluster_rightmost_slot: u32,
) {
let command_encoder = render_context.command_encoder();
let mut software_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor {
label: Some(if first_pass {
"raster_software_first"
} else {
"raster_software_second"
}),
timestamp_writes: None,
});
software_pass.set_pipeline(visibility_buffer_hardware_software_pipeline);
software_pass.set_bind_group(
0,
&meshlet_view_bind_groups.visibility_buffer_raster,
&[view_offset.offset],
);
software_pass
.dispatch_workgroups_indirect(visibility_buffer_hardware_software_indirect_args, 0);
drop(software_pass);
let mut hardware_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some(if first_pass {
"raster_hardware_first"
} else {
"raster_hardware_second"
}),
color_attachments: &[Some(RenderPassColorAttachment {
view: dummy_render_target,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(LinearRgba::BLACK.into()),
store: StoreOp::Discard,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(viewport) = camera.and_then(|camera| camera.viewport.as_ref()) {
hardware_pass.set_camera_viewport(viewport);
}
hardware_pass.set_render_pipeline(visibility_buffer_hardware_raster_pipeline);
hardware_pass.set_push_constants(
ShaderStages::VERTEX,
0,
&raster_cluster_rightmost_slot.to_le_bytes(),
);
hardware_pass.set_bind_group(
0,
&meshlet_view_bind_groups.visibility_buffer_raster,
&[view_offset.offset],
);
hardware_pass.draw_indirect(visibility_buffer_hardware_raster_indirect_args, 0);
}
fn resolve_depth(
render_context: &mut RenderContext,
depth_stencil_attachment: RenderPassDepthStencilAttachment,
meshlet_view_bind_groups: &MeshletViewBindGroups,
resolve_depth_pipeline: &RenderPipeline,
camera: &ExtractedCamera,
) {
let mut resolve_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("resolve_depth"),
color_attachments: &[],
depth_stencil_attachment: Some(depth_stencil_attachment),
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(viewport) = &camera.viewport {
resolve_pass.set_camera_viewport(viewport);
}
resolve_pass.set_render_pipeline(resolve_depth_pipeline);
resolve_pass.set_bind_group(0, &meshlet_view_bind_groups.resolve_depth, &[]);
resolve_pass.draw(0..3, 0..1);
}
fn resolve_material_depth(
render_context: &mut RenderContext,
meshlet_view_resources: &MeshletViewResources,
meshlet_view_bind_groups: &MeshletViewBindGroups,
resolve_material_depth_pipeline: &RenderPipeline,
camera: &ExtractedCamera,
) {
if let (Some(material_depth), Some(resolve_material_depth_bind_group)) = (
meshlet_view_resources.material_depth.as_ref(),
meshlet_view_bind_groups.resolve_material_depth.as_ref(),
) {
let mut resolve_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("resolve_material_depth"),
color_attachments: &[],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &material_depth.default_view,
depth_ops: Some(Operations {
load: LoadOp::Clear(0.0),
store: StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(viewport) = &camera.viewport {
resolve_pass.set_camera_viewport(viewport);
}
resolve_pass.set_render_pipeline(resolve_material_depth_pipeline);
resolve_pass.set_bind_group(0, resolve_material_depth_bind_group, &[]);
resolve_pass.draw(0..3, 0..1);
}
}

View File

@@ -0,0 +1,240 @@
#define_import_path bevy_pbr::meshlet_visibility_buffer_resolve
#import bevy_pbr::{
meshlet_bindings::{
Meshlet,
meshlet_visibility_buffer,
meshlet_cluster_meshlet_ids,
meshlets,
meshlet_cluster_instance_ids,
meshlet_instance_uniforms,
get_meshlet_vertex_id,
get_meshlet_vertex_position,
get_meshlet_vertex_normal,
get_meshlet_vertex_uv,
},
mesh_view_bindings::view,
mesh_functions::mesh_position_local_to_world,
mesh_types::Mesh,
view_transformations::{position_world_to_clip, frag_coord_to_ndc},
}
#import bevy_render::maths::{affine3_to_square, mat2x4_f32_to_mat3x3_unpack}
#ifdef PREPASS_FRAGMENT
#ifdef MOTION_VECTOR_PREPASS
#import bevy_pbr::{
prepass_bindings::previous_view_uniforms,
pbr_prepass_functions::calculate_motion_vector,
}
#endif
#endif
/// Functions to be used by materials for reading from a meshlet visibility buffer texture.
#ifdef MESHLET_MESH_MATERIAL_PASS
struct PartialDerivatives {
barycentrics: vec3<f32>,
ddx: vec3<f32>,
ddy: vec3<f32>,
}
// https://github.com/ConfettiFX/The-Forge/blob/9d43e69141a9cd0ce2ce2d2db5122234d3a2d5b5/Common_3/Renderer/VisibilityBuffer2/Shaders/FSL/vb_shading_utilities.h.fsl#L90-L150
fn compute_partial_derivatives(vertex_world_positions: array<vec4<f32>, 3>, ndc_uv: vec2<f32>, half_screen_size: vec2<f32>) -> PartialDerivatives {
var result: PartialDerivatives;
let vertex_clip_position_0 = position_world_to_clip(vertex_world_positions[0].xyz);
let vertex_clip_position_1 = position_world_to_clip(vertex_world_positions[1].xyz);
let vertex_clip_position_2 = position_world_to_clip(vertex_world_positions[2].xyz);
let inv_w = 1.0 / vec3(vertex_clip_position_0.w, vertex_clip_position_1.w, vertex_clip_position_2.w);
let ndc_0 = vertex_clip_position_0.xy * inv_w[0];
let ndc_1 = vertex_clip_position_1.xy * inv_w[1];
let ndc_2 = vertex_clip_position_2.xy * inv_w[2];
let inv_det = 1.0 / determinant(mat2x2(ndc_2 - ndc_1, ndc_0 - ndc_1));
result.ddx = vec3(ndc_1.y - ndc_2.y, ndc_2.y - ndc_0.y, ndc_0.y - ndc_1.y) * inv_det * inv_w;
result.ddy = vec3(ndc_2.x - ndc_1.x, ndc_0.x - ndc_2.x, ndc_1.x - ndc_0.x) * inv_det * inv_w;
var ddx_sum = dot(result.ddx, vec3(1.0));
var ddy_sum = dot(result.ddy, vec3(1.0));
let delta_v = ndc_uv - ndc_0;
let interp_inv_w = inv_w.x + delta_v.x * ddx_sum + delta_v.y * ddy_sum;
let interp_w = 1.0 / interp_inv_w;
result.barycentrics = vec3(
interp_w * (inv_w[0] + delta_v.x * result.ddx.x + delta_v.y * result.ddy.x),
interp_w * (delta_v.x * result.ddx.y + delta_v.y * result.ddy.y),
interp_w * (delta_v.x * result.ddx.z + delta_v.y * result.ddy.z),
);
result.ddx *= half_screen_size.x;
result.ddy *= half_screen_size.y;
ddx_sum *= half_screen_size.x;
ddy_sum *= half_screen_size.y;
result.ddy *= -1.0;
ddy_sum *= -1.0;
let interp_ddx_w = 1.0 / (interp_inv_w + ddx_sum);
let interp_ddy_w = 1.0 / (interp_inv_w + ddy_sum);
result.ddx = interp_ddx_w * (result.barycentrics * interp_inv_w + result.ddx) - result.barycentrics;
result.ddy = interp_ddy_w * (result.barycentrics * interp_inv_w + result.ddy) - result.barycentrics;
return result;
}
struct VertexOutput {
position: vec4<f32>,
world_position: vec4<f32>,
world_normal: vec3<f32>,
uv: vec2<f32>,
ddx_uv: vec2<f32>,
ddy_uv: vec2<f32>,
world_tangent: vec4<f32>,
mesh_flags: u32,
cluster_id: u32,
material_bind_group_slot: u32,
#ifdef PREPASS_FRAGMENT
#ifdef MOTION_VECTOR_PREPASS
motion_vector: vec2<f32>,
#endif
#endif
}
/// Load the visibility buffer texture and resolve it into a VertexOutput.
fn resolve_vertex_output(frag_coord: vec4<f32>) -> VertexOutput {
let packed_ids = u32(textureLoad(meshlet_visibility_buffer, vec2<u32>(frag_coord.xy)).r);
let cluster_id = packed_ids >> 7u;
let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id];
var meshlet = meshlets[meshlet_id];
let triangle_id = extractBits(packed_ids, 0u, 7u);
let index_ids = meshlet.start_index_id + (triangle_id * 3u) + vec3(0u, 1u, 2u);
let vertex_ids = vec3(get_meshlet_vertex_id(index_ids[0]), get_meshlet_vertex_id(index_ids[1]), get_meshlet_vertex_id(index_ids[2]));
let vertex_0 = load_vertex(&meshlet, vertex_ids[0]);
let vertex_1 = load_vertex(&meshlet, vertex_ids[1]);
let vertex_2 = load_vertex(&meshlet, vertex_ids[2]);
let instance_id = meshlet_cluster_instance_ids[cluster_id];
var instance_uniform = meshlet_instance_uniforms[instance_id];
let world_from_local = affine3_to_square(instance_uniform.world_from_local);
let world_position_0 = mesh_position_local_to_world(world_from_local, vec4(vertex_0.position, 1.0));
let world_position_1 = mesh_position_local_to_world(world_from_local, vec4(vertex_1.position, 1.0));
let world_position_2 = mesh_position_local_to_world(world_from_local, vec4(vertex_2.position, 1.0));
let frag_coord_ndc = frag_coord_to_ndc(frag_coord).xy;
let partial_derivatives = compute_partial_derivatives(
array(world_position_0, world_position_1, world_position_2),
frag_coord_ndc,
view.viewport.zw / 2.0,
);
let world_position = mat3x4(world_position_0, world_position_1, world_position_2) * partial_derivatives.barycentrics;
let world_positions_camera_relative = mat3x3(
world_position_0.xyz - view.world_position,
world_position_1.xyz - view.world_position,
world_position_2.xyz - view.world_position,
);
let ddx_world_position = world_positions_camera_relative * partial_derivatives.ddx;
let ddy_world_position = world_positions_camera_relative * partial_derivatives.ddy;
let world_normal = mat3x3(
normal_local_to_world(vertex_0.normal, &instance_uniform),
normal_local_to_world(vertex_1.normal, &instance_uniform),
normal_local_to_world(vertex_2.normal, &instance_uniform),
) * partial_derivatives.barycentrics;
let uv = mat3x2(vertex_0.uv, vertex_1.uv, vertex_2.uv) * partial_derivatives.barycentrics;
let ddx_uv = mat3x2(vertex_0.uv, vertex_1.uv, vertex_2.uv) * partial_derivatives.ddx;
let ddy_uv = mat3x2(vertex_0.uv, vertex_1.uv, vertex_2.uv) * partial_derivatives.ddy;
let world_tangent = calculate_world_tangent(world_normal, ddx_world_position, ddy_world_position, ddx_uv, ddy_uv);
#ifdef PREPASS_FRAGMENT
#ifdef MOTION_VECTOR_PREPASS
let previous_world_from_local = affine3_to_square(instance_uniform.previous_world_from_local);
let previous_world_position_0 = mesh_position_local_to_world(previous_world_from_local, vec4(vertex_0.position, 1.0));
let previous_world_position_1 = mesh_position_local_to_world(previous_world_from_local, vec4(vertex_1.position, 1.0));
let previous_world_position_2 = mesh_position_local_to_world(previous_world_from_local, vec4(vertex_2.position, 1.0));
let previous_world_position = mat3x4(previous_world_position_0, previous_world_position_1, previous_world_position_2) * partial_derivatives.barycentrics;
let motion_vector = calculate_motion_vector(world_position, previous_world_position);
#endif
#endif
return VertexOutput(
frag_coord,
world_position,
world_normal,
uv,
ddx_uv,
ddy_uv,
world_tangent,
instance_uniform.flags,
instance_id ^ meshlet_id,
instance_uniform.material_and_lightmap_bind_group_slot & 0xffffu,
#ifdef PREPASS_FRAGMENT
#ifdef MOTION_VECTOR_PREPASS
motion_vector,
#endif
#endif
);
}
struct MeshletVertex {
position: vec3<f32>,
normal: vec3<f32>,
uv: vec2<f32>,
}
fn load_vertex(meshlet: ptr<function, Meshlet>, vertex_id: u32) -> MeshletVertex {
return MeshletVertex(
get_meshlet_vertex_position(meshlet, vertex_id),
get_meshlet_vertex_normal(meshlet, vertex_id),
get_meshlet_vertex_uv(meshlet, vertex_id),
);
}
fn normal_local_to_world(vertex_normal: vec3<f32>, instance_uniform: ptr<function, Mesh>) -> vec3<f32> {
if any(vertex_normal != vec3<f32>(0.0)) {
return normalize(
mat2x4_f32_to_mat3x3_unpack(
(*instance_uniform).local_from_world_transpose_a,
(*instance_uniform).local_from_world_transpose_b,
) * vertex_normal
);
} else {
return vertex_normal;
}
}
// https://www.jeremyong.com/graphics/2023/12/16/surface-gradient-bump-mapping/#surface-gradient-from-a-tangent-space-normal-vector-without-an-explicit-tangent-basis
fn calculate_world_tangent(
world_normal: vec3<f32>,
ddx_world_position: vec3<f32>,
ddy_world_position: vec3<f32>,
ddx_uv: vec2<f32>,
ddy_uv: vec2<f32>,
) -> vec4<f32> {
// Project the position gradients onto the tangent plane
let ddx_world_position_s = ddx_world_position - dot(ddx_world_position, world_normal) * world_normal;
let ddy_world_position_s = ddy_world_position - dot(ddy_world_position, world_normal) * world_normal;
// Compute the jacobian matrix to leverage the chain rule
let jacobian_sign = sign(ddx_uv.x * ddy_uv.y - ddx_uv.y * ddy_uv.x);
var world_tangent = jacobian_sign * (ddy_uv.y * ddx_world_position_s - ddx_uv.y * ddy_world_position_s);
// The sign intrinsic returns 0 if the argument is 0
if jacobian_sign != 0.0 {
world_tangent = normalize(world_tangent);
}
// The second factor here ensures a consistent handedness between
// the tangent frame and surface basis w.r.t. screenspace.
let w = jacobian_sign * sign(dot(ddy_world_position, cross(world_normal, ddx_world_position)));
return vec4(world_tangent, -w); // TODO: Unclear why we need to negate this to match mikktspace generated tangents
}
#endif

View File

@@ -0,0 +1,189 @@
#import bevy_pbr::{
meshlet_bindings::{
meshlet_cluster_meshlet_ids,
meshlets,
meshlet_cluster_instance_ids,
meshlet_instance_uniforms,
meshlet_raster_clusters,
meshlet_software_raster_cluster_count,
meshlet_visibility_buffer,
view,
get_meshlet_vertex_count,
get_meshlet_triangle_count,
get_meshlet_vertex_id,
get_meshlet_vertex_position,
},
mesh_functions::mesh_position_local_to_world,
view_transformations::ndc_to_uv,
}
#import bevy_render::maths::affine3_to_square
/// Compute shader for rasterizing small clusters into a visibility buffer.
// TODO: Fixed-point math and top-left rule
var<workgroup> viewport_vertices: array<vec3f, 255>;
@compute
@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1-2 vertices per thread, 1 triangle per thread, 1 cluster per workgroup
fn rasterize_cluster(
@builtin(workgroup_id) workgroup_id: vec3<u32>,
@builtin(local_invocation_index) local_invocation_index: u32,
#ifdef MESHLET_2D_DISPATCH
@builtin(num_workgroups) num_workgroups: vec3<u32>,
#endif
) {
var workgroup_id_1d = workgroup_id.x;
#ifdef MESHLET_2D_DISPATCH
workgroup_id_1d += workgroup_id.y * num_workgroups.x;
if workgroup_id_1d >= meshlet_software_raster_cluster_count { return; }
#endif
let cluster_id = meshlet_raster_clusters[workgroup_id_1d];
let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id];
var meshlet = meshlets[meshlet_id];
let instance_id = meshlet_cluster_instance_ids[cluster_id];
let instance_uniform = meshlet_instance_uniforms[instance_id];
let world_from_local = affine3_to_square(instance_uniform.world_from_local);
// Load and project 1 vertex per thread, and then again if there are more than 128 vertices in the meshlet
for (var i = 0u; i <= 128u; i += 128u) {
let vertex_id = local_invocation_index + i;
if vertex_id < get_meshlet_vertex_count(&meshlet) {
let vertex_position = get_meshlet_vertex_position(&meshlet, vertex_id);
// Project vertex to viewport space
let world_position = mesh_position_local_to_world(world_from_local, vec4(vertex_position, 1.0));
let clip_position = view.clip_from_world * vec4(world_position.xyz, 1.0);
let ndc_position = clip_position.xyz / clip_position.w;
let viewport_position_xy = ndc_to_uv(ndc_position.xy) * view.viewport.zw;
// Write vertex to workgroup shared memory
viewport_vertices[vertex_id] = vec3(viewport_position_xy, ndc_position.z);
}
}
workgroupBarrier();
// Load 1 triangle's worth of vertex data per thread
let triangle_id = local_invocation_index;
if triangle_id >= get_meshlet_triangle_count(&meshlet) { return; }
let index_ids = meshlet.start_index_id + (triangle_id * 3u) + vec3(0u, 1u, 2u);
let vertex_ids = vec3(get_meshlet_vertex_id(index_ids[0]), get_meshlet_vertex_id(index_ids[1]), get_meshlet_vertex_id(index_ids[2]));
let vertex_0 = viewport_vertices[vertex_ids[2]];
let vertex_1 = viewport_vertices[vertex_ids[1]];
let vertex_2 = viewport_vertices[vertex_ids[0]];
let packed_ids = (cluster_id << 7u) | triangle_id;
// Backface culling
let triangle_double_area = edge_function(vertex_0.xy, vertex_1.xy, vertex_2.xy);
if triangle_double_area <= 0.0 { return; }
// Setup triangle gradients
let w_x = vec3(vertex_1.y - vertex_2.y, vertex_2.y - vertex_0.y, vertex_0.y - vertex_1.y);
let w_y = vec3(vertex_2.x - vertex_1.x, vertex_0.x - vertex_2.x, vertex_1.x - vertex_0.x);
let vertices_z = vec3(vertex_0.z, vertex_1.z, vertex_2.z) / triangle_double_area;
let z_x = dot(vertices_z, w_x);
let z_y = dot(vertices_z, w_y);
// Compute triangle bounding box
var min_x = floor(min3(vertex_0.x, vertex_1.x, vertex_2.x));
var min_y = floor(min3(vertex_0.y, vertex_1.y, vertex_2.y));
var max_x = ceil(max3(vertex_0.x, vertex_1.x, vertex_2.x));
var max_y = ceil(max3(vertex_0.y, vertex_1.y, vertex_2.y));
min_x = max(min_x, 0.0);
min_y = max(min_y, 0.0);
max_x = min(max_x, view.viewport.z - 1.0);
max_y = min(max_y, view.viewport.w - 1.0);
// Setup initial triangle equations
let starting_pixel = vec2(min_x, min_y) + 0.5;
var w_row = vec3(
edge_function(vertex_1.xy, vertex_2.xy, starting_pixel),
edge_function(vertex_2.xy, vertex_0.xy, starting_pixel),
edge_function(vertex_0.xy, vertex_1.xy, starting_pixel),
);
var z_row = dot(vertices_z, w_row);
// Rasterize triangle
if subgroupAny(max_x - min_x > 4.0) {
// Scanline setup
let edge_012 = -w_x;
let open_edge = edge_012 < vec3(0.0);
let inverse_edge_012 = select(1.0 / edge_012, vec3(1e8), edge_012 == vec3(0.0));
let max_x_diff = vec3(max_x - min_x);
for (var y = min_y; y <= max_y; y += 1.0) {
// Calculate start and end X interval for pixels in this row within the triangle
let cross_x = w_row * inverse_edge_012;
let min_x2 = select(vec3(0.0), cross_x, open_edge);
let max_x2 = select(cross_x, max_x_diff, open_edge);
var x0 = ceil(max3(min_x2[0], min_x2[1], min_x2[2]));
var x1 = min3(max_x2[0], max_x2[1], max_x2[2]);
var w = w_row + w_x * x0;
var z = z_row + z_x * x0;
x0 += min_x;
x1 += min_x;
// Iterate scanline X interval
for (var x = x0; x <= x1; x += 1.0) {
// Check if point at pixel is within triangle (TODO: this shouldn't be needed, but there's bugs without it)
if min3(w[0], w[1], w[2]) >= 0.0 {
write_visibility_buffer_pixel(x, y, z, packed_ids);
}
// Increment triangle equations along the X-axis
w += w_x;
z += z_x;
}
// Increment triangle equations along the Y-axis
w_row += w_y;
z_row += z_y;
}
} else {
// Iterate over every pixel in the triangle's bounding box
for (var y = min_y; y <= max_y; y += 1.0) {
var w = w_row;
var z = z_row;
for (var x = min_x; x <= max_x; x += 1.0) {
// Check if point at pixel is within triangle
if min3(w[0], w[1], w[2]) >= 0.0 {
write_visibility_buffer_pixel(x, y, z, packed_ids);
}
// Increment triangle equations along the X-axis
w += w_x;
z += z_x;
}
// Increment triangle equations along the Y-axis
w_row += w_y;
z_row += z_y;
}
}
}
fn write_visibility_buffer_pixel(x: f32, y: f32, z: f32, packed_ids: u32) {
let depth = bitcast<u32>(z);
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
let visibility = (u64(depth) << 32u) | u64(packed_ids);
#else
let visibility = depth;
#endif
textureAtomicMax(meshlet_visibility_buffer, vec2(u32(x), u32(y)), visibility);
}
fn edge_function(a: vec2<f32>, b: vec2<f32>, c: vec2<f32>) -> f32 {
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
}
fn min3(a: f32, b: f32, c: f32) -> f32 {
return min(a, min(b, c));
}
fn max3(a: f32, b: f32, c: f32) -> f32 {
return max(a, max(b, c));
}

46
vendor/bevy_pbr/src/parallax.rs vendored Normal file
View File

@@ -0,0 +1,46 @@
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
/// The [parallax mapping] method to use to compute depth based on the
/// material's [`depth_map`].
///
/// Parallax Mapping uses a depth map texture to give the illusion of depth
/// variation on a mesh surface that is geometrically flat.
///
/// See the `parallax_mapping.wgsl` shader code for implementation details
/// and explanation of the methods used.
///
/// [`depth_map`]: crate::StandardMaterial::depth_map
/// [parallax mapping]: https://en.wikipedia.org/wiki/Parallax_mapping
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Reflect)]
#[reflect(Default, Clone, PartialEq)]
pub enum ParallaxMappingMethod {
/// A simple linear interpolation, using a single texture sample.
///
/// This method is named "Parallax Occlusion Mapping".
///
/// Unlike [`ParallaxMappingMethod::Relief`], only requires a single lookup,
/// but may skip small details and result in writhing material artifacts.
#[default]
Occlusion,
/// Discovers the best depth value based on binary search.
///
/// Each iteration incurs a texture sample.
/// The result has fewer visual artifacts than [`ParallaxMappingMethod::Occlusion`].
///
/// This method is named "Relief Mapping".
Relief {
/// How many additional steps to use at most to find the depth value.
max_steps: u32,
},
}
impl ParallaxMappingMethod {
/// [`ParallaxMappingMethod::Relief`] with a 5 steps, a reasonable default.
pub const DEFAULT_RELIEF_MAPPING: Self = ParallaxMappingMethod::Relief { max_steps: 5 };
pub(crate) fn max_steps(&self) -> u32 {
match self {
ParallaxMappingMethod::Occlusion => 0,
ParallaxMappingMethod::Relief { max_steps } => *max_steps,
}
}
}

1529
vendor/bevy_pbr/src/pbr_material.rs vendored Normal file

File diff suppressed because it is too large Load Diff

1319
vendor/bevy_pbr/src/prepass/mod.rs vendored Normal file

File diff suppressed because it is too large Load Diff

219
vendor/bevy_pbr/src/prepass/prepass.wgsl vendored Normal file
View File

@@ -0,0 +1,219 @@
#import bevy_pbr::{
prepass_bindings,
mesh_bindings::mesh,
mesh_functions,
prepass_io::{Vertex, VertexOutput, FragmentOutput},
skinning,
morph,
mesh_view_bindings::view,
view_transformations::position_world_to_clip,
}
#ifdef DEFERRED_PREPASS
#import bevy_pbr::rgb9e5
#endif
#ifdef MORPH_TARGETS
fn morph_vertex(vertex_in: Vertex) -> Vertex {
var vertex = vertex_in;
let first_vertex = mesh[vertex.instance_index].first_vertex_index;
let vertex_index = vertex.index - first_vertex;
let weight_count = morph::layer_count();
for (var i: u32 = 0u; i < weight_count; i ++) {
let weight = morph::weight_at(i);
if weight == 0.0 {
continue;
}
vertex.position += weight * morph::morph(vertex_index, morph::position_offset, i);
#ifdef VERTEX_NORMALS
vertex.normal += weight * morph::morph(vertex_index, morph::normal_offset, i);
#endif
#ifdef VERTEX_TANGENTS
vertex.tangent += vec4(weight * morph::morph(vertex_index, morph::tangent_offset, i), 0.0);
#endif
}
return vertex;
}
// Returns the morphed position of the given vertex from the previous frame.
//
// This function is used for motion vector calculation, and, as such, it doesn't
// bother morphing the normals and tangents.
fn morph_prev_vertex(vertex_in: Vertex) -> Vertex {
var vertex = vertex_in;
let weight_count = morph::layer_count();
for (var i: u32 = 0u; i < weight_count; i ++) {
let weight = morph::prev_weight_at(i);
if weight == 0.0 {
continue;
}
vertex.position += weight * morph::morph(vertex.index, morph::position_offset, i);
// Don't bother morphing normals and tangents; we don't need them for
// motion vector calculation.
}
return vertex;
}
#endif // MORPH_TARGETS
@vertex
fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
var out: VertexOutput;
#ifdef MORPH_TARGETS
var vertex = morph_vertex(vertex_no_morph);
#else
var vertex = vertex_no_morph;
#endif
let mesh_world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index);
#ifdef SKINNED
var world_from_local = skinning::skin_model(
vertex.joint_indices,
vertex.joint_weights,
vertex_no_morph.instance_index
);
#else // SKINNED
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
var world_from_local = mesh_world_from_local;
#endif // SKINNED
out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4<f32>(vertex.position, 1.0));
out.position = position_world_to_clip(out.world_position.xyz);
#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION
out.unclipped_depth = out.position.z;
out.position.z = min(out.position.z, 1.0); // Clamp depth to avoid clipping
#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION
#ifdef VERTEX_UVS_A
out.uv = vertex.uv;
#endif // VERTEX_UVS_A
#ifdef VERTEX_UVS_B
out.uv_b = vertex.uv_b;
#endif // VERTEX_UVS_B
#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS
#ifdef VERTEX_NORMALS
#ifdef SKINNED
out.world_normal = skinning::skin_normals(world_from_local, vertex.normal);
#else // SKINNED
out.world_normal = mesh_functions::mesh_normal_local_to_world(
vertex.normal,
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
vertex_no_morph.instance_index
);
#endif // SKINNED
#endif // VERTEX_NORMALS
#ifdef VERTEX_TANGENTS
out.world_tangent = mesh_functions::mesh_tangent_local_to_world(
world_from_local,
vertex.tangent,
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
vertex_no_morph.instance_index
);
#endif // VERTEX_TANGENTS
#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS
#ifdef VERTEX_COLORS
out.color = vertex.color;
#endif
// Compute the motion vector for TAA among other purposes. For this we need
// to know where the vertex was last frame.
#ifdef MOTION_VECTOR_PREPASS
// Take morph targets into account.
#ifdef MORPH_TARGETS
#ifdef HAS_PREVIOUS_MORPH
let prev_vertex = morph_prev_vertex(vertex_no_morph);
#else // HAS_PREVIOUS_MORPH
let prev_vertex = vertex_no_morph;
#endif // HAS_PREVIOUS_MORPH
#else // MORPH_TARGETS
let prev_vertex = vertex_no_morph;
#endif // MORPH_TARGETS
// Take skinning into account.
#ifdef SKINNED
#ifdef HAS_PREVIOUS_SKIN
let prev_model = skinning::skin_prev_model(
prev_vertex.joint_indices,
prev_vertex.joint_weights,
vertex_no_morph.instance_index
);
#else // HAS_PREVIOUS_SKIN
let prev_model = mesh_functions::get_previous_world_from_local(prev_vertex.instance_index);
#endif // HAS_PREVIOUS_SKIN
#else // SKINNED
let prev_model = mesh_functions::get_previous_world_from_local(prev_vertex.instance_index);
#endif // SKINNED
out.previous_world_position = mesh_functions::mesh_position_local_to_world(
prev_model,
vec4<f32>(prev_vertex.position, 1.0)
);
#endif // MOTION_VECTOR_PREPASS
#ifdef VERTEX_OUTPUT_INSTANCE_INDEX
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
out.instance_index = vertex_no_morph.instance_index;
#endif
#ifdef VISIBILITY_RANGE_DITHER
out.visibility_range_dither = mesh_functions::get_visibility_range_dither_level(
vertex_no_morph.instance_index, mesh_world_from_local[3]);
#endif // VISIBILITY_RANGE_DITHER
return out;
}
#ifdef PREPASS_FRAGMENT
@fragment
fn fragment(in: VertexOutput) -> FragmentOutput {
var out: FragmentOutput;
#ifdef NORMAL_PREPASS
out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0);
#endif
#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION
out.frag_depth = in.unclipped_depth;
#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION
#ifdef MOTION_VECTOR_PREPASS
let clip_position_t = view.unjittered_clip_from_world * in.world_position;
let clip_position = clip_position_t.xy / clip_position_t.w;
let previous_clip_position_t = prepass_bindings::previous_view_uniforms.clip_from_world * in.previous_world_position;
let previous_clip_position = previous_clip_position_t.xy / previous_clip_position_t.w;
// These motion vectors are used as offsets to UV positions and are stored
// in the range -1,1 to allow offsetting from the one corner to the
// diagonally-opposite corner in UV coordinates, in either direction.
// A difference between diagonally-opposite corners of clip space is in the
// range -2,2, so this needs to be scaled by 0.5. And the V direction goes
// down where clip space y goes up, so y needs to be flipped.
out.motion_vector = (clip_position - previous_clip_position) * vec2(0.5, -0.5);
#endif // MOTION_VECTOR_PREPASS
#ifdef DEFERRED_PREPASS
// There isn't any material info available for this default prepass shader so we are just writing 
// emissive magenta out to the deferred gbuffer to be rendered by the first deferred lighting pass layer.
// This is here so if the default prepass fragment is used for deferred magenta will be rendered, and also
// as an example to show that a user could write to the deferred gbuffer if they were to start from this shader.
out.deferred = vec4(0u, bevy_pbr::rgb9e5::vec3_to_rgb9e5_(vec3(1.0, 0.0, 1.0)), 0u, 0u);
out.deferred_lighting_pass_id = 1u;
#endif
return out;
}
#endif // PREPASS_FRAGMENT

View File

@@ -0,0 +1,75 @@
use bevy_core_pipeline::prepass::ViewPrepassTextures;
use bevy_render::render_resource::{
binding_types::{
texture_2d, texture_2d_multisampled, texture_depth_2d, texture_depth_2d_multisampled,
},
BindGroupLayoutEntryBuilder, TextureAspect, TextureSampleType, TextureView,
TextureViewDescriptor,
};
use bevy_utils::default;
use crate::MeshPipelineViewLayoutKey;
pub fn get_bind_group_layout_entries(
layout_key: MeshPipelineViewLayoutKey,
) -> [Option<BindGroupLayoutEntryBuilder>; 4] {
let mut entries: [Option<BindGroupLayoutEntryBuilder>; 4] = [None; 4];
let multisampled = layout_key.contains(MeshPipelineViewLayoutKey::MULTISAMPLED);
if layout_key.contains(MeshPipelineViewLayoutKey::DEPTH_PREPASS) {
// Depth texture
entries[0] = if multisampled {
Some(texture_depth_2d_multisampled())
} else {
Some(texture_depth_2d())
};
}
if layout_key.contains(MeshPipelineViewLayoutKey::NORMAL_PREPASS) {
// Normal texture
entries[1] = if multisampled {
Some(texture_2d_multisampled(TextureSampleType::Float {
filterable: false,
}))
} else {
Some(texture_2d(TextureSampleType::Float { filterable: false }))
};
}
if layout_key.contains(MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS) {
// Motion Vectors texture
entries[2] = if multisampled {
Some(texture_2d_multisampled(TextureSampleType::Float {
filterable: false,
}))
} else {
Some(texture_2d(TextureSampleType::Float { filterable: false }))
};
}
if layout_key.contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS) {
// Deferred texture
entries[3] = Some(texture_2d(TextureSampleType::Uint));
}
entries
}
pub fn get_bindings(prepass_textures: Option<&ViewPrepassTextures>) -> [Option<TextureView>; 4] {
let depth_desc = TextureViewDescriptor {
label: Some("prepass_depth"),
aspect: TextureAspect::DepthOnly,
..default()
};
let depth_view = prepass_textures
.and_then(|x| x.depth.as_ref())
.map(|texture| texture.texture.texture.create_view(&depth_desc));
[
depth_view,
prepass_textures.and_then(|pt| pt.normal_view().cloned()),
prepass_textures.and_then(|pt| pt.motion_vectors_view().cloned()),
prepass_textures.and_then(|pt| pt.deferred_view().cloned()),
]
}

View File

@@ -0,0 +1,11 @@
#define_import_path bevy_pbr::prepass_bindings
struct PreviousViewUniforms {
view_from_world: mat4x4<f32>,
clip_from_world: mat4x4<f32>,
clip_from_view: mat4x4<f32>,
}
@group(0) @binding(2) var<uniform> previous_view_uniforms: PreviousViewUniforms;
// Material bindings will be in @group(2)

View File

@@ -0,0 +1,100 @@
#define_import_path bevy_pbr::prepass_io
// Most of these attributes are not used in the default prepass fragment shader, but they are still needed so we can
// pass them to custom prepass shaders like pbr_prepass.wgsl.
struct Vertex {
@builtin(instance_index) instance_index: u32,
@location(0) position: vec3<f32>,
#ifdef VERTEX_UVS_A
@location(1) uv: vec2<f32>,
#endif
#ifdef VERTEX_UVS_B
@location(2) uv_b: vec2<f32>,
#endif
#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS
#ifdef VERTEX_NORMALS
@location(3) normal: vec3<f32>,
#endif
#ifdef VERTEX_TANGENTS
@location(4) tangent: vec4<f32>,
#endif
#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS
#ifdef SKINNED
@location(5) joint_indices: vec4<u32>,
@location(6) joint_weights: vec4<f32>,
#endif
#ifdef VERTEX_COLORS
@location(7) color: vec4<f32>,
#endif
#ifdef MORPH_TARGETS
@builtin(vertex_index) index: u32,
#endif // MORPH_TARGETS
}
struct VertexOutput {
// This is `clip position` when the struct is used as a vertex stage output
// and `frag coord` when used as a fragment stage input
@builtin(position) position: vec4<f32>,
#ifdef VERTEX_UVS_A
@location(0) uv: vec2<f32>,
#endif
#ifdef VERTEX_UVS_B
@location(1) uv_b: vec2<f32>,
#endif
#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS
@location(2) world_normal: vec3<f32>,
#ifdef VERTEX_TANGENTS
@location(3) world_tangent: vec4<f32>,
#endif
#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS
@location(4) world_position: vec4<f32>,
#ifdef MOTION_VECTOR_PREPASS
@location(5) previous_world_position: vec4<f32>,
#endif
#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION
@location(6) unclipped_depth: f32,
#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION
#ifdef VERTEX_OUTPUT_INSTANCE_INDEX
@location(7) instance_index: u32,
#endif
#ifdef VERTEX_COLORS
@location(8) color: vec4<f32>,
#endif
#ifdef VISIBILITY_RANGE_DITHER
@location(9) @interpolate(flat) visibility_range_dither: i32,
#endif // VISIBILITY_RANGE_DITHER
}
#ifdef PREPASS_FRAGMENT
struct FragmentOutput {
#ifdef NORMAL_PREPASS
@location(0) normal: vec4<f32>,
#endif
#ifdef MOTION_VECTOR_PREPASS
@location(1) motion_vector: vec2<f32>,
#endif
#ifdef DEFERRED_PREPASS
@location(2) deferred: vec4<u32>,
@location(3) deferred_lighting_pass_id: u32,
#endif
#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION
@builtin(frag_depth) frag_depth: f32,
#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION
}
#endif //PREPASS_FRAGMENT

View File

@@ -0,0 +1,35 @@
#define_import_path bevy_pbr::prepass_utils
#import bevy_pbr::mesh_view_bindings as view_bindings
#ifdef DEPTH_PREPASS
fn prepass_depth(frag_coord: vec4<f32>, sample_index: u32) -> f32 {
#ifdef MULTISAMPLED
return textureLoad(view_bindings::depth_prepass_texture, vec2<i32>(frag_coord.xy), i32(sample_index));
#else // MULTISAMPLED
return textureLoad(view_bindings::depth_prepass_texture, vec2<i32>(frag_coord.xy), 0);
#endif // MULTISAMPLED
}
#endif // DEPTH_PREPASS
#ifdef NORMAL_PREPASS
fn prepass_normal(frag_coord: vec4<f32>, sample_index: u32) -> vec3<f32> {
#ifdef MULTISAMPLED
let normal_sample = textureLoad(view_bindings::normal_prepass_texture, vec2<i32>(frag_coord.xy), i32(sample_index));
#else
let normal_sample = textureLoad(view_bindings::normal_prepass_texture, vec2<i32>(frag_coord.xy), 0);
#endif // MULTISAMPLED
return normalize(normal_sample.xyz * 2.0 - vec3(1.0));
}
#endif // NORMAL_PREPASS
#ifdef MOTION_VECTOR_PREPASS
fn prepass_motion_vector(frag_coord: vec4<f32>, sample_index: u32) -> vec2<f32> {
#ifdef MULTISAMPLED
let motion_vector_sample = textureLoad(view_bindings::motion_vector_prepass_texture, vec2<i32>(frag_coord.xy), i32(sample_index));
#else
let motion_vector_sample = textureLoad(view_bindings::motion_vector_prepass_texture, vec2<i32>(frag_coord.xy), 0);
#endif
return motion_vector_sample.rg;
}
#endif // MOTION_VECTOR_PREPASS

View File

@@ -0,0 +1,142 @@
// Builds GPU indirect draw parameters from metadata.
//
// This only runs when indirect drawing is enabled. It takes the output of
// `mesh_preprocess.wgsl` and creates indirect parameters for the GPU.
//
// This shader runs separately for indexed and non-indexed meshes. Unlike
// `mesh_preprocess.wgsl`, which runs one instance per mesh *instance*, one
// instance of this shader corresponds to a single *batch* which could contain
// arbitrarily many instances of a single mesh.
#import bevy_pbr::mesh_preprocess_types::{
IndirectBatchSet,
IndirectParametersIndexed,
IndirectParametersNonIndexed,
IndirectParametersCpuMetadata,
IndirectParametersGpuMetadata,
MeshInput
}
// The data for each mesh that the CPU supplied to the GPU.
@group(0) @binding(0) var<storage> current_input: array<MeshInput>;
// Data that we use to generate the indirect parameters.
//
// The `mesh_preprocess.wgsl` shader emits these.
@group(0) @binding(1) var<storage> indirect_parameters_cpu_metadata:
array<IndirectParametersCpuMetadata>;
@group(0) @binding(2) var<storage> indirect_parameters_gpu_metadata:
array<IndirectParametersGpuMetadata>;
// Information about each batch set.
//
// A *batch set* is a set of meshes that might be multi-drawn together.
@group(0) @binding(3) var<storage, read_write> indirect_batch_sets: array<IndirectBatchSet>;
#ifdef INDEXED
// The buffer of indirect draw parameters that we generate, and that the GPU
// reads to issue the draws.
//
// This buffer is for indexed meshes.
@group(0) @binding(4) var<storage, read_write> indirect_parameters:
array<IndirectParametersIndexed>;
#else // INDEXED
// The buffer of indirect draw parameters that we generate, and that the GPU
// reads to issue the draws.
//
// This buffer is for non-indexed meshes.
@group(0) @binding(4) var<storage, read_write> indirect_parameters:
array<IndirectParametersNonIndexed>;
#endif // INDEXED
@compute
@workgroup_size(64)
fn main(@builtin(global_invocation_id) global_invocation_id: vec3<u32>) {
// Figure out our instance index (i.e. batch index). If this thread doesn't
// correspond to any index, bail.
let instance_index = global_invocation_id.x;
if (instance_index >= arrayLength(&indirect_parameters_cpu_metadata)) {
return;
}
// Unpack the metadata for this batch.
let base_output_index = indirect_parameters_cpu_metadata[instance_index].base_output_index;
let batch_set_index = indirect_parameters_cpu_metadata[instance_index].batch_set_index;
let mesh_index = indirect_parameters_gpu_metadata[instance_index].mesh_index;
// If we aren't using `multi_draw_indirect_count`, we have a 1:1 fixed
// assignment of batches to slots in the indirect parameters buffer, so we
// can just use the instance index as the index of our indirect parameters.
let early_instance_count =
indirect_parameters_gpu_metadata[instance_index].early_instance_count;
let late_instance_count = indirect_parameters_gpu_metadata[instance_index].late_instance_count;
// If in the early phase, we draw only the early meshes. If in the late
// phase, we draw only the late meshes. If in the main phase, draw all the
// meshes.
#ifdef EARLY_PHASE
let instance_count = early_instance_count;
#else // EARLY_PHASE
#ifdef LATE_PHASE
let instance_count = late_instance_count;
#else // LATE_PHASE
let instance_count = early_instance_count + late_instance_count;
#endif // LATE_PHASE
#endif // EARLY_PHASE
var indirect_parameters_index = instance_index;
// If the current hardware and driver support `multi_draw_indirect_count`,
// dynamically reserve an index for the indirect parameters we're to
// generate.
#ifdef MULTI_DRAW_INDIRECT_COUNT_SUPPORTED
// If this batch belongs to a batch set, then allocate space for the
// indirect commands in that batch set.
if (batch_set_index != 0xffffffffu) {
// Bail out now if there are no instances. Note that we can only bail if
// we're in a batch set. That's because only batch sets are drawn using
// `multi_draw_indirect_count`. If we aren't using
// `multi_draw_indirect_count`, then we need to continue in order to
// zero out the instance count; otherwise, it'll have garbage data in
// it.
if (instance_count == 0u) {
return;
}
let indirect_parameters_base =
indirect_batch_sets[batch_set_index].indirect_parameters_base;
let indirect_parameters_offset =
atomicAdd(&indirect_batch_sets[batch_set_index].indirect_parameters_count, 1u);
indirect_parameters_index = indirect_parameters_base + indirect_parameters_offset;
}
#endif // MULTI_DRAW_INDIRECT_COUNT_SUPPORTED
// Build up the indirect parameters. The structures for indexed and
// non-indexed meshes are slightly different.
indirect_parameters[indirect_parameters_index].instance_count = instance_count;
#ifdef LATE_PHASE
// The late mesh instances are stored after the early mesh instances, so we
// offset the output index by the number of early mesh instances.
indirect_parameters[indirect_parameters_index].first_instance =
base_output_index + early_instance_count;
#else // LATE_PHASE
indirect_parameters[indirect_parameters_index].first_instance = base_output_index;
#endif // LATE_PHASE
indirect_parameters[indirect_parameters_index].base_vertex =
current_input[mesh_index].first_vertex_index;
#ifdef INDEXED
indirect_parameters[indirect_parameters_index].index_count =
current_input[mesh_index].index_count;
indirect_parameters[indirect_parameters_index].first_index =
current_input[mesh_index].first_index_index;
#else // INDEXED
indirect_parameters[indirect_parameters_index].vertex_count =
current_input[mesh_index].index_count;
#endif // INDEXED
}

View File

@@ -0,0 +1,193 @@
#define_import_path bevy_pbr::clustered_forward
#import bevy_pbr::{
mesh_view_bindings as bindings,
utils::rand_f,
}
#import bevy_render::{
color_operations::hsv_to_rgb,
maths::PI_2,
}
// Offsets within the `cluster_offsets_and_counts` buffer for a single cluster.
//
// These offsets must be monotonically nondecreasing. That is, indices are
// always sorted into the following order: point lights, spot lights, reflection
// probes, irradiance volumes.
struct ClusterableObjectIndexRanges {
// The offset of the index of the first point light.
first_point_light_index_offset: u32,
// The offset of the index of the first spot light, which also terminates
// the list of point lights.
first_spot_light_index_offset: u32,
// The offset of the index of the first reflection probe, which also
// terminates the list of spot lights.
first_reflection_probe_index_offset: u32,
// The offset of the index of the first irradiance volumes, which also
// terminates the list of reflection probes.
first_irradiance_volume_index_offset: u32,
first_decal_offset: u32,
// One past the offset of the index of the final clusterable object for this
// cluster.
last_clusterable_object_index_offset: u32,
}
// NOTE: Keep in sync with bevy_pbr/src/light.rs
fn view_z_to_z_slice(view_z: f32, is_orthographic: bool) -> u32 {
var z_slice: u32 = 0u;
if is_orthographic {
// NOTE: view_z is correct in the orthographic case
z_slice = u32(floor((view_z - bindings::lights.cluster_factors.z) * bindings::lights.cluster_factors.w));
} else {
// NOTE: had to use -view_z to make it positive else log(negative) is nan
z_slice = u32(log(-view_z) * bindings::lights.cluster_factors.z - bindings::lights.cluster_factors.w + 1.0);
}
// NOTE: We use min as we may limit the far z plane used for clustering to be closer than
// the furthest thing being drawn. This means that we need to limit to the maximum cluster.
return min(z_slice, bindings::lights.cluster_dimensions.z - 1u);
}
fn fragment_cluster_index(frag_coord: vec2<f32>, view_z: f32, is_orthographic: bool) -> u32 {
let xy = vec2<u32>(floor((frag_coord - bindings::view.viewport.xy) * bindings::lights.cluster_factors.xy));
let z_slice = view_z_to_z_slice(view_z, is_orthographic);
// NOTE: Restricting cluster index to avoid undefined behavior when accessing uniform buffer
// arrays based on the cluster index.
return min(
(xy.y * bindings::lights.cluster_dimensions.x + xy.x) * bindings::lights.cluster_dimensions.z + z_slice,
bindings::lights.cluster_dimensions.w - 1u
);
}
// this must match CLUSTER_COUNT_SIZE in light.rs
const CLUSTER_COUNT_SIZE = 9u;
// Returns the indices of clusterable objects belonging to the given cluster.
//
// Note that if fewer than 3 SSBO bindings are available (in WebGL 2,
// primarily), light probes aren't clustered, and therefore both light probe
// index ranges will be empty.
fn unpack_clusterable_object_index_ranges(cluster_index: u32) -> ClusterableObjectIndexRanges {
#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3
let offset_and_counts_a = bindings::cluster_offsets_and_counts.data[cluster_index][0];
let offset_and_counts_b = bindings::cluster_offsets_and_counts.data[cluster_index][1];
// Sum up the counts to produce the range brackets.
//
// We could have stored the range brackets in `cluster_offsets_and_counts`
// directly, but doing it this way makes the logic in this path more
// consistent with the WebGL 2 path below.
let point_light_offset = offset_and_counts_a.x;
let spot_light_offset = point_light_offset + offset_and_counts_a.y;
let reflection_probe_offset = spot_light_offset + offset_and_counts_a.z;
let irradiance_volume_offset = reflection_probe_offset + offset_and_counts_a.w;
let decal_offset = irradiance_volume_offset + offset_and_counts_b.x;
let last_clusterable_offset = decal_offset + offset_and_counts_b.y;
return ClusterableObjectIndexRanges(
point_light_offset,
spot_light_offset,
reflection_probe_offset,
irradiance_volume_offset,
decal_offset,
last_clusterable_offset
);
#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3
let raw_offset_and_counts = bindings::cluster_offsets_and_counts.data[cluster_index >> 2u][cluster_index & ((1u << 2u) - 1u)];
// [ 31 .. 18 | 17 .. 9 | 8 .. 0 ]
// [ offset | point light count | spot light count ]
let offset_and_counts = vec3<u32>(
(raw_offset_and_counts >> (CLUSTER_COUNT_SIZE * 2u)) & ((1u << (32u - (CLUSTER_COUNT_SIZE * 2u))) - 1u),
(raw_offset_and_counts >> CLUSTER_COUNT_SIZE) & ((1u << CLUSTER_COUNT_SIZE) - 1u),
raw_offset_and_counts & ((1u << CLUSTER_COUNT_SIZE) - 1u),
);
// We don't cluster reflection probes or irradiance volumes on this
// platform, as there's no room in the UBO. Thus, those offset ranges
// (corresponding to `offset_d` and `offset_e` above) are empty and are
// simply copies of `offset_c`.
let offset_a = offset_and_counts.x;
let offset_b = offset_a + offset_and_counts.y;
let offset_c = offset_b + offset_and_counts.z;
return ClusterableObjectIndexRanges(offset_a, offset_b, offset_c, offset_c, offset_c, offset_c);
#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3
}
// Returns the index of the clusterable object at the given offset.
//
// Note that, in the case of a light probe, the index refers to an element in
// one of the two `light_probes` sublists, not the `clusterable_objects` list.
fn get_clusterable_object_id(index: u32) -> u32 {
#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3
return bindings::clusterable_object_index_lists.data[index];
#else
// The index is correct but in clusterable_object_index_lists we pack 4 u8s into a u32
// This means the index into clusterable_object_index_lists is index / 4
let indices = bindings::clusterable_object_index_lists.data[index >> 4u][(index >> 2u) &
((1u << 2u) - 1u)];
// And index % 4 gives the sub-index of the u8 within the u32 so we shift by 8 * sub-index
return (indices >> (8u * (index & ((1u << 2u) - 1u)))) & ((1u << 8u) - 1u);
#endif
}
fn cluster_debug_visualization(
input_color: vec4<f32>,
view_z: f32,
is_orthographic: bool,
clusterable_object_index_ranges: ClusterableObjectIndexRanges,
cluster_index: u32,
) -> vec4<f32> {
var output_color = input_color;
// Cluster allocation debug (using 'over' alpha blending)
#ifdef CLUSTERED_FORWARD_DEBUG_Z_SLICES
// NOTE: This debug mode visualizes the z-slices
let cluster_overlay_alpha = 0.1;
var z_slice: u32 = view_z_to_z_slice(view_z, is_orthographic);
// A hack to make the colors alternate a bit more
if (z_slice & 1u) == 1u {
z_slice = z_slice + bindings::lights.cluster_dimensions.z / 2u;
}
let slice_color_hsv = vec3(
f32(z_slice) / f32(bindings::lights.cluster_dimensions.z + 1u) * PI_2,
1.0,
0.5
);
let slice_color = hsv_to_rgb(slice_color_hsv);
output_color = vec4<f32>(
(1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * slice_color,
output_color.a
);
#endif // CLUSTERED_FORWARD_DEBUG_Z_SLICES
#ifdef CLUSTERED_FORWARD_DEBUG_CLUSTER_COMPLEXITY
// NOTE: This debug mode visualizes the number of clusterable objects within
// the cluster that contains the fragment. It shows a sort of cluster
// complexity measure.
let cluster_overlay_alpha = 0.1;
let max_complexity_per_cluster = 64.0;
let object_count = clusterable_object_index_ranges.first_reflection_probe_index_offset -
clusterable_object_index_ranges.first_point_light_index_offset;
output_color.r = (1.0 - cluster_overlay_alpha) * output_color.r + cluster_overlay_alpha *
smoothstep(0.0, max_complexity_per_cluster, f32(object_count));
output_color.g = (1.0 - cluster_overlay_alpha) * output_color.g + cluster_overlay_alpha *
(1.0 - smoothstep(0.0, max_complexity_per_cluster, f32(object_count)));
#endif // CLUSTERED_FORWARD_DEBUG_CLUSTER_COMPLEXITY
#ifdef CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY
// NOTE: Visualizes the cluster to which the fragment belongs
let cluster_overlay_alpha = 0.1;
var rng = cluster_index;
let cluster_color_hsv = vec3(rand_f(&rng) * PI_2, 1.0, 0.5);
let cluster_color = hsv_to_rgb(cluster_color_hsv);
output_color = vec4<f32>(
(1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * cluster_color,
output_color.a
);
#endif // CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY
return output_color;
}

148
vendor/bevy_pbr/src/render/fog.rs vendored Normal file
View File

@@ -0,0 +1,148 @@
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_color::{ColorToComponents, LinearRgba};
use bevy_ecs::prelude::*;
use bevy_math::{Vec3, Vec4};
use bevy_render::{
extract_component::ExtractComponentPlugin,
render_resource::{DynamicUniformBuffer, Shader, ShaderType},
renderer::{RenderDevice, RenderQueue},
view::ExtractedView,
Render, RenderApp, RenderSet,
};
use crate::{DistanceFog, FogFalloff};
/// The GPU-side representation of the fog configuration that's sent as a uniform to the shader
#[derive(Copy, Clone, ShaderType, Default, Debug)]
pub struct GpuFog {
/// Fog color
base_color: Vec4,
/// The color used for the fog where the view direction aligns with directional lights
directional_light_color: Vec4,
/// Allocated differently depending on fog mode.
/// See `mesh_view_types.wgsl` for a detailed explanation
be: Vec3,
/// The exponent applied to the directional light alignment calculation
directional_light_exponent: f32,
/// Allocated differently depending on fog mode.
/// See `mesh_view_types.wgsl` for a detailed explanation
bi: Vec3,
/// Unsigned int representation of the active fog falloff mode
mode: u32,
}
// Important: These must be kept in sync with `mesh_view_types.wgsl`
const GPU_FOG_MODE_OFF: u32 = 0;
const GPU_FOG_MODE_LINEAR: u32 = 1;
const GPU_FOG_MODE_EXPONENTIAL: u32 = 2;
const GPU_FOG_MODE_EXPONENTIAL_SQUARED: u32 = 3;
const GPU_FOG_MODE_ATMOSPHERIC: u32 = 4;
/// Metadata for fog
#[derive(Default, Resource)]
pub struct FogMeta {
pub gpu_fogs: DynamicUniformBuffer<GpuFog>,
}
/// Prepares fog metadata and writes the fog-related uniform buffers to the GPU
pub fn prepare_fog(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut fog_meta: ResMut<FogMeta>,
views: Query<(Entity, Option<&DistanceFog>), With<ExtractedView>>,
) {
let views_iter = views.iter();
let view_count = views_iter.len();
let Some(mut writer) = fog_meta
.gpu_fogs
.get_writer(view_count, &render_device, &render_queue)
else {
return;
};
for (entity, fog) in views_iter {
let gpu_fog = if let Some(fog) = fog {
match &fog.falloff {
FogFalloff::Linear { start, end } => GpuFog {
mode: GPU_FOG_MODE_LINEAR,
base_color: LinearRgba::from(fog.color).to_vec4(),
directional_light_color: LinearRgba::from(fog.directional_light_color)
.to_vec4(),
directional_light_exponent: fog.directional_light_exponent,
be: Vec3::new(*start, *end, 0.0),
..Default::default()
},
FogFalloff::Exponential { density } => GpuFog {
mode: GPU_FOG_MODE_EXPONENTIAL,
base_color: LinearRgba::from(fog.color).to_vec4(),
directional_light_color: LinearRgba::from(fog.directional_light_color)
.to_vec4(),
directional_light_exponent: fog.directional_light_exponent,
be: Vec3::new(*density, 0.0, 0.0),
..Default::default()
},
FogFalloff::ExponentialSquared { density } => GpuFog {
mode: GPU_FOG_MODE_EXPONENTIAL_SQUARED,
base_color: LinearRgba::from(fog.color).to_vec4(),
directional_light_color: LinearRgba::from(fog.directional_light_color)
.to_vec4(),
directional_light_exponent: fog.directional_light_exponent,
be: Vec3::new(*density, 0.0, 0.0),
..Default::default()
},
FogFalloff::Atmospheric {
extinction,
inscattering,
} => GpuFog {
mode: GPU_FOG_MODE_ATMOSPHERIC,
base_color: LinearRgba::from(fog.color).to_vec4(),
directional_light_color: LinearRgba::from(fog.directional_light_color)
.to_vec4(),
directional_light_exponent: fog.directional_light_exponent,
be: *extinction,
bi: *inscattering,
},
}
} else {
// If no fog is added to a camera, by default it's off
GpuFog {
mode: GPU_FOG_MODE_OFF,
..Default::default()
}
};
// This is later read by `SetMeshViewBindGroup<I>`
commands.entity(entity).insert(ViewFogUniformOffset {
offset: writer.write(&gpu_fog),
});
}
}
/// Inserted on each `Entity` with an `ExtractedView` to keep track of its offset
/// in the `gpu_fogs` `DynamicUniformBuffer` within `FogMeta`
#[derive(Component)]
pub struct ViewFogUniformOffset {
pub offset: u32,
}
/// Handle for the fog WGSL Shader internal asset
pub const FOG_SHADER_HANDLE: Handle<Shader> = weak_handle!("e943f446-2856-471c-af5e-68dd276eec42");
/// A plugin that consolidates fog extraction, preparation and related resources/assets
pub struct FogPlugin;
impl Plugin for FogPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, FOG_SHADER_HANDLE, "fog.wgsl", Shader::from_wgsl);
app.register_type::<DistanceFog>();
app.add_plugins(ExtractComponentPlugin::<DistanceFog>::default());
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<FogMeta>()
.add_systems(Render, prepare_fog.in_set(RenderSet::PrepareResources));
}
}
}

79
vendor/bevy_pbr/src/render/fog.wgsl vendored Normal file
View File

@@ -0,0 +1,79 @@
#define_import_path bevy_pbr::fog
#import bevy_pbr::{
mesh_view_bindings::fog,
mesh_view_types::Fog,
}
// Fog formulas adapted from:
// https://learn.microsoft.com/en-us/windows/win32/direct3d9/fog-formulas
// https://catlikecoding.com/unity/tutorials/rendering/part-14/
// https://iquilezles.org/articles/fog/ (Atmospheric Fog and Scattering)
fn scattering_adjusted_fog_color(
fog_params: Fog,
scattering: vec3<f32>,
) -> vec4<f32> {
if (fog_params.directional_light_color.a > 0.0) {
return vec4<f32>(
fog_params.base_color.rgb
+ scattering * fog_params.directional_light_color.rgb * fog_params.directional_light_color.a,
fog_params.base_color.a,
);
} else {
return fog_params.base_color;
}
}
fn linear_fog(
fog_params: Fog,
input_color: vec4<f32>,
distance: f32,
scattering: vec3<f32>,
) -> vec4<f32> {
var fog_color = scattering_adjusted_fog_color(fog_params, scattering);
let start = fog_params.be.x;
let end = fog_params.be.y;
fog_color.a *= 1.0 - clamp((end - distance) / (end - start), 0.0, 1.0);
return vec4<f32>(mix(input_color.rgb, fog_color.rgb, fog_color.a), input_color.a);
}
fn exponential_fog(
fog_params: Fog,
input_color: vec4<f32>,
distance: f32,
scattering: vec3<f32>,
) -> vec4<f32> {
var fog_color = scattering_adjusted_fog_color(fog_params, scattering);
let density = fog_params.be.x;
fog_color.a *= 1.0 - 1.0 / exp(distance * density);
return vec4<f32>(mix(input_color.rgb, fog_color.rgb, fog_color.a), input_color.a);
}
fn exponential_squared_fog(
fog_params: Fog,
input_color: vec4<f32>,
distance: f32,
scattering: vec3<f32>,
) -> vec4<f32> {
var fog_color = scattering_adjusted_fog_color(fog_params, scattering);
let distance_times_density = distance * fog_params.be.x;
fog_color.a *= 1.0 - 1.0 / exp(distance_times_density * distance_times_density);
return vec4<f32>(mix(input_color.rgb, fog_color.rgb, fog_color.a), input_color.a);
}
fn atmospheric_fog(
fog_params: Fog,
input_color: vec4<f32>,
distance: f32,
scattering: vec3<f32>,
) -> vec4<f32> {
var fog_color = scattering_adjusted_fog_color(fog_params, scattering);
let extinction_factor = 1.0 - 1.0 / exp(distance * fog_params.be);
let inscattering_factor = 1.0 - 1.0 / exp(distance * fog_params.bi);
return vec4<f32>(
input_color.rgb * (1.0 - extinction_factor * fog_color.a)
+ fog_color.rgb * inscattering_factor * fog_color.a,
input_color.a
);
}

View File

@@ -0,0 +1,60 @@
#define_import_path bevy_pbr::forward_io
struct Vertex {
@builtin(instance_index) instance_index: u32,
#ifdef VERTEX_POSITIONS
@location(0) position: vec3<f32>,
#endif
#ifdef VERTEX_NORMALS
@location(1) normal: vec3<f32>,
#endif
#ifdef VERTEX_UVS_A
@location(2) uv: vec2<f32>,
#endif
#ifdef VERTEX_UVS_B
@location(3) uv_b: vec2<f32>,
#endif
#ifdef VERTEX_TANGENTS
@location(4) tangent: vec4<f32>,
#endif
#ifdef VERTEX_COLORS
@location(5) color: vec4<f32>,
#endif
#ifdef SKINNED
@location(6) joint_indices: vec4<u32>,
@location(7) joint_weights: vec4<f32>,
#endif
#ifdef MORPH_TARGETS
@builtin(vertex_index) index: u32,
#endif
};
struct VertexOutput {
// This is `clip position` when the struct is used as a vertex stage output
// and `frag coord` when used as a fragment stage input
@builtin(position) position: vec4<f32>,
@location(0) world_position: vec4<f32>,
@location(1) world_normal: vec3<f32>,
#ifdef VERTEX_UVS_A
@location(2) uv: vec2<f32>,
#endif
#ifdef VERTEX_UVS_B
@location(3) uv_b: vec2<f32>,
#endif
#ifdef VERTEX_TANGENTS
@location(4) world_tangent: vec4<f32>,
#endif
#ifdef VERTEX_COLORS
@location(5) color: vec4<f32>,
#endif
#ifdef VERTEX_OUTPUT_INSTANCE_INDEX
@location(6) @interpolate(flat) instance_index: u32,
#endif
#ifdef VISIBILITY_RANGE_DITHER
@location(7) @interpolate(flat) visibility_range_dither: i32,
#endif
}
struct FragmentOutput {
@location(0) color: vec4<f32>,
}

File diff suppressed because it is too large Load Diff

2386
vendor/bevy_pbr/src/render/light.rs vendored Normal file

File diff suppressed because it is too large Load Diff

3279
vendor/bevy_pbr/src/render/mesh.rs vendored Normal file

File diff suppressed because it is too large Load Diff

120
vendor/bevy_pbr/src/render/mesh.wgsl vendored Normal file
View File

@@ -0,0 +1,120 @@
#import bevy_pbr::{
mesh_bindings::mesh,
mesh_functions,
skinning,
morph::morph,
forward_io::{Vertex, VertexOutput},
view_transformations::position_world_to_clip,
}
#ifdef MORPH_TARGETS
fn morph_vertex(vertex_in: Vertex) -> Vertex {
var vertex = vertex_in;
let first_vertex = mesh[vertex.instance_index].first_vertex_index;
let vertex_index = vertex.index - first_vertex;
let weight_count = bevy_pbr::morph::layer_count();
for (var i: u32 = 0u; i < weight_count; i ++) {
let weight = bevy_pbr::morph::weight_at(i);
if weight == 0.0 {
continue;
}
vertex.position += weight * morph(vertex_index, bevy_pbr::morph::position_offset, i);
#ifdef VERTEX_NORMALS
vertex.normal += weight * morph(vertex_index, bevy_pbr::morph::normal_offset, i);
#endif
#ifdef VERTEX_TANGENTS
vertex.tangent += vec4(weight * morph(vertex_index, bevy_pbr::morph::tangent_offset, i), 0.0);
#endif
}
return vertex;
}
#endif
@vertex
fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
var out: VertexOutput;
#ifdef MORPH_TARGETS
var vertex = morph_vertex(vertex_no_morph);
#else
var vertex = vertex_no_morph;
#endif
let mesh_world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index);
#ifdef SKINNED
var world_from_local = skinning::skin_model(
vertex.joint_indices,
vertex.joint_weights,
vertex_no_morph.instance_index
);
#else
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416 .
var world_from_local = mesh_world_from_local;
#endif
#ifdef VERTEX_NORMALS
#ifdef SKINNED
out.world_normal = skinning::skin_normals(world_from_local, vertex.normal);
#else
out.world_normal = mesh_functions::mesh_normal_local_to_world(
vertex.normal,
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
vertex_no_morph.instance_index
);
#endif
#endif
#ifdef VERTEX_POSITIONS
out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4<f32>(vertex.position, 1.0));
out.position = position_world_to_clip(out.world_position.xyz);
#endif
#ifdef VERTEX_UVS_A
out.uv = vertex.uv;
#endif
#ifdef VERTEX_UVS_B
out.uv_b = vertex.uv_b;
#endif
#ifdef VERTEX_TANGENTS
out.world_tangent = mesh_functions::mesh_tangent_local_to_world(
world_from_local,
vertex.tangent,
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
vertex_no_morph.instance_index
);
#endif
#ifdef VERTEX_COLORS
out.color = vertex.color;
#endif
#ifdef VERTEX_OUTPUT_INSTANCE_INDEX
// Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug.
// See https://github.com/gfx-rs/naga/issues/2416
out.instance_index = vertex_no_morph.instance_index;
#endif
#ifdef VISIBILITY_RANGE_DITHER
out.visibility_range_dither = mesh_functions::get_visibility_range_dither_level(
vertex_no_morph.instance_index, mesh_world_from_local[3]);
#endif
return out;
}
@fragment
fn fragment(
mesh: VertexOutput,
) -> @location(0) vec4<f32> {
#ifdef VERTEX_COLORS
return mesh.color;
#else
return vec4<f32>(1.0, 0.0, 1.0, 1.0);
#endif
}

View File

@@ -0,0 +1,548 @@
//! Bind group layout related definitions for the mesh pipeline.
use bevy_math::Mat4;
use bevy_render::{
mesh::morph::MAX_MORPH_WEIGHTS,
render_resource::*,
renderer::{RenderAdapter, RenderDevice},
};
use crate::{binding_arrays_are_usable, render::skin::MAX_JOINTS, LightmapSlab};
const MORPH_WEIGHT_SIZE: usize = size_of::<f32>();
/// This is used to allocate buffers.
/// The correctness of the value depends on the GPU/platform.
/// The current value is chosen because it is guaranteed to work everywhere.
/// To allow for bigger values, a check must be made for the limits
/// of the GPU at runtime, which would mean not using consts anymore.
pub const MORPH_BUFFER_SIZE: usize = MAX_MORPH_WEIGHTS * MORPH_WEIGHT_SIZE;
const JOINT_SIZE: usize = size_of::<Mat4>();
pub(crate) const JOINT_BUFFER_SIZE: usize = MAX_JOINTS * JOINT_SIZE;
/// Individual layout entries.
mod layout_entry {
use core::num::NonZeroU32;
use super::{JOINT_BUFFER_SIZE, MORPH_BUFFER_SIZE};
use crate::{render::skin, MeshUniform, LIGHTMAPS_PER_SLAB};
use bevy_render::{
render_resource::{
binding_types::{
sampler, storage_buffer_read_only_sized, texture_2d, texture_3d,
uniform_buffer_sized,
},
BindGroupLayoutEntryBuilder, BufferSize, GpuArrayBuffer, SamplerBindingType,
ShaderStages, TextureSampleType,
},
renderer::RenderDevice,
};
pub(super) fn model(render_device: &RenderDevice) -> BindGroupLayoutEntryBuilder {
GpuArrayBuffer::<MeshUniform>::binding_layout(render_device)
.visibility(ShaderStages::VERTEX_FRAGMENT)
}
pub(super) fn skinning(render_device: &RenderDevice) -> BindGroupLayoutEntryBuilder {
// If we can use storage buffers, do so. Otherwise, fall back to uniform
// buffers.
let size = BufferSize::new(JOINT_BUFFER_SIZE as u64);
if skin::skins_use_uniform_buffers(render_device) {
uniform_buffer_sized(true, size)
} else {
storage_buffer_read_only_sized(false, size)
}
}
pub(super) fn weights() -> BindGroupLayoutEntryBuilder {
uniform_buffer_sized(true, BufferSize::new(MORPH_BUFFER_SIZE as u64))
}
pub(super) fn targets() -> BindGroupLayoutEntryBuilder {
texture_3d(TextureSampleType::Float { filterable: false })
}
pub(super) fn lightmaps_texture_view() -> BindGroupLayoutEntryBuilder {
texture_2d(TextureSampleType::Float { filterable: true }).visibility(ShaderStages::FRAGMENT)
}
pub(super) fn lightmaps_sampler() -> BindGroupLayoutEntryBuilder {
sampler(SamplerBindingType::Filtering).visibility(ShaderStages::FRAGMENT)
}
pub(super) fn lightmaps_texture_view_array() -> BindGroupLayoutEntryBuilder {
texture_2d(TextureSampleType::Float { filterable: true })
.visibility(ShaderStages::FRAGMENT)
.count(NonZeroU32::new(LIGHTMAPS_PER_SLAB as u32).unwrap())
}
pub(super) fn lightmaps_sampler_array() -> BindGroupLayoutEntryBuilder {
sampler(SamplerBindingType::Filtering)
.visibility(ShaderStages::FRAGMENT)
.count(NonZeroU32::new(LIGHTMAPS_PER_SLAB as u32).unwrap())
}
}
/// Individual [`BindGroupEntry`]
/// for bind groups.
mod entry {
use crate::render::skin;
use super::{JOINT_BUFFER_SIZE, MORPH_BUFFER_SIZE};
use bevy_render::{
render_resource::{
BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, Sampler,
TextureView, WgpuSampler, WgpuTextureView,
},
renderer::RenderDevice,
};
fn entry(binding: u32, size: Option<u64>, buffer: &Buffer) -> BindGroupEntry {
BindGroupEntry {
binding,
resource: BindingResource::Buffer(BufferBinding {
buffer,
offset: 0,
size: size.map(|size| BufferSize::new(size).unwrap()),
}),
}
}
pub(super) fn model(binding: u32, resource: BindingResource) -> BindGroupEntry {
BindGroupEntry { binding, resource }
}
pub(super) fn skinning<'a>(
render_device: &RenderDevice,
binding: u32,
buffer: &'a Buffer,
) -> BindGroupEntry<'a> {
let size = if skin::skins_use_uniform_buffers(render_device) {
Some(JOINT_BUFFER_SIZE as u64)
} else {
None
};
entry(binding, size, buffer)
}
pub(super) fn weights(binding: u32, buffer: &Buffer) -> BindGroupEntry {
entry(binding, Some(MORPH_BUFFER_SIZE as u64), buffer)
}
pub(super) fn targets(binding: u32, texture: &TextureView) -> BindGroupEntry {
BindGroupEntry {
binding,
resource: BindingResource::TextureView(texture),
}
}
pub(super) fn lightmaps_texture_view(binding: u32, texture: &TextureView) -> BindGroupEntry {
BindGroupEntry {
binding,
resource: BindingResource::TextureView(texture),
}
}
pub(super) fn lightmaps_sampler(binding: u32, sampler: &Sampler) -> BindGroupEntry {
BindGroupEntry {
binding,
resource: BindingResource::Sampler(sampler),
}
}
pub(super) fn lightmaps_texture_view_array<'a>(
binding: u32,
textures: &'a [&'a WgpuTextureView],
) -> BindGroupEntry<'a> {
BindGroupEntry {
binding,
resource: BindingResource::TextureViewArray(textures),
}
}
pub(super) fn lightmaps_sampler_array<'a>(
binding: u32,
samplers: &'a [&'a WgpuSampler],
) -> BindGroupEntry<'a> {
BindGroupEntry {
binding,
resource: BindingResource::SamplerArray(samplers),
}
}
}
/// All possible [`BindGroupLayout`]s in bevy's default mesh shader (`mesh.wgsl`).
#[derive(Clone)]
pub struct MeshLayouts {
/// The mesh model uniform (transform) and nothing else.
pub model_only: BindGroupLayout,
/// Includes the lightmap texture and uniform.
pub lightmapped: BindGroupLayout,
/// Also includes the uniform for skinning
pub skinned: BindGroupLayout,
/// Like [`MeshLayouts::skinned`], but includes slots for the previous
/// frame's joint matrices, so that we can compute motion vectors.
pub skinned_motion: BindGroupLayout,
/// Also includes the uniform and [`MorphAttributes`] for morph targets.
///
/// [`MorphAttributes`]: bevy_render::mesh::morph::MorphAttributes
pub morphed: BindGroupLayout,
/// Like [`MeshLayouts::morphed`], but includes a slot for the previous
/// frame's morph weights, so that we can compute motion vectors.
pub morphed_motion: BindGroupLayout,
/// Also includes both uniforms for skinning and morph targets, also the
/// morph target [`MorphAttributes`] binding.
///
/// [`MorphAttributes`]: bevy_render::mesh::morph::MorphAttributes
pub morphed_skinned: BindGroupLayout,
/// Like [`MeshLayouts::morphed_skinned`], but includes slots for the
/// previous frame's joint matrices and morph weights, so that we can
/// compute motion vectors.
pub morphed_skinned_motion: BindGroupLayout,
}
impl MeshLayouts {
/// Prepare the layouts used by the default bevy [`Mesh`].
///
/// [`Mesh`]: bevy_render::prelude::Mesh
pub fn new(render_device: &RenderDevice, render_adapter: &RenderAdapter) -> Self {
MeshLayouts {
model_only: Self::model_only_layout(render_device),
lightmapped: Self::lightmapped_layout(render_device, render_adapter),
skinned: Self::skinned_layout(render_device),
skinned_motion: Self::skinned_motion_layout(render_device),
morphed: Self::morphed_layout(render_device),
morphed_motion: Self::morphed_motion_layout(render_device),
morphed_skinned: Self::morphed_skinned_layout(render_device),
morphed_skinned_motion: Self::morphed_skinned_motion_layout(render_device),
}
}
// ---------- create individual BindGroupLayouts ----------
fn model_only_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(
"mesh_layout",
&BindGroupLayoutEntries::single(
ShaderStages::empty(),
layout_entry::model(render_device),
),
)
}
/// Creates the layout for skinned meshes.
fn skinned_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(
"skinned_mesh_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
// The current frame's joint matrix buffer.
(1, layout_entry::skinning(render_device)),
),
),
)
}
/// Creates the layout for skinned meshes with the infrastructure to compute
/// motion vectors.
fn skinned_motion_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(
"skinned_motion_mesh_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
// The current frame's joint matrix buffer.
(1, layout_entry::skinning(render_device)),
// The previous frame's joint matrix buffer.
(6, layout_entry::skinning(render_device)),
),
),
)
}
/// Creates the layout for meshes with morph targets.
fn morphed_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(
"morphed_mesh_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
// The current frame's morph weight buffer.
(2, layout_entry::weights()),
(3, layout_entry::targets()),
),
),
)
}
/// Creates the layout for meshes with morph targets and the infrastructure
/// to compute motion vectors.
fn morphed_motion_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(
"morphed_mesh_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
// The current frame's morph weight buffer.
(2, layout_entry::weights()),
(3, layout_entry::targets()),
// The previous frame's morph weight buffer.
(7, layout_entry::weights()),
),
),
)
}
/// Creates the bind group layout for meshes with both skins and morph
/// targets.
fn morphed_skinned_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(
"morphed_skinned_mesh_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
// The current frame's joint matrix buffer.
(1, layout_entry::skinning(render_device)),
// The current frame's morph weight buffer.
(2, layout_entry::weights()),
(3, layout_entry::targets()),
),
),
)
}
/// Creates the bind group layout for meshes with both skins and morph
/// targets, in addition to the infrastructure to compute motion vectors.
fn morphed_skinned_motion_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(
"morphed_skinned_motion_mesh_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
// The current frame's joint matrix buffer.
(1, layout_entry::skinning(render_device)),
// The current frame's morph weight buffer.
(2, layout_entry::weights()),
(3, layout_entry::targets()),
// The previous frame's joint matrix buffer.
(6, layout_entry::skinning(render_device)),
// The previous frame's morph weight buffer.
(7, layout_entry::weights()),
),
),
)
}
fn lightmapped_layout(
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
) -> BindGroupLayout {
if binding_arrays_are_usable(render_device, render_adapter) {
render_device.create_bind_group_layout(
"lightmapped_mesh_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
(4, layout_entry::lightmaps_texture_view_array()),
(5, layout_entry::lightmaps_sampler_array()),
),
),
)
} else {
render_device.create_bind_group_layout(
"lightmapped_mesh_layout",
&BindGroupLayoutEntries::with_indices(
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
(4, layout_entry::lightmaps_texture_view()),
(5, layout_entry::lightmaps_sampler()),
),
),
)
}
}
// ---------- BindGroup methods ----------
pub fn model_only(&self, render_device: &RenderDevice, model: &BindingResource) -> BindGroup {
render_device.create_bind_group(
"model_only_mesh_bind_group",
&self.model_only,
&[entry::model(0, model.clone())],
)
}
pub fn lightmapped(
&self,
render_device: &RenderDevice,
model: &BindingResource,
lightmap_slab: &LightmapSlab,
bindless_lightmaps: bool,
) -> BindGroup {
if bindless_lightmaps {
let (texture_views, samplers) = lightmap_slab.build_binding_arrays();
render_device.create_bind_group(
"lightmapped_mesh_bind_group",
&self.lightmapped,
&[
entry::model(0, model.clone()),
entry::lightmaps_texture_view_array(4, &texture_views),
entry::lightmaps_sampler_array(5, &samplers),
],
)
} else {
let (texture_view, sampler) = lightmap_slab.bindings_for_first_lightmap();
render_device.create_bind_group(
"lightmapped_mesh_bind_group",
&self.lightmapped,
&[
entry::model(0, model.clone()),
entry::lightmaps_texture_view(4, texture_view),
entry::lightmaps_sampler(5, sampler),
],
)
}
}
/// Creates the bind group for skinned meshes with no morph targets.
pub fn skinned(
&self,
render_device: &RenderDevice,
model: &BindingResource,
current_skin: &Buffer,
) -> BindGroup {
render_device.create_bind_group(
"skinned_mesh_bind_group",
&self.skinned,
&[
entry::model(0, model.clone()),
entry::skinning(render_device, 1, current_skin),
],
)
}
/// Creates the bind group for skinned meshes with no morph targets, with
/// the infrastructure to compute motion vectors.
///
/// `current_skin` is the buffer of joint matrices for this frame;
/// `prev_skin` is the buffer for the previous frame. The latter is used for
/// motion vector computation. If there is no such applicable buffer,
/// `current_skin` and `prev_skin` will reference the same buffer.
pub fn skinned_motion(
&self,
render_device: &RenderDevice,
model: &BindingResource,
current_skin: &Buffer,
prev_skin: &Buffer,
) -> BindGroup {
render_device.create_bind_group(
"skinned_motion_mesh_bind_group",
&self.skinned_motion,
&[
entry::model(0, model.clone()),
entry::skinning(render_device, 1, current_skin),
entry::skinning(render_device, 6, prev_skin),
],
)
}
/// Creates the bind group for meshes with no skins but morph targets.
pub fn morphed(
&self,
render_device: &RenderDevice,
model: &BindingResource,
current_weights: &Buffer,
targets: &TextureView,
) -> BindGroup {
render_device.create_bind_group(
"morphed_mesh_bind_group",
&self.morphed,
&[
entry::model(0, model.clone()),
entry::weights(2, current_weights),
entry::targets(3, targets),
],
)
}
/// Creates the bind group for meshes with no skins but morph targets, in
/// addition to the infrastructure to compute motion vectors.
///
/// `current_weights` is the buffer of morph weights for this frame;
/// `prev_weights` is the buffer for the previous frame. The latter is used
/// for motion vector computation. If there is no such applicable buffer,
/// `current_weights` and `prev_weights` will reference the same buffer.
pub fn morphed_motion(
&self,
render_device: &RenderDevice,
model: &BindingResource,
current_weights: &Buffer,
targets: &TextureView,
prev_weights: &Buffer,
) -> BindGroup {
render_device.create_bind_group(
"morphed_motion_mesh_bind_group",
&self.morphed_motion,
&[
entry::model(0, model.clone()),
entry::weights(2, current_weights),
entry::targets(3, targets),
entry::weights(7, prev_weights),
],
)
}
/// Creates the bind group for meshes with skins and morph targets.
pub fn morphed_skinned(
&self,
render_device: &RenderDevice,
model: &BindingResource,
current_skin: &Buffer,
current_weights: &Buffer,
targets: &TextureView,
) -> BindGroup {
render_device.create_bind_group(
"morphed_skinned_mesh_bind_group",
&self.morphed_skinned,
&[
entry::model(0, model.clone()),
entry::skinning(render_device, 1, current_skin),
entry::weights(2, current_weights),
entry::targets(3, targets),
],
)
}
/// Creates the bind group for meshes with skins and morph targets, in
/// addition to the infrastructure to compute motion vectors.
///
/// See the documentation for [`MeshLayouts::skinned_motion`] and
/// [`MeshLayouts::morphed_motion`] above for more information about the
/// `current_skin`, `prev_skin`, `current_weights`, and `prev_weights`
/// buffers.
pub fn morphed_skinned_motion(
&self,
render_device: &RenderDevice,
model: &BindingResource,
current_skin: &Buffer,
current_weights: &Buffer,
targets: &TextureView,
prev_skin: &Buffer,
prev_weights: &Buffer,
) -> BindGroup {
render_device.create_bind_group(
"morphed_skinned_motion_mesh_bind_group",
&self.morphed_skinned_motion,
&[
entry::model(0, model.clone()),
entry::skinning(render_device, 1, current_skin),
entry::weights(2, current_weights),
entry::targets(3, targets),
entry::skinning(render_device, 6, prev_skin),
entry::weights(7, prev_weights),
],
)
}
}

View File

@@ -0,0 +1,11 @@
#define_import_path bevy_pbr::mesh_bindings
#import bevy_pbr::mesh_types::Mesh
#ifndef MESHLET_MESH_MATERIAL_PASS
#ifdef PER_OBJECT_BUFFER_BATCH_SIZE
@group(1) @binding(0) var<uniform> mesh: array<Mesh, #{PER_OBJECT_BUFFER_BATCH_SIZE}u>;
#else
@group(1) @binding(0) var<storage> mesh: array<Mesh>;
#endif // PER_OBJECT_BUFFER_BATCH_SIZE
#endif // MESHLET_MESH_MATERIAL_PASS

View File

@@ -0,0 +1,168 @@
#define_import_path bevy_pbr::mesh_functions
#import bevy_pbr::{
mesh_view_bindings::{
view,
visibility_ranges,
VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE
},
mesh_bindings::mesh,
mesh_types::MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT,
view_transformations::position_world_to_clip,
}
#import bevy_render::maths::{affine3_to_square, mat2x4_f32_to_mat3x3_unpack}
#ifndef MESHLET_MESH_MATERIAL_PASS
fn get_world_from_local(instance_index: u32) -> mat4x4<f32> {
return affine3_to_square(mesh[instance_index].world_from_local);
}
fn get_previous_world_from_local(instance_index: u32) -> mat4x4<f32> {
return affine3_to_square(mesh[instance_index].previous_world_from_local);
}
fn get_local_from_world(instance_index: u32) -> mat4x4<f32> {
// the model matrix is translation * rotation * scale
// the inverse is then scale^-1 * rotation ^-1 * translation^-1
// the 3x3 matrix only contains the information for the rotation and scale
let inverse_model_3x3 = transpose(mat2x4_f32_to_mat3x3_unpack(
mesh[instance_index].local_from_world_transpose_a,
mesh[instance_index].local_from_world_transpose_b,
));
// construct scale^-1 * rotation^-1 from the 3x3
let inverse_model_4x4_no_trans = mat4x4<f32>(
vec4(inverse_model_3x3[0], 0.0),
vec4(inverse_model_3x3[1], 0.0),
vec4(inverse_model_3x3[2], 0.0),
vec4(0.0,0.0,0.0,1.0)
);
// we can get translation^-1 by negating the translation of the model
let model = get_world_from_local(instance_index);
let inverse_model_4x4_only_trans = mat4x4<f32>(
vec4(1.0,0.0,0.0,0.0),
vec4(0.0,1.0,0.0,0.0),
vec4(0.0,0.0,1.0,0.0),
vec4(-model[3].xyz, 1.0)
);
return inverse_model_4x4_no_trans * inverse_model_4x4_only_trans;
}
#endif // MESHLET_MESH_MATERIAL_PASS
fn mesh_position_local_to_world(world_from_local: mat4x4<f32>, vertex_position: vec4<f32>) -> vec4<f32> {
return world_from_local * vertex_position;
}
// NOTE: The intermediate world_position assignment is important
// for precision purposes when using the 'equals' depth comparison
// function.
fn mesh_position_local_to_clip(world_from_local: mat4x4<f32>, vertex_position: vec4<f32>) -> vec4<f32> {
let world_position = mesh_position_local_to_world(world_from_local, vertex_position);
return position_world_to_clip(world_position.xyz);
}
#ifndef MESHLET_MESH_MATERIAL_PASS
fn mesh_normal_local_to_world(vertex_normal: vec3<f32>, instance_index: u32) -> vec3<f32> {
// NOTE: The mikktspace method of normal mapping requires that the world normal is
// re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents
// and normal maps so that the exact inverse process is applied when shading. Blender, Unity,
// Unreal Engine, Godot, and more all use the mikktspace method.
// We only skip normalization for invalid normals so that they don't become NaN.
// Do not change this code unless you really know what you are doing.
// http://www.mikktspace.com/
if any(vertex_normal != vec3<f32>(0.0)) {
return normalize(
mat2x4_f32_to_mat3x3_unpack(
mesh[instance_index].local_from_world_transpose_a,
mesh[instance_index].local_from_world_transpose_b,
) * vertex_normal
);
} else {
return vertex_normal;
}
}
#endif // MESHLET_MESH_MATERIAL_PASS
// Calculates the sign of the determinant of the 3x3 model matrix based on a
// mesh flag
fn sign_determinant_model_3x3m(mesh_flags: u32) -> f32 {
// bool(u32) is false if 0u else true
// f32(bool) is 1.0 if true else 0.0
// * 2.0 - 1.0 remaps 0.0 or 1.0 to -1.0 or 1.0 respectively
return f32(bool(mesh_flags & MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT)) * 2.0 - 1.0;
}
#ifndef MESHLET_MESH_MATERIAL_PASS
fn mesh_tangent_local_to_world(world_from_local: mat4x4<f32>, vertex_tangent: vec4<f32>, instance_index: u32) -> vec4<f32> {
// NOTE: The mikktspace method of normal mapping requires that the world tangent is
// re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents
// and normal maps so that the exact inverse process is applied when shading. Blender, Unity,
// Unreal Engine, Godot, and more all use the mikktspace method.
// We only skip normalization for invalid tangents so that they don't become NaN.
// Do not change this code unless you really know what you are doing.
// http://www.mikktspace.com/
if any(vertex_tangent != vec4<f32>(0.0)) {
return vec4<f32>(
normalize(
mat3x3<f32>(
world_from_local[0].xyz,
world_from_local[1].xyz,
world_from_local[2].xyz,
) * vertex_tangent.xyz
),
// NOTE: Multiplying by the sign of the determinant of the 3x3 model matrix accounts for
// situations such as negative scaling.
vertex_tangent.w * sign_determinant_model_3x3m(mesh[instance_index].flags)
);
} else {
return vertex_tangent;
}
}
#endif // MESHLET_MESH_MATERIAL_PASS
// Returns an appropriate dither level for the current mesh instance.
//
// This looks up the LOD range in the `visibility_ranges` table and compares the
// camera distance to determine the dithering level.
#ifdef VISIBILITY_RANGE_DITHER
fn get_visibility_range_dither_level(instance_index: u32, world_position: vec4<f32>) -> i32 {
#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6
// If we're using a storage buffer, then the length is variable.
let visibility_buffer_array_len = arrayLength(&visibility_ranges);
#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6
// If we're using a uniform buffer, then the length is constant
let visibility_buffer_array_len = VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE;
#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6
let visibility_buffer_index = mesh[instance_index].flags & 0xffffu;
if (visibility_buffer_index > visibility_buffer_array_len) {
return -16;
}
let lod_range = visibility_ranges[visibility_buffer_index];
let camera_distance = length(view.world_position.xyz - world_position.xyz);
// This encodes the following mapping:
//
// `lod_range.` x y z w camera distance
// ←───────┼────────┼────────┼────────┼────────→
// LOD level -16 -16 0 0 16 16 LOD level
let offset = select(-16, 0, camera_distance >= lod_range.z);
let bounds = select(lod_range.xy, lod_range.zw, camera_distance >= lod_range.z);
let level = i32(round((camera_distance - bounds.x) / (bounds.y - bounds.x) * 16.0));
return offset + clamp(level, 0, 16);
}
#endif
#ifndef MESHLET_MESH_MATERIAL_PASS
fn get_tag(instance_index: u32) -> u32 {
return mesh[instance_index].tag;
}
#endif

View File

@@ -0,0 +1,373 @@
// GPU mesh transforming and culling.
//
// This is a compute shader that expands each `MeshInputUniform` out to a full
// `MeshUniform` for each view before rendering. (Thus `MeshInputUniform` and
// `MeshUniform` are in a 1:N relationship.) It runs in parallel for all meshes
// for all views. As part of this process, the shader gathers each mesh's
// transform on the previous frame and writes it into the `MeshUniform` so that
// TAA works. It also performs frustum culling and occlusion culling, if
// requested.
//
// If occlusion culling is on, this shader runs twice: once to prepare the
// meshes that were visible last frame, and once to prepare the meshes that
// weren't visible last frame but became visible this frame. The two invocations
// are known as *early mesh preprocessing* and *late mesh preprocessing*
// respectively.
#import bevy_pbr::mesh_preprocess_types::{
IndirectParametersCpuMetadata, IndirectParametersGpuMetadata, MeshInput
}
#import bevy_pbr::mesh_types::{Mesh, MESH_FLAGS_NO_FRUSTUM_CULLING_BIT}
#import bevy_pbr::mesh_view_bindings::view
#import bevy_pbr::occlusion_culling
#import bevy_pbr::prepass_bindings::previous_view_uniforms
#import bevy_pbr::view_transformations::{
position_world_to_ndc, position_world_to_view, ndc_to_uv, view_z_to_depth_ndc,
position_world_to_prev_ndc, position_world_to_prev_view, prev_view_z_to_depth_ndc
}
#import bevy_render::maths
#import bevy_render::view::View
// Information about each mesh instance needed to cull it on GPU.
//
// At the moment, this just consists of its axis-aligned bounding box (AABB).
struct MeshCullingData {
// The 3D center of the AABB in model space, padded with an extra unused
// float value.
aabb_center: vec4<f32>,
// The 3D extents of the AABB in model space, divided by two, padded with
// an extra unused float value.
aabb_half_extents: vec4<f32>,
}
// One invocation of this compute shader: i.e. one mesh instance in a view.
struct PreprocessWorkItem {
// The index of the `MeshInput` in the `current_input` buffer that we read
// from.
input_index: u32,
// In direct mode, the index of the `Mesh` in `output` that we write to. In
// indirect mode, the index of the `IndirectParameters` in
// `indirect_parameters` that we write to.
output_or_indirect_parameters_index: u32,
}
// The parameters for the indirect compute dispatch for the late mesh
// preprocessing phase.
struct LatePreprocessWorkItemIndirectParameters {
// The number of workgroups we're going to dispatch.
//
// This value should always be equal to `ceil(work_item_count / 64)`.
dispatch_x: atomic<u32>,
// The number of workgroups in the Y direction; always 1.
dispatch_y: u32,
// The number of workgroups in the Z direction; always 1.
dispatch_z: u32,
// The precise number of work items.
work_item_count: atomic<u32>,
// Padding.
//
// This isn't the usual structure padding; it's needed because some hardware
// requires indirect compute dispatch parameters to be aligned on 64-byte
// boundaries.
pad: vec4<u32>,
}
// These have to be in a structure because of Naga limitations on DX12.
struct PushConstants {
// The offset into the `late_preprocess_work_item_indirect_parameters`
// buffer.
late_preprocess_work_item_indirect_offset: u32,
}
// The current frame's `MeshInput`.
@group(0) @binding(3) var<storage> current_input: array<MeshInput>;
// The `MeshInput` values from the previous frame.
@group(0) @binding(4) var<storage> previous_input: array<MeshInput>;
// Indices into the `MeshInput` buffer.
//
// There may be many indices that map to the same `MeshInput`.
@group(0) @binding(5) var<storage> work_items: array<PreprocessWorkItem>;
// The output array of `Mesh`es.
@group(0) @binding(6) var<storage, read_write> output: array<Mesh>;
#ifdef INDIRECT
// The array of indirect parameters for drawcalls.
@group(0) @binding(7) var<storage> indirect_parameters_cpu_metadata:
array<IndirectParametersCpuMetadata>;
@group(0) @binding(8) var<storage, read_write> indirect_parameters_gpu_metadata:
array<IndirectParametersGpuMetadata>;
#endif
#ifdef FRUSTUM_CULLING
// Data needed to cull the meshes.
//
// At the moment, this consists only of AABBs.
@group(0) @binding(9) var<storage> mesh_culling_data: array<MeshCullingData>;
#endif // FRUSTUM_CULLING
#ifdef OCCLUSION_CULLING
@group(0) @binding(10) var depth_pyramid: texture_2d<f32>;
#ifdef EARLY_PHASE
@group(0) @binding(11) var<storage, read_write> late_preprocess_work_items:
array<PreprocessWorkItem>;
#endif // EARLY_PHASE
@group(0) @binding(12) var<storage, read_write> late_preprocess_work_item_indirect_parameters:
array<LatePreprocessWorkItemIndirectParameters>;
var<push_constant> push_constants: PushConstants;
#endif // OCCLUSION_CULLING
#ifdef FRUSTUM_CULLING
// Returns true if the view frustum intersects an oriented bounding box (OBB).
//
// `aabb_center.w` should be 1.0.
fn view_frustum_intersects_obb(
world_from_local: mat4x4<f32>,
aabb_center: vec4<f32>,
aabb_half_extents: vec3<f32>,
) -> bool {
for (var i = 0; i < 5; i += 1) {
// Calculate relative radius of the sphere associated with this plane.
let plane_normal = view.frustum[i];
let relative_radius = dot(
abs(
vec3(
dot(plane_normal.xyz, world_from_local[0].xyz),
dot(plane_normal.xyz, world_from_local[1].xyz),
dot(plane_normal.xyz, world_from_local[2].xyz),
)
),
aabb_half_extents
);
// Check the frustum plane.
if (!maths::sphere_intersects_plane_half_space(
plane_normal, aabb_center, relative_radius)) {
return false;
}
}
return true;
}
#endif
@compute
@workgroup_size(64)
fn main(@builtin(global_invocation_id) global_invocation_id: vec3<u32>) {
// Figure out our instance index. If this thread doesn't correspond to any
// index, bail.
let instance_index = global_invocation_id.x;
#ifdef LATE_PHASE
if (instance_index >= atomicLoad(&late_preprocess_work_item_indirect_parameters[
push_constants.late_preprocess_work_item_indirect_offset].work_item_count)) {
return;
}
#else // LATE_PHASE
if (instance_index >= arrayLength(&work_items)) {
return;
}
#endif
// Unpack the work item.
let input_index = work_items[instance_index].input_index;
#ifdef INDIRECT
let indirect_parameters_index = work_items[instance_index].output_or_indirect_parameters_index;
// If we're the first mesh instance in this batch, write the index of our
// `MeshInput` into the appropriate slot so that the indirect parameters
// building shader can access it.
#ifndef LATE_PHASE
if (instance_index == 0u || work_items[instance_index - 1].output_or_indirect_parameters_index != indirect_parameters_index) {
indirect_parameters_gpu_metadata[indirect_parameters_index].mesh_index = input_index;
}
#endif // LATE_PHASE
#else // INDIRECT
let mesh_output_index = work_items[instance_index].output_or_indirect_parameters_index;
#endif // INDIRECT
// Unpack the input matrix.
let world_from_local_affine_transpose = current_input[input_index].world_from_local;
let world_from_local = maths::affine3_to_square(world_from_local_affine_transpose);
// Frustum cull if necessary.
#ifdef FRUSTUM_CULLING
if ((current_input[input_index].flags & MESH_FLAGS_NO_FRUSTUM_CULLING_BIT) == 0u) {
let aabb_center = mesh_culling_data[input_index].aabb_center.xyz;
let aabb_half_extents = mesh_culling_data[input_index].aabb_half_extents.xyz;
// Do an OBB-based frustum cull.
let model_center = world_from_local * vec4(aabb_center, 1.0);
if (!view_frustum_intersects_obb(world_from_local, model_center, aabb_half_extents)) {
return;
}
}
#endif
// See whether the `MeshInputUniform` was updated on this frame. If it
// wasn't, then we know the transforms of this mesh must be identical to
// those on the previous frame, and therefore we don't need to access the
// `previous_input_index` (in fact, we can't; that index are only valid for
// one frame and will be invalid).
let timestamp = current_input[input_index].timestamp;
let mesh_changed_this_frame = timestamp == view.frame_count;
// Look up the previous model matrix, if it could have been.
let previous_input_index = current_input[input_index].previous_input_index;
var previous_world_from_local_affine_transpose: mat3x4<f32>;
if (mesh_changed_this_frame && previous_input_index != 0xffffffffu) {
previous_world_from_local_affine_transpose =
previous_input[previous_input_index].world_from_local;
} else {
previous_world_from_local_affine_transpose = world_from_local_affine_transpose;
}
let previous_world_from_local =
maths::affine3_to_square(previous_world_from_local_affine_transpose);
// Occlusion cull if necessary. This is done by calculating the screen-space
// axis-aligned bounding box (AABB) of the mesh and testing it against the
// appropriate level of the depth pyramid (a.k.a. hierarchical Z-buffer). If
// no part of the AABB is in front of the corresponding pixel quad in the
// hierarchical Z-buffer, then this mesh must be occluded, and we can skip
// rendering it.
#ifdef OCCLUSION_CULLING
let aabb_center = mesh_culling_data[input_index].aabb_center.xyz;
let aabb_half_extents = mesh_culling_data[input_index].aabb_half_extents.xyz;
// Initialize the AABB and the maximum depth.
let infinity = bitcast<f32>(0x7f800000u);
let neg_infinity = bitcast<f32>(0xff800000u);
var aabb = vec4(infinity, infinity, neg_infinity, neg_infinity);
var max_depth_view = neg_infinity;
// Build up the AABB by taking each corner of this mesh's OBB, transforming
// it, and updating the AABB and depth accordingly.
for (var i = 0u; i < 8u; i += 1u) {
let local_pos = aabb_center + select(
vec3(-1.0),
vec3(1.0),
vec3((i & 1) != 0, (i & 2) != 0, (i & 4) != 0)
) * aabb_half_extents;
#ifdef EARLY_PHASE
// If we're in the early phase, we're testing against the last frame's
// depth buffer, so we need to use the previous frame's transform.
let prev_world_pos = (previous_world_from_local * vec4(local_pos, 1.0)).xyz;
let view_pos = position_world_to_prev_view(prev_world_pos);
let ndc_pos = position_world_to_prev_ndc(prev_world_pos);
#else // EARLY_PHASE
// Otherwise, if this is the late phase, we use the current frame's
// transform.
let world_pos = (world_from_local * vec4(local_pos, 1.0)).xyz;
let view_pos = position_world_to_view(world_pos);
let ndc_pos = position_world_to_ndc(world_pos);
#endif // EARLY_PHASE
let uv_pos = ndc_to_uv(ndc_pos.xy);
// Update the AABB and maximum view-space depth.
aabb = vec4(min(aabb.xy, uv_pos), max(aabb.zw, uv_pos));
max_depth_view = max(max_depth_view, view_pos.z);
}
// Clip to the near plane to avoid the NDC depth becoming negative.
#ifdef EARLY_PHASE
max_depth_view = min(-previous_view_uniforms.clip_from_view[3][2], max_depth_view);
#else // EARLY_PHASE
max_depth_view = min(-view.clip_from_view[3][2], max_depth_view);
#endif // EARLY_PHASE
// Figure out the depth of the occluder, and compare it to our own depth.
let aabb_pixel_size = occlusion_culling::get_aabb_size_in_pixels(aabb, depth_pyramid);
let occluder_depth_ndc =
occlusion_culling::get_occluder_depth(aabb, aabb_pixel_size, depth_pyramid);
#ifdef EARLY_PHASE
let max_depth_ndc = prev_view_z_to_depth_ndc(max_depth_view);
#else // EARLY_PHASE
let max_depth_ndc = view_z_to_depth_ndc(max_depth_view);
#endif
// Are we culled out?
if (max_depth_ndc < occluder_depth_ndc) {
#ifdef EARLY_PHASE
// If this is the early phase, we need to make a note of this mesh so
// that we examine it again in the late phase, so that we handle the
// case in which a mesh that was invisible last frame became visible in
// this frame.
let output_work_item_index = atomicAdd(&late_preprocess_work_item_indirect_parameters[
push_constants.late_preprocess_work_item_indirect_offset].work_item_count, 1u);
if (output_work_item_index % 64u == 0u) {
// Our workgroup size is 64, and the indirect parameters for the
// late mesh preprocessing phase are counted in workgroups, so if
// we're the first thread in this workgroup, bump the workgroup
// count.
atomicAdd(&late_preprocess_work_item_indirect_parameters[
push_constants.late_preprocess_work_item_indirect_offset].dispatch_x, 1u);
}
// Enqueue a work item for the late prepass phase.
late_preprocess_work_items[output_work_item_index].input_index = input_index;
late_preprocess_work_items[output_work_item_index].output_or_indirect_parameters_index =
indirect_parameters_index;
#endif // EARLY_PHASE
// This mesh is culled. Skip it.
return;
}
#endif // OCCLUSION_CULLING
// Calculate inverse transpose.
let local_from_world_transpose = transpose(maths::inverse_affine3(transpose(
world_from_local_affine_transpose)));
// Pack inverse transpose.
let local_from_world_transpose_a = mat2x4<f32>(
vec4<f32>(local_from_world_transpose[0].xyz, local_from_world_transpose[1].x),
vec4<f32>(local_from_world_transpose[1].yz, local_from_world_transpose[2].xy));
let local_from_world_transpose_b = local_from_world_transpose[2].z;
// Figure out the output index. In indirect mode, this involves bumping the
// instance index in the indirect parameters metadata, which
// `build_indirect_params.wgsl` will use to generate the actual indirect
// parameters. Otherwise, this index was directly supplied to us.
#ifdef INDIRECT
#ifdef LATE_PHASE
let batch_output_index = atomicLoad(
&indirect_parameters_gpu_metadata[indirect_parameters_index].early_instance_count
) + atomicAdd(
&indirect_parameters_gpu_metadata[indirect_parameters_index].late_instance_count,
1u
);
#else // LATE_PHASE
let batch_output_index = atomicAdd(
&indirect_parameters_gpu_metadata[indirect_parameters_index].early_instance_count,
1u
);
#endif // LATE_PHASE
let mesh_output_index =
indirect_parameters_cpu_metadata[indirect_parameters_index].base_output_index +
batch_output_index;
#endif // INDIRECT
// Write the output.
output[mesh_output_index].world_from_local = world_from_local_affine_transpose;
output[mesh_output_index].previous_world_from_local =
previous_world_from_local_affine_transpose;
output[mesh_output_index].local_from_world_transpose_a = local_from_world_transpose_a;
output[mesh_output_index].local_from_world_transpose_b = local_from_world_transpose_b;
output[mesh_output_index].flags = current_input[input_index].flags;
output[mesh_output_index].lightmap_uv_rect = current_input[input_index].lightmap_uv_rect;
output[mesh_output_index].first_vertex_index = current_input[input_index].first_vertex_index;
output[mesh_output_index].current_skin_index = current_input[input_index].current_skin_index;
output[mesh_output_index].material_and_lightmap_bind_group_slot =
current_input[input_index].material_and_lightmap_bind_group_slot;
output[mesh_output_index].tag = current_input[input_index].tag;
}

View File

@@ -0,0 +1,50 @@
#define_import_path bevy_pbr::mesh_types
struct Mesh {
// Affine 4x3 matrices transposed to 3x4
// Use bevy_render::maths::affine3_to_square to unpack
world_from_local: mat3x4<f32>,
previous_world_from_local: mat3x4<f32>,
// 3x3 matrix packed in mat2x4 and f32 as:
// [0].xyz, [1].x,
// [1].yz, [2].xy
// [2].z
// Use bevy_pbr::mesh_functions::mat2x4_f32_to_mat3x3_unpack to unpack
local_from_world_transpose_a: mat2x4<f32>,
local_from_world_transpose_b: f32,
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32,
lightmap_uv_rect: vec2<u32>,
// The index of the mesh's first vertex in the vertex buffer.
first_vertex_index: u32,
current_skin_index: u32,
// Low 16 bits: index of the material inside the bind group data.
// High 16 bits: index of the lightmap in the binding array.
material_and_lightmap_bind_group_slot: u32,
// User supplied index to identify the mesh instance
tag: u32,
pad: u32,
};
#ifdef SKINNED
struct SkinnedMesh {
data: array<mat4x4<f32>, 256u>,
};
#endif
#ifdef MORPH_TARGETS
struct MorphWeights {
weights: array<vec4<f32>, 16u>, // 16 = 64 / 4 (64 = MAX_MORPH_WEIGHTS)
};
#endif
// [2^0, 2^16)
const MESH_FLAGS_VISIBILITY_RANGE_INDEX_BITS: u32 = 65535u;
// 2^28
const MESH_FLAGS_NO_FRUSTUM_CULLING_BIT: u32 = 268435456u;
// 2^29
const MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 536870912u;
// 2^30
const MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT: u32 = 1073741824u;
// 2^31 - if the flag is set, the sign is positive, else it is negative
const MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT: u32 = 2147483648u;

View File

@@ -0,0 +1,765 @@
use alloc::sync::Arc;
use bevy_core_pipeline::{
core_3d::ViewTransmissionTexture,
oit::{resolve::is_oit_supported, OitBuffers, OrderIndependentTransparencySettings},
prepass::ViewPrepassTextures,
tonemapping::{
get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts,
},
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
query::Has,
resource::Resource,
system::{Commands, Query, Res},
world::{FromWorld, World},
};
use bevy_image::BevyDefault as _;
use bevy_math::Vec4;
use bevy_render::{
globals::{GlobalsBuffer, GlobalsUniform},
render_asset::RenderAssets,
render_resource::{binding_types::*, *},
renderer::{RenderAdapter, RenderDevice},
texture::{FallbackImage, FallbackImageMsaa, FallbackImageZero, GpuImage},
view::{
Msaa, RenderVisibilityRanges, ViewUniform, ViewUniforms,
VISIBILITY_RANGES_STORAGE_BUFFER_COUNT,
},
};
use core::{array, num::NonZero};
use environment_map::EnvironmentMapLight;
use crate::{
decal::{
self,
clustered::{
DecalsBuffer, RenderClusteredDecals, RenderViewClusteredDecalBindGroupEntries,
},
},
environment_map::{self, RenderViewEnvironmentMapBindGroupEntries},
irradiance_volume::{
self, IrradianceVolume, RenderViewIrradianceVolumeBindGroupEntries,
IRRADIANCE_VOLUMES_ARE_USABLE,
},
prepass, EnvironmentMapUniformBuffer, FogMeta, GlobalClusterableObjectMeta,
GpuClusterableObjects, GpuFog, GpuLights, LightMeta, LightProbesBuffer, LightProbesUniform,
MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionResources,
ScreenSpaceReflectionsBuffer, ScreenSpaceReflectionsUniform, ShadowSamplers,
ViewClusterBindings, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT,
};
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
use bevy_render::render_resource::binding_types::texture_cube;
#[cfg(debug_assertions)]
use {crate::MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES, bevy_utils::once, tracing::warn};
#[derive(Clone)]
pub struct MeshPipelineViewLayout {
pub bind_group_layout: BindGroupLayout,
#[cfg(debug_assertions)]
pub texture_count: usize,
}
bitflags::bitflags! {
/// A key that uniquely identifies a [`MeshPipelineViewLayout`].
///
/// Used to generate all possible layouts for the mesh pipeline in [`generate_view_layouts`],
/// so special care must be taken to not add too many flags, as the number of possible layouts
/// will grow exponentially.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct MeshPipelineViewLayoutKey: u32 {
const MULTISAMPLED = 1 << 0;
const DEPTH_PREPASS = 1 << 1;
const NORMAL_PREPASS = 1 << 2;
const MOTION_VECTOR_PREPASS = 1 << 3;
const DEFERRED_PREPASS = 1 << 4;
const OIT_ENABLED = 1 << 5;
}
}
impl MeshPipelineViewLayoutKey {
// The number of possible layouts
pub const COUNT: usize = Self::all().bits() as usize + 1;
/// Builds a unique label for each layout based on the flags
pub fn label(&self) -> String {
use MeshPipelineViewLayoutKey as Key;
format!(
"mesh_view_layout{}{}{}{}{}{}",
self.contains(Key::MULTISAMPLED)
.then_some("_multisampled")
.unwrap_or_default(),
self.contains(Key::DEPTH_PREPASS)
.then_some("_depth")
.unwrap_or_default(),
self.contains(Key::NORMAL_PREPASS)
.then_some("_normal")
.unwrap_or_default(),
self.contains(Key::MOTION_VECTOR_PREPASS)
.then_some("_motion")
.unwrap_or_default(),
self.contains(Key::DEFERRED_PREPASS)
.then_some("_deferred")
.unwrap_or_default(),
self.contains(Key::OIT_ENABLED)
.then_some("_oit")
.unwrap_or_default(),
)
}
}
impl From<MeshPipelineKey> for MeshPipelineViewLayoutKey {
fn from(value: MeshPipelineKey) -> Self {
let mut result = MeshPipelineViewLayoutKey::empty();
if value.msaa_samples() > 1 {
result |= MeshPipelineViewLayoutKey::MULTISAMPLED;
}
if value.contains(MeshPipelineKey::DEPTH_PREPASS) {
result |= MeshPipelineViewLayoutKey::DEPTH_PREPASS;
}
if value.contains(MeshPipelineKey::NORMAL_PREPASS) {
result |= MeshPipelineViewLayoutKey::NORMAL_PREPASS;
}
if value.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) {
result |= MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS;
}
if value.contains(MeshPipelineKey::DEFERRED_PREPASS) {
result |= MeshPipelineViewLayoutKey::DEFERRED_PREPASS;
}
if value.contains(MeshPipelineKey::OIT_ENABLED) {
result |= MeshPipelineViewLayoutKey::OIT_ENABLED;
}
result
}
}
impl From<Msaa> for MeshPipelineViewLayoutKey {
fn from(value: Msaa) -> Self {
let mut result = MeshPipelineViewLayoutKey::empty();
if value.samples() > 1 {
result |= MeshPipelineViewLayoutKey::MULTISAMPLED;
}
result
}
}
impl From<Option<&ViewPrepassTextures>> for MeshPipelineViewLayoutKey {
fn from(value: Option<&ViewPrepassTextures>) -> Self {
let mut result = MeshPipelineViewLayoutKey::empty();
if let Some(prepass_textures) = value {
if prepass_textures.depth.is_some() {
result |= MeshPipelineViewLayoutKey::DEPTH_PREPASS;
}
if prepass_textures.normal.is_some() {
result |= MeshPipelineViewLayoutKey::NORMAL_PREPASS;
}
if prepass_textures.motion_vectors.is_some() {
result |= MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS;
}
if prepass_textures.deferred.is_some() {
result |= MeshPipelineViewLayoutKey::DEFERRED_PREPASS;
}
}
result
}
}
pub(crate) fn buffer_layout(
buffer_binding_type: BufferBindingType,
has_dynamic_offset: bool,
min_binding_size: Option<NonZero<u64>>,
) -> BindGroupLayoutEntryBuilder {
match buffer_binding_type {
BufferBindingType::Uniform => uniform_buffer_sized(has_dynamic_offset, min_binding_size),
BufferBindingType::Storage { read_only } => {
if read_only {
storage_buffer_read_only_sized(has_dynamic_offset, min_binding_size)
} else {
storage_buffer_sized(has_dynamic_offset, min_binding_size)
}
}
}
}
/// Returns the appropriate bind group layout vec based on the parameters
fn layout_entries(
clustered_forward_buffer_binding_type: BufferBindingType,
visibility_ranges_buffer_binding_type: BufferBindingType,
layout_key: MeshPipelineViewLayoutKey,
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
) -> Vec<BindGroupLayoutEntry> {
let mut entries = DynamicBindGroupLayoutEntries::new_with_indices(
ShaderStages::FRAGMENT,
(
// View
(
0,
uniform_buffer::<ViewUniform>(true).visibility(ShaderStages::VERTEX_FRAGMENT),
),
// Lights
(1, uniform_buffer::<GpuLights>(true)),
// Point Shadow Texture Cube Array
(
2,
#[cfg(all(
not(target_abi = "sim"),
any(
not(feature = "webgl"),
not(target_arch = "wasm32"),
feature = "webgpu"
)
))]
texture_cube_array(TextureSampleType::Depth),
#[cfg(any(
target_abi = "sim",
all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu"))
))]
texture_cube(TextureSampleType::Depth),
),
// Point Shadow Texture Array Comparison Sampler
(3, sampler(SamplerBindingType::Comparison)),
// Point Shadow Texture Array Linear Sampler
#[cfg(feature = "experimental_pbr_pcss")]
(4, sampler(SamplerBindingType::Filtering)),
// Directional Shadow Texture Array
(
5,
#[cfg(any(
not(feature = "webgl"),
not(target_arch = "wasm32"),
feature = "webgpu"
))]
texture_2d_array(TextureSampleType::Depth),
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
texture_2d(TextureSampleType::Depth),
),
// Directional Shadow Texture Array Comparison Sampler
(6, sampler(SamplerBindingType::Comparison)),
// Directional Shadow Texture Array Linear Sampler
#[cfg(feature = "experimental_pbr_pcss")]
(7, sampler(SamplerBindingType::Filtering)),
// PointLights
(
8,
buffer_layout(
clustered_forward_buffer_binding_type,
false,
Some(GpuClusterableObjects::min_size(
clustered_forward_buffer_binding_type,
)),
),
),
// ClusteredLightIndexLists
(
9,
buffer_layout(
clustered_forward_buffer_binding_type,
false,
Some(
ViewClusterBindings::min_size_clusterable_object_index_lists(
clustered_forward_buffer_binding_type,
),
),
),
),
// ClusterOffsetsAndCounts
(
10,
buffer_layout(
clustered_forward_buffer_binding_type,
false,
Some(ViewClusterBindings::min_size_cluster_offsets_and_counts(
clustered_forward_buffer_binding_type,
)),
),
),
// Globals
(
11,
uniform_buffer::<GlobalsUniform>(false).visibility(ShaderStages::VERTEX_FRAGMENT),
),
// Fog
(12, uniform_buffer::<GpuFog>(true)),
// Light probes
(13, uniform_buffer::<LightProbesUniform>(true)),
// Visibility ranges
(
14,
buffer_layout(
visibility_ranges_buffer_binding_type,
false,
Some(Vec4::min_size()),
)
.visibility(ShaderStages::VERTEX),
),
// Screen space reflection settings
(15, uniform_buffer::<ScreenSpaceReflectionsUniform>(true)),
// Screen space ambient occlusion texture
(
16,
texture_2d(TextureSampleType::Float { filterable: false }),
),
),
);
// EnvironmentMapLight
let environment_map_entries =
environment_map::get_bind_group_layout_entries(render_device, render_adapter);
entries = entries.extend_with_indices((
(17, environment_map_entries[0]),
(18, environment_map_entries[1]),
(19, environment_map_entries[2]),
(20, environment_map_entries[3]),
));
// Irradiance volumes
if IRRADIANCE_VOLUMES_ARE_USABLE {
let irradiance_volume_entries =
irradiance_volume::get_bind_group_layout_entries(render_device, render_adapter);
entries = entries.extend_with_indices((
(21, irradiance_volume_entries[0]),
(22, irradiance_volume_entries[1]),
));
}
// Clustered decals
if let Some(clustered_decal_entries) =
decal::clustered::get_bind_group_layout_entries(render_device, render_adapter)
{
entries = entries.extend_with_indices((
(23, clustered_decal_entries[0]),
(24, clustered_decal_entries[1]),
(25, clustered_decal_entries[2]),
));
}
// Tonemapping
let tonemapping_lut_entries = get_lut_bind_group_layout_entries();
entries = entries.extend_with_indices((
(26, tonemapping_lut_entries[0]),
(27, tonemapping_lut_entries[1]),
));
// Prepass
if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32")))
|| (cfg!(all(feature = "webgl", target_arch = "wasm32"))
&& !layout_key.contains(MeshPipelineViewLayoutKey::MULTISAMPLED))
{
for (entry, binding) in prepass::get_bind_group_layout_entries(layout_key)
.iter()
.zip([28, 29, 30, 31])
{
if let Some(entry) = entry {
entries = entries.extend_with_indices(((binding as u32, *entry),));
}
}
}
// View Transmission Texture
entries = entries.extend_with_indices((
(
32,
texture_2d(TextureSampleType::Float { filterable: true }),
),
(33, sampler(SamplerBindingType::Filtering)),
));
// OIT
if layout_key.contains(MeshPipelineViewLayoutKey::OIT_ENABLED) {
// Check if we can use OIT. This is a hack to avoid errors on webgl --
// the OIT plugin will warn the user that OIT is not supported on their
// platform, so we don't need to do it here.
if is_oit_supported(render_adapter, render_device, false) {
entries = entries.extend_with_indices((
// oit_layers
(34, storage_buffer_sized(false, None)),
// oit_layer_ids,
(35, storage_buffer_sized(false, None)),
// oit_layer_count
(
36,
uniform_buffer::<OrderIndependentTransparencySettings>(true),
),
));
}
}
entries.to_vec()
}
/// Stores the view layouts for every combination of pipeline keys.
///
/// This is wrapped in an [`Arc`] so that it can be efficiently cloned and
/// placed inside specializable pipeline types.
#[derive(Resource, Clone, Deref, DerefMut)]
pub struct MeshPipelineViewLayouts(
pub Arc<[MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT]>,
);
impl FromWorld for MeshPipelineViewLayouts {
fn from_world(world: &mut World) -> Self {
// Generates all possible view layouts for the mesh pipeline, based on all combinations of
// [`MeshPipelineViewLayoutKey`] flags.
let render_device = world.resource::<RenderDevice>();
let render_adapter = world.resource::<RenderAdapter>();
let clustered_forward_buffer_binding_type = render_device
.get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT);
let visibility_ranges_buffer_binding_type = render_device
.get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT);
Self(Arc::new(array::from_fn(|i| {
let key = MeshPipelineViewLayoutKey::from_bits_truncate(i as u32);
let entries = layout_entries(
clustered_forward_buffer_binding_type,
visibility_ranges_buffer_binding_type,
key,
render_device,
render_adapter,
);
#[cfg(debug_assertions)]
let texture_count: usize = entries
.iter()
.filter(|entry| matches!(entry.ty, BindingType::Texture { .. }))
.count();
MeshPipelineViewLayout {
bind_group_layout: render_device
.create_bind_group_layout(key.label().as_str(), &entries),
#[cfg(debug_assertions)]
texture_count,
}
})))
}
}
impl MeshPipelineViewLayouts {
pub fn get_view_layout(&self, layout_key: MeshPipelineViewLayoutKey) -> &BindGroupLayout {
let index = layout_key.bits() as usize;
let layout = &self[index];
#[cfg(debug_assertions)]
if layout.texture_count > MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES {
// Issue our own warning here because Naga's error message is a bit cryptic in this situation
once!(warn!("Too many textures in mesh pipeline view layout, this might cause us to hit `wgpu::Limits::max_sampled_textures_per_shader_stage` in some environments."));
}
&layout.bind_group_layout
}
}
/// Generates all possible view layouts for the mesh pipeline, based on all combinations of
/// [`MeshPipelineViewLayoutKey`] flags.
pub fn generate_view_layouts(
render_device: &RenderDevice,
render_adapter: &RenderAdapter,
clustered_forward_buffer_binding_type: BufferBindingType,
visibility_ranges_buffer_binding_type: BufferBindingType,
) -> [MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT] {
array::from_fn(|i| {
let key = MeshPipelineViewLayoutKey::from_bits_truncate(i as u32);
let entries = layout_entries(
clustered_forward_buffer_binding_type,
visibility_ranges_buffer_binding_type,
key,
render_device,
render_adapter,
);
#[cfg(debug_assertions)]
let texture_count: usize = entries
.iter()
.filter(|entry| matches!(entry.ty, BindingType::Texture { .. }))
.count();
MeshPipelineViewLayout {
bind_group_layout: render_device
.create_bind_group_layout(key.label().as_str(), &entries),
#[cfg(debug_assertions)]
texture_count,
}
})
}
#[derive(Component)]
pub struct MeshViewBindGroup {
pub value: BindGroup,
}
pub fn prepare_mesh_view_bind_groups(
mut commands: Commands,
(render_device, render_adapter): (Res<RenderDevice>, Res<RenderAdapter>),
mesh_pipeline: Res<MeshPipeline>,
shadow_samplers: Res<ShadowSamplers>,
(light_meta, global_light_meta): (Res<LightMeta>, Res<GlobalClusterableObjectMeta>),
fog_meta: Res<FogMeta>,
(view_uniforms, environment_map_uniform): (Res<ViewUniforms>, Res<EnvironmentMapUniformBuffer>),
views: Query<(
Entity,
&ViewShadowBindings,
&ViewClusterBindings,
&Msaa,
Option<&ScreenSpaceAmbientOcclusionResources>,
Option<&ViewPrepassTextures>,
Option<&ViewTransmissionTexture>,
&Tonemapping,
Option<&RenderViewLightProbes<EnvironmentMapLight>>,
Option<&RenderViewLightProbes<IrradianceVolume>>,
Has<OrderIndependentTransparencySettings>,
)>,
(images, mut fallback_images, fallback_image, fallback_image_zero): (
Res<RenderAssets<GpuImage>>,
FallbackImageMsaa,
Res<FallbackImage>,
Res<FallbackImageZero>,
),
globals_buffer: Res<GlobalsBuffer>,
tonemapping_luts: Res<TonemappingLuts>,
light_probes_buffer: Res<LightProbesBuffer>,
visibility_ranges: Res<RenderVisibilityRanges>,
ssr_buffer: Res<ScreenSpaceReflectionsBuffer>,
oit_buffers: Res<OitBuffers>,
(decals_buffer, render_decals): (Res<DecalsBuffer>, Res<RenderClusteredDecals>),
) {
if let (
Some(view_binding),
Some(light_binding),
Some(clusterable_objects_binding),
Some(globals),
Some(fog_binding),
Some(light_probes_binding),
Some(visibility_ranges_buffer),
Some(ssr_binding),
Some(environment_map_binding),
) = (
view_uniforms.uniforms.binding(),
light_meta.view_gpu_lights.binding(),
global_light_meta.gpu_clusterable_objects.binding(),
globals_buffer.buffer.binding(),
fog_meta.gpu_fogs.binding(),
light_probes_buffer.binding(),
visibility_ranges.buffer().buffer(),
ssr_buffer.binding(),
environment_map_uniform.binding(),
) {
for (
entity,
shadow_bindings,
cluster_bindings,
msaa,
ssao_resources,
prepass_textures,
transmission_texture,
tonemapping,
render_view_environment_maps,
render_view_irradiance_volumes,
has_oit,
) in &views
{
let fallback_ssao = fallback_images
.image_for_samplecount(1, TextureFormat::bevy_default())
.texture_view
.clone();
let ssao_view = ssao_resources
.map(|t| &t.screen_space_ambient_occlusion_texture.default_view)
.unwrap_or(&fallback_ssao);
let mut layout_key = MeshPipelineViewLayoutKey::from(*msaa)
| MeshPipelineViewLayoutKey::from(prepass_textures);
if has_oit {
layout_key |= MeshPipelineViewLayoutKey::OIT_ENABLED;
}
let layout = &mesh_pipeline.get_view_layout(layout_key);
let mut entries = DynamicBindGroupEntries::new_with_indices((
(0, view_binding.clone()),
(1, light_binding.clone()),
(2, &shadow_bindings.point_light_depth_texture_view),
(3, &shadow_samplers.point_light_comparison_sampler),
#[cfg(feature = "experimental_pbr_pcss")]
(4, &shadow_samplers.point_light_linear_sampler),
(5, &shadow_bindings.directional_light_depth_texture_view),
(6, &shadow_samplers.directional_light_comparison_sampler),
#[cfg(feature = "experimental_pbr_pcss")]
(7, &shadow_samplers.directional_light_linear_sampler),
(8, clusterable_objects_binding.clone()),
(
9,
cluster_bindings
.clusterable_object_index_lists_binding()
.unwrap(),
),
(10, cluster_bindings.offsets_and_counts_binding().unwrap()),
(11, globals.clone()),
(12, fog_binding.clone()),
(13, light_probes_binding.clone()),
(14, visibility_ranges_buffer.as_entire_binding()),
(15, ssr_binding.clone()),
(16, ssao_view),
));
let environment_map_bind_group_entries = RenderViewEnvironmentMapBindGroupEntries::get(
render_view_environment_maps,
&images,
&fallback_image,
&render_device,
&render_adapter,
);
match environment_map_bind_group_entries {
RenderViewEnvironmentMapBindGroupEntries::Single {
diffuse_texture_view,
specular_texture_view,
sampler,
} => {
entries = entries.extend_with_indices((
(17, diffuse_texture_view),
(18, specular_texture_view),
(19, sampler),
(20, environment_map_binding.clone()),
));
}
RenderViewEnvironmentMapBindGroupEntries::Multiple {
ref diffuse_texture_views,
ref specular_texture_views,
sampler,
} => {
entries = entries.extend_with_indices((
(17, diffuse_texture_views.as_slice()),
(18, specular_texture_views.as_slice()),
(19, sampler),
(20, environment_map_binding.clone()),
));
}
}
let irradiance_volume_bind_group_entries = if IRRADIANCE_VOLUMES_ARE_USABLE {
Some(RenderViewIrradianceVolumeBindGroupEntries::get(
render_view_irradiance_volumes,
&images,
&fallback_image,
&render_device,
&render_adapter,
))
} else {
None
};
match irradiance_volume_bind_group_entries {
Some(RenderViewIrradianceVolumeBindGroupEntries::Single {
texture_view,
sampler,
}) => {
entries = entries.extend_with_indices(((21, texture_view), (22, sampler)));
}
Some(RenderViewIrradianceVolumeBindGroupEntries::Multiple {
ref texture_views,
sampler,
}) => {
entries = entries
.extend_with_indices(((21, texture_views.as_slice()), (22, sampler)));
}
None => {}
}
let decal_bind_group_entries = RenderViewClusteredDecalBindGroupEntries::get(
&render_decals,
&decals_buffer,
&images,
&fallback_image,
&render_device,
&render_adapter,
);
// Add the decal bind group entries.
if let Some(ref render_view_decal_bind_group_entries) = decal_bind_group_entries {
entries = entries.extend_with_indices((
// `clustered_decals`
(
23,
render_view_decal_bind_group_entries
.decals
.as_entire_binding(),
),
// `clustered_decal_textures`
(
24,
render_view_decal_bind_group_entries
.texture_views
.as_slice(),
),
// `clustered_decal_sampler`
(25, render_view_decal_bind_group_entries.sampler),
));
}
let lut_bindings =
get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image);
entries = entries.extend_with_indices(((26, lut_bindings.0), (27, lut_bindings.1)));
// When using WebGL, we can't have a depth texture with multisampling
let prepass_bindings;
if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32"))) || msaa.samples() == 1
{
prepass_bindings = prepass::get_bindings(prepass_textures);
for (binding, index) in prepass_bindings
.iter()
.map(Option::as_ref)
.zip([28, 29, 30, 31])
.flat_map(|(b, i)| b.map(|b| (b, i)))
{
entries = entries.extend_with_indices(((index, binding),));
}
};
let transmission_view = transmission_texture
.map(|transmission| &transmission.view)
.unwrap_or(&fallback_image_zero.texture_view);
let transmission_sampler = transmission_texture
.map(|transmission| &transmission.sampler)
.unwrap_or(&fallback_image_zero.sampler);
entries =
entries.extend_with_indices(((32, transmission_view), (33, transmission_sampler)));
if has_oit {
if let (
Some(oit_layers_binding),
Some(oit_layer_ids_binding),
Some(oit_settings_binding),
) = (
oit_buffers.layers.binding(),
oit_buffers.layer_ids.binding(),
oit_buffers.settings.binding(),
) {
entries = entries.extend_with_indices((
(34, oit_layers_binding.clone()),
(35, oit_layer_ids_binding.clone()),
(36, oit_settings_binding.clone()),
));
}
}
commands.entity(entity).insert(MeshViewBindGroup {
value: render_device.create_bind_group("mesh_view_bind_group", layout, &entries),
});
}
}
}

View File

@@ -0,0 +1,119 @@
#define_import_path bevy_pbr::mesh_view_bindings
#import bevy_pbr::mesh_view_types as types
#import bevy_render::{
view::View,
globals::Globals,
}
@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var<uniform> lights: types::Lights;
#ifdef NO_CUBE_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_comparison_sampler: sampler_comparison;
#ifdef PCSS_SAMPLERS_AVAILABLE
@group(0) @binding(4) var point_shadow_textures_linear_sampler: sampler;
#endif // PCSS_SAMPLERS_AVAILABLE
#ifdef NO_ARRAY_TEXTURES_SUPPORT
@group(0) @binding(5) var directional_shadow_textures: texture_depth_2d;
#else
@group(0) @binding(5) var directional_shadow_textures: texture_depth_2d_array;
#endif
@group(0) @binding(6) var directional_shadow_textures_comparison_sampler: sampler_comparison;
#ifdef PCSS_SAMPLERS_AVAILABLE
@group(0) @binding(7) var directional_shadow_textures_linear_sampler: sampler;
#endif // PCSS_SAMPLERS_AVAILABLE
#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3
@group(0) @binding(8) var<storage> clusterable_objects: types::ClusterableObjects;
@group(0) @binding(9) var<storage> clusterable_object_index_lists: types::ClusterLightIndexLists;
@group(0) @binding(10) var<storage> cluster_offsets_and_counts: types::ClusterOffsetsAndCounts;
#else
@group(0) @binding(8) var<uniform> clusterable_objects: types::ClusterableObjects;
@group(0) @binding(9) var<uniform> clusterable_object_index_lists: types::ClusterLightIndexLists;
@group(0) @binding(10) var<uniform> cluster_offsets_and_counts: types::ClusterOffsetsAndCounts;
#endif
@group(0) @binding(11) var<uniform> globals: Globals;
@group(0) @binding(12) var<uniform> fog: types::Fog;
@group(0) @binding(13) var<uniform> light_probes: types::LightProbes;
const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u;
#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6
@group(0) @binding(14) var<storage> visibility_ranges: array<vec4<f32>>;
#else
@group(0) @binding(14) var<uniform> visibility_ranges: array<vec4<f32>, VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE>;
#endif
@group(0) @binding(15) var<uniform> ssr_settings: types::ScreenSpaceReflectionsSettings;
@group(0) @binding(16) var screen_space_ambient_occlusion_texture: texture_2d<f32>;
#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY
@group(0) @binding(17) var diffuse_environment_maps: binding_array<texture_cube<f32>, 8u>;
@group(0) @binding(18) var specular_environment_maps: binding_array<texture_cube<f32>, 8u>;
#else
@group(0) @binding(17) var diffuse_environment_map: texture_cube<f32>;
@group(0) @binding(18) var specular_environment_map: texture_cube<f32>;
#endif
@group(0) @binding(19) var environment_map_sampler: sampler;
@group(0) @binding(20) var<uniform> environment_map_uniform: types::EnvironmentMapUniform;
#ifdef IRRADIANCE_VOLUMES_ARE_USABLE
#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY
@group(0) @binding(21) var irradiance_volumes: binding_array<texture_3d<f32>, 8u>;
#else
@group(0) @binding(21) var irradiance_volume: texture_3d<f32>;
#endif
@group(0) @binding(22) var irradiance_volume_sampler: sampler;
#endif
#ifdef CLUSTERED_DECALS_ARE_USABLE
@group(0) @binding(23) var<storage> clustered_decals: types::ClusteredDecals;
@group(0) @binding(24) var clustered_decal_textures: binding_array<texture_2d<f32>, 8u>;
@group(0) @binding(25) var clustered_decal_sampler: sampler;
#endif // CLUSTERED_DECALS_ARE_USABLE
// NB: If you change these, make sure to update `tonemapping_shared.wgsl` too.
@group(0) @binding(26) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(27) var dt_lut_sampler: sampler;
#ifdef MULTISAMPLED
#ifdef DEPTH_PREPASS
@group(0) @binding(28) var depth_prepass_texture: texture_depth_multisampled_2d;
#endif // DEPTH_PREPASS
#ifdef NORMAL_PREPASS
@group(0) @binding(29) var normal_prepass_texture: texture_multisampled_2d<f32>;
#endif // NORMAL_PREPASS
#ifdef MOTION_VECTOR_PREPASS
@group(0) @binding(30) var motion_vector_prepass_texture: texture_multisampled_2d<f32>;
#endif // MOTION_VECTOR_PREPASS
#else // MULTISAMPLED
#ifdef DEPTH_PREPASS
@group(0) @binding(28) var depth_prepass_texture: texture_depth_2d;
#endif // DEPTH_PREPASS
#ifdef NORMAL_PREPASS
@group(0) @binding(29) var normal_prepass_texture: texture_2d<f32>;
#endif // NORMAL_PREPASS
#ifdef MOTION_VECTOR_PREPASS
@group(0) @binding(30) var motion_vector_prepass_texture: texture_2d<f32>;
#endif // MOTION_VECTOR_PREPASS
#endif // MULTISAMPLED
#ifdef DEFERRED_PREPASS
@group(0) @binding(31) var deferred_prepass_texture: texture_2d<u32>;
#endif // DEFERRED_PREPASS
@group(0) @binding(32) var view_transmission_texture: texture_2d<f32>;
@group(0) @binding(33) var view_transmission_sampler: sampler;
#ifdef OIT_ENABLED
@group(0) @binding(34) var<storage, read_write> oit_layers: array<vec2<u32>>;
@group(0) @binding(35) var<storage, read_write> oit_layer_ids: array<atomic<i32>>;
@group(0) @binding(36) var<uniform> oit_settings: types::OrderIndependentTransparencySettings;
#endif // OIT_ENABLED

View File

@@ -0,0 +1,185 @@
#define_import_path bevy_pbr::mesh_view_types
struct ClusterableObject {
// For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3]
// For spot lights: the direction (x,z), spot_scale and spot_offset
light_custom_data: vec4<f32>,
color_inverse_square_range: vec4<f32>,
position_radius: vec4<f32>,
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32,
shadow_depth_bias: f32,
shadow_normal_bias: f32,
spot_light_tan_angle: f32,
soft_shadow_size: f32,
shadow_map_near_z: f32,
texture_index: u32,
pad: f32,
};
const POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;
const POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE: u32 = 2u;
const POINT_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 4u;
const POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 8u;
struct DirectionalCascade {
clip_from_world: mat4x4<f32>,
texel_size: f32,
far_bound: f32,
}
struct DirectionalLight {
cascades: array<DirectionalCascade, #{MAX_CASCADES_PER_LIGHT}>,
color: vec4<f32>,
direction_to_light: vec3<f32>,
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32,
soft_shadow_size: f32,
shadow_depth_bias: f32,
shadow_normal_bias: f32,
num_cascades: u32,
cascades_overlap_proportion: f32,
depth_texture_base_index: u32,
};
const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u;
const DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 2u;
const DIRECTIONAL_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 4u;
struct Lights {
// NOTE: this array size must be kept in sync with the constants defined in bevy_pbr/src/render/light.rs
directional_lights: array<DirectionalLight, #{MAX_DIRECTIONAL_LIGHTS}u>,
ambient_color: vec4<f32>,
// x/y/z dimensions and n_clusters in w
cluster_dimensions: vec4<u32>,
// xy are vec2<f32>(cluster_dimensions.xy) / vec2<f32>(view.width, view.height)
//
// For perspective projections:
// z is cluster_dimensions.z / log(far / near)
// w is cluster_dimensions.z * log(near) / log(far / near)
//
// For orthographic projections:
// NOTE: near and far are +ve but -z is infront of the camera
// z is -near
// w is cluster_dimensions.z / (-far - -near)
cluster_factors: vec4<f32>,
n_directional_lights: u32,
spot_light_shadowmap_offset: i32,
environment_map_smallest_specular_mip_level: u32,
environment_map_intensity: f32,
};
struct Fog {
base_color: vec4<f32>,
directional_light_color: vec4<f32>,
// `be` and `bi` are allocated differently depending on the fog mode
//
// For Linear Fog:
// be.x = start, be.y = end
// For Exponential and ExponentialSquared Fog:
// be.x = density
// For Atmospheric Fog:
// be = per-channel extinction density
// bi = per-channel inscattering density
be: vec3<f32>,
directional_light_exponent: f32,
bi: vec3<f32>,
mode: u32,
}
// Important: These must be kept in sync with `fog.rs`
const FOG_MODE_OFF: u32 = 0u;
const FOG_MODE_LINEAR: u32 = 1u;
const FOG_MODE_EXPONENTIAL: u32 = 2u;
const FOG_MODE_EXPONENTIAL_SQUARED: u32 = 3u;
const FOG_MODE_ATMOSPHERIC: u32 = 4u;
#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3
struct ClusterableObjects {
data: array<ClusterableObject>,
};
struct ClusterLightIndexLists {
data: array<u32>,
};
struct ClusterOffsetsAndCounts {
data: array<array<vec4<u32>, 2>>,
};
#else
struct ClusterableObjects {
data: array<ClusterableObject, 204u>,
};
struct ClusterLightIndexLists {
// each u32 contains 4 u8 indices into the ClusterableObjects array
data: array<vec4<u32>, 1024u>,
};
struct ClusterOffsetsAndCounts {
// each u32 contains a 24-bit index into ClusterLightIndexLists in the high 24 bits
// and an 8-bit count of the number of lights in the low 8 bits
data: array<vec4<u32>, 1024u>,
};
#endif
struct LightProbe {
// This is stored as the transpose in order to save space in this structure.
// It'll be transposed in the `environment_map_light` function.
light_from_world_transposed: mat3x4<f32>,
cubemap_index: i32,
intensity: f32,
// Whether this light probe contributes diffuse light to lightmapped meshes.
affects_lightmapped_mesh_diffuse: u32,
};
struct LightProbes {
// This must match `MAX_VIEW_REFLECTION_PROBES` on the Rust side.
reflection_probes: array<LightProbe, 8u>,
irradiance_volumes: array<LightProbe, 8u>,
reflection_probe_count: i32,
irradiance_volume_count: i32,
// The index of the view environment map cubemap binding, or -1 if there's
// no such cubemap.
view_cubemap_index: i32,
// The smallest valid mipmap level for the specular environment cubemap
// associated with the view.
smallest_specular_mip_level_for_view: u32,
// The intensity of the environment map associated with the view.
intensity_for_view: f32,
// Whether the environment map attached to the view affects the diffuse
// lighting for lightmapped meshes.
view_environment_map_affects_lightmapped_mesh_diffuse: u32,
};
// Settings for screen space reflections.
//
// For more information on these settings, see the documentation for
// `bevy_pbr::ssr::ScreenSpaceReflections`.
struct ScreenSpaceReflectionsSettings {
perceptual_roughness_threshold: f32,
thickness: f32,
linear_steps: u32,
linear_march_exponent: f32,
bisection_steps: u32,
use_secant: u32,
};
struct EnvironmentMapUniform {
// Transformation matrix for the environment cubemaps in world space.
transform: mat4x4<f32>,
};
// Shader version of the order independent transparency settings component.
struct OrderIndependentTransparencySettings {
layers_count: i32,
alpha_threshold: f32,
};
struct ClusteredDecal {
local_from_world: mat4x4<f32>,
image_index: i32,
tag: u32,
pad_a: u32,
pad_b: u32,
}
struct ClusteredDecals {
decals: array<ClusteredDecal>,
}

17
vendor/bevy_pbr/src/render/mod.rs vendored Normal file
View File

@@ -0,0 +1,17 @@
mod fog;
mod gpu_preprocess;
mod light;
pub(crate) mod mesh;
mod mesh_bindings;
mod mesh_view_bindings;
mod morph;
pub(crate) mod skin;
pub use fog::*;
pub use gpu_preprocess::*;
pub use light::*;
pub use mesh::*;
pub use mesh_bindings::MeshLayouts;
pub use mesh_view_bindings::*;
pub use morph::*;
pub use skin::{extract_skins, prepare_skins, skins_use_uniform_buffers, SkinUniforms, MAX_JOINTS};

150
vendor/bevy_pbr/src/render/morph.rs vendored Normal file
View File

@@ -0,0 +1,150 @@
use core::{iter, mem};
use bevy_ecs::prelude::*;
use bevy_render::sync_world::MainEntityHashMap;
use bevy_render::{
batching::NoAutomaticBatching,
mesh::morph::{MeshMorphWeights, MAX_MORPH_WEIGHTS},
render_resource::{BufferUsages, RawBufferVec},
renderer::{RenderDevice, RenderQueue},
view::ViewVisibility,
Extract,
};
use bytemuck::NoUninit;
#[derive(Component)]
pub struct MorphIndex {
pub index: u32,
}
/// Maps each mesh affected by morph targets to the applicable offset within the
/// [`MorphUniforms`] buffer.
///
/// We store both the current frame's mapping and the previous frame's mapping
/// for the purposes of motion vector calculation.
#[derive(Default, Resource)]
pub struct MorphIndices {
/// Maps each entity with a morphed mesh to the appropriate offset within
/// [`MorphUniforms::current_buffer`].
pub current: MainEntityHashMap<MorphIndex>,
/// Maps each entity with a morphed mesh to the appropriate offset within
/// [`MorphUniforms::prev_buffer`].
pub prev: MainEntityHashMap<MorphIndex>,
}
/// The GPU buffers containing morph weights for all meshes with morph targets.
///
/// This is double-buffered: we store the weights of the previous frame in
/// addition to those of the current frame. This is for motion vector
/// calculation. Every frame, we swap buffers and reuse the morph target weight
/// buffer from two frames ago for the current frame.
#[derive(Resource)]
pub struct MorphUniforms {
/// The morph weights for the current frame.
pub current_buffer: RawBufferVec<f32>,
/// The morph weights for the previous frame.
pub prev_buffer: RawBufferVec<f32>,
}
impl Default for MorphUniforms {
fn default() -> Self {
Self {
current_buffer: RawBufferVec::new(BufferUsages::UNIFORM),
prev_buffer: RawBufferVec::new(BufferUsages::UNIFORM),
}
}
}
pub fn prepare_morphs(
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut uniform: ResMut<MorphUniforms>,
) {
if uniform.current_buffer.is_empty() {
return;
}
let len = uniform.current_buffer.len();
uniform.current_buffer.reserve(len, &render_device);
uniform
.current_buffer
.write_buffer(&render_device, &render_queue);
// We don't need to write `uniform.prev_buffer` because we already wrote it
// last frame, and the data should still be on the GPU.
}
const fn can_align(step: usize, target: usize) -> bool {
step % target == 0 || target % step == 0
}
const WGPU_MIN_ALIGN: usize = 256;
/// Align a [`RawBufferVec`] to `N` bytes by padding the end with `T::default()` values.
fn add_to_alignment<T: NoUninit + Default>(buffer: &mut RawBufferVec<T>) {
let n = WGPU_MIN_ALIGN;
let t_size = size_of::<T>();
if !can_align(n, t_size) {
// This panic is stripped at compile time, due to n, t_size and can_align being const
panic!(
"RawBufferVec should contain only types with a size multiple or divisible by {n}, \
{} has a size of {t_size}, which is neither multiple or divisible by {n}",
core::any::type_name::<T>()
);
}
let buffer_size = buffer.len();
let byte_size = t_size * buffer_size;
let bytes_over_n = byte_size % n;
if bytes_over_n == 0 {
return;
}
let bytes_to_add = n - bytes_over_n;
let ts_to_add = bytes_to_add / t_size;
buffer.extend(iter::repeat_with(T::default).take(ts_to_add));
}
// Notes on implementation: see comment on top of the extract_skins system in skin module.
// This works similarly, but for `f32` instead of `Mat4`
pub fn extract_morphs(
morph_indices: ResMut<MorphIndices>,
uniform: ResMut<MorphUniforms>,
query: Extract<Query<(Entity, &ViewVisibility, &MeshMorphWeights)>>,
) {
// Borrow check workaround.
let (morph_indices, uniform) = (morph_indices.into_inner(), uniform.into_inner());
// Swap buffers. We need to keep the previous frame's buffer around for the
// purposes of motion vector computation.
mem::swap(&mut morph_indices.current, &mut morph_indices.prev);
mem::swap(&mut uniform.current_buffer, &mut uniform.prev_buffer);
morph_indices.current.clear();
uniform.current_buffer.clear();
for (entity, view_visibility, morph_weights) in &query {
if !view_visibility.get() {
continue;
}
let start = uniform.current_buffer.len();
let weights = morph_weights.weights();
let legal_weights = weights.iter().take(MAX_MORPH_WEIGHTS).copied();
uniform.current_buffer.extend(legal_weights);
add_to_alignment::<f32>(&mut uniform.current_buffer);
let index = (start * size_of::<f32>()) as u32;
morph_indices
.current
.insert(entity.into(), MorphIndex { index });
}
}
// NOTE: Because morph targets require per-morph target texture bindings, they cannot
// currently be batched.
pub fn no_automatic_morph_batching(
mut commands: Commands,
query: Query<Entity, (With<MeshMorphWeights>, Without<NoAutomaticBatching>)>,
) {
for entity in &query {
commands.entity(entity).try_insert(NoAutomaticBatching);
}
}

52
vendor/bevy_pbr/src/render/morph.wgsl vendored Normal file
View File

@@ -0,0 +1,52 @@
#define_import_path bevy_pbr::morph
#ifdef MORPH_TARGETS
#import bevy_pbr::mesh_types::MorphWeights;
@group(1) @binding(2) var<uniform> morph_weights: MorphWeights;
@group(1) @binding(3) var morph_targets: texture_3d<f32>;
@group(1) @binding(7) var<uniform> prev_morph_weights: MorphWeights;
// NOTE: Those are the "hardcoded" values found in `MorphAttributes` struct
// in crates/bevy_render/src/mesh/morph/visitors.rs
// In an ideal world, the offsets are established dynamically and passed as #defines
// to the shader, but it's out of scope for the initial implementation of morph targets.
const position_offset: u32 = 0u;
const normal_offset: u32 = 3u;
const tangent_offset: u32 = 6u;
const total_component_count: u32 = 9u;
fn layer_count() -> u32 {
let dimensions = textureDimensions(morph_targets);
return u32(dimensions.z);
}
fn component_texture_coord(vertex_index: u32, component_offset: u32) -> vec2<u32> {
let width = u32(textureDimensions(morph_targets).x);
let component_index = total_component_count * vertex_index + component_offset;
return vec2<u32>(component_index % width, component_index / width);
}
fn weight_at(weight_index: u32) -> f32 {
let i = weight_index;
return morph_weights.weights[i / 4u][i % 4u];
}
fn prev_weight_at(weight_index: u32) -> f32 {
let i = weight_index;
return prev_morph_weights.weights[i / 4u][i % 4u];
}
fn morph_pixel(vertex: u32, component: u32, weight: u32) -> f32 {
let coord = component_texture_coord(vertex, component);
// Due to https://gpuweb.github.io/gpuweb/wgsl/#texel-formats
// While the texture stores a f32, the textureLoad returns a vec4<>, where
// only the first component is set.
return textureLoad(morph_targets, vec3(coord, weight), 0).r;
}
fn morph(vertex_index: u32, component_offset: u32, weight_index: u32) -> vec3<f32> {
return vec3<f32>(
morph_pixel(vertex_index, component_offset, weight_index),
morph_pixel(vertex_index, component_offset + 1u, weight_index),
morph_pixel(vertex_index, component_offset + 2u, weight_index),
);
}
#endif // MORPH_TARGETS

View File

@@ -0,0 +1,30 @@
// Occlusion culling utility functions.
#define_import_path bevy_pbr::occlusion_culling
fn get_aabb_size_in_pixels(aabb: vec4<f32>, depth_pyramid: texture_2d<f32>) -> vec2<f32> {
let depth_pyramid_size_mip_0 = vec2<f32>(textureDimensions(depth_pyramid, 0));
let aabb_width_pixels = (aabb.z - aabb.x) * depth_pyramid_size_mip_0.x;
let aabb_height_pixels = (aabb.w - aabb.y) * depth_pyramid_size_mip_0.y;
return vec2(aabb_width_pixels, aabb_height_pixels);
}
fn get_occluder_depth(
aabb: vec4<f32>,
aabb_pixel_size: vec2<f32>,
depth_pyramid: texture_2d<f32>
) -> f32 {
let aabb_width_pixels = aabb_pixel_size.x;
let aabb_height_pixels = aabb_pixel_size.y;
let depth_pyramid_size_mip_0 = vec2<f32>(textureDimensions(depth_pyramid, 0));
let depth_level = max(0, i32(ceil(log2(max(aabb_width_pixels, aabb_height_pixels))))); // TODO: Naga doesn't like this being a u32
let depth_pyramid_size = vec2<f32>(textureDimensions(depth_pyramid, depth_level));
let aabb_top_left = vec2<u32>(aabb.xy * depth_pyramid_size);
let depth_quad_a = textureLoad(depth_pyramid, aabb_top_left, depth_level).x;
let depth_quad_b = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 0u), depth_level).x;
let depth_quad_c = textureLoad(depth_pyramid, aabb_top_left + vec2(0u, 1u), depth_level).x;
let depth_quad_d = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 1u), depth_level).x;
return min(min(depth_quad_a, depth_quad_b), min(depth_quad_c, depth_quad_d));
}

View File

@@ -0,0 +1,139 @@
#define_import_path bevy_pbr::parallax_mapping
#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d}
#import bevy_pbr::{
pbr_bindings::{depth_map_texture, depth_map_sampler},
mesh_bindings::mesh
}
#ifdef BINDLESS
#import bevy_pbr::pbr_bindings::material_indices
#endif // BINDLESS
fn sample_depth_map(uv: vec2<f32>, material_bind_group_slot: u32) -> f32 {
// We use `textureSampleLevel` over `textureSample` because the wgpu DX12
// backend (Fxc) panics when using "gradient instructions" inside a loop.
// It results in the whole loop being unrolled by the shader compiler,
// which it can't do because the upper limit of the loop in steep parallax
// mapping is a variable set by the user.
// The "gradient instructions" comes from `textureSample` computing MIP level
// based on UV derivative. With `textureSampleLevel`, we provide ourselves
// the MIP level, so no gradient instructions are used, and we can use
// sample_depth_map in our loop.
// See https://stackoverflow.com/questions/56581141/direct3d11-gradient-instruction-used-in-a-loop-with-varying-iteration-forcing
return textureSampleLevel(
#ifdef BINDLESS
bindless_textures_2d[material_indices[material_bind_group_slot].depth_map_texture],
bindless_samplers_filtering[material_indices[material_bind_group_slot].depth_map_sampler],
#else // BINDLESS
depth_map_texture,
depth_map_sampler,
#endif // BINDLESS
uv,
0.0
).r;
}
// An implementation of parallax mapping, see https://en.wikipedia.org/wiki/Parallax_mapping
// Code derived from: https://web.archive.org/web/20150419215321/http://sunandblackcat.com/tipFullView.php?l=eng&topicid=28
fn parallaxed_uv(
depth_scale: f32,
max_layer_count: f32,
max_steps: u32,
// The original interpolated uv
original_uv: vec2<f32>,
// The vector from the camera to the fragment at the surface in tangent space
Vt: vec3<f32>,
material_bind_group_slot: u32,
) -> vec2<f32> {
if max_layer_count < 1.0 {
return original_uv;
}
var uv = original_uv;
// Steep Parallax Mapping
// ======================
// Split the depth map into `layer_count` layers.
// When Vt hits the surface of the mesh (excluding depth displacement),
// if the depth is not below or on surface including depth displacement (textureSample), then
// look forward (+= delta_uv) on depth texture according to
// Vt and distance between hit surface and depth map surface,
// repeat until below the surface.
//
// Where `layer_count` is interpolated between `1.0` and
// `max_layer_count` according to the steepness of Vt.
let view_steepness = abs(Vt.z);
// We mix with minimum value 1.0 because otherwise,
// with 0.0, we get a division by zero in surfaces parallel to viewport,
// resulting in a singularity.
let layer_count = mix(max_layer_count, 1.0, view_steepness);
let layer_depth = 1.0 / layer_count;
var delta_uv = depth_scale * layer_depth * Vt.xy * vec2(1.0, -1.0) / view_steepness;
var current_layer_depth = 0.0;
var texture_depth = sample_depth_map(uv, material_bind_group_slot);
// texture_depth > current_layer_depth means the depth map depth is deeper
// than the depth the ray would be at this UV offset so the ray has not
// intersected the surface
for (var i: i32 = 0; texture_depth > current_layer_depth && i <= i32(layer_count); i++) {
current_layer_depth += layer_depth;
uv += delta_uv;
texture_depth = sample_depth_map(uv, material_bind_group_slot);
}
#ifdef RELIEF_MAPPING
// Relief Mapping
// ==============
// "Refine" the rough result from Steep Parallax Mapping
// with a **binary search** between the layer selected by steep parallax
// and the next one to find a point closer to the depth map surface.
// This reduces the jaggy step artifacts from steep parallax mapping.
delta_uv *= 0.5;
var delta_depth = 0.5 * layer_depth;
uv -= delta_uv;
current_layer_depth -= delta_depth;
for (var i: u32 = 0u; i < max_steps; i++) {
texture_depth = sample_depth_map(uv, material_bind_group_slot);
// Halve the deltas for the next step
delta_uv *= 0.5;
delta_depth *= 0.5;
// Step based on whether the current depth is above or below the depth map
if (texture_depth > current_layer_depth) {
uv += delta_uv;
current_layer_depth += delta_depth;
} else {
uv -= delta_uv;
current_layer_depth -= delta_depth;
}
}
#else
// Parallax Occlusion mapping
// ==========================
// "Refine" Steep Parallax Mapping by interpolating between the
// previous layer's depth and the computed layer depth.
// Only requires a single lookup, unlike Relief Mapping, but
// may skip small details and result in writhing material artifacts.
let previous_uv = uv - delta_uv;
let next_depth = texture_depth - current_layer_depth;
let previous_depth = sample_depth_map(previous_uv, material_bind_group_slot) -
current_layer_depth + layer_depth;
let weight = next_depth / (next_depth - previous_depth);
uv = mix(uv, previous_uv, weight);
current_layer_depth += mix(next_depth, previous_depth, weight);
#endif
// Note: `current_layer_depth` is not returned, but may be useful
// for light computation later on in future improvements of the pbr shader.
return uv;
}

107
vendor/bevy_pbr/src/render/pbr.wgsl vendored Normal file
View File

@@ -0,0 +1,107 @@
#import bevy_pbr::{
pbr_types,
pbr_functions::alpha_discard,
pbr_fragment::pbr_input_from_standard_material,
decal::clustered::apply_decal_base_color,
}
#ifdef PREPASS_PIPELINE
#import bevy_pbr::{
prepass_io::{VertexOutput, FragmentOutput},
pbr_deferred_functions::deferred_output,
}
#else
#import bevy_pbr::{
forward_io::{VertexOutput, FragmentOutput},
pbr_functions,
pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT,
}
#endif
#ifdef MESHLET_MESH_MATERIAL_PASS
#import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output
#endif
#ifdef OIT_ENABLED
#import bevy_core_pipeline::oit::oit_draw
#endif // OIT_ENABLED
#ifdef FORWARD_DECAL
#import bevy_pbr::decal::forward::get_forward_decal_info
#endif
@fragment
fn fragment(
#ifdef MESHLET_MESH_MATERIAL_PASS
@builtin(position) frag_coord: vec4<f32>,
#else
vertex_output: VertexOutput,
@builtin(front_facing) is_front: bool,
#endif
) -> FragmentOutput {
#ifdef MESHLET_MESH_MATERIAL_PASS
let vertex_output = resolve_vertex_output(frag_coord);
let is_front = true;
#endif
var in = vertex_output;
// If we're in the crossfade section of a visibility range, conditionally
// discard the fragment according to the visibility pattern.
#ifdef VISIBILITY_RANGE_DITHER
pbr_functions::visibility_range_dither(in.position, in.visibility_range_dither);
#endif
#ifdef FORWARD_DECAL
let forward_decal_info = get_forward_decal_info(in);
in.world_position = forward_decal_info.world_position;
in.uv = forward_decal_info.uv;
#endif
// generate a PbrInput struct from the StandardMaterial bindings
var pbr_input = pbr_input_from_standard_material(in, is_front);
// alpha discard
pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color);
// clustered decals
pbr_input.material.base_color = apply_decal_base_color(
in.world_position.xyz,
in.position.xy,
pbr_input.material.base_color
);
#ifdef PREPASS_PIPELINE
// write the gbuffer, lighting pass id, and optionally normal and motion_vector textures
let out = deferred_output(in, pbr_input);
#else
// in forward mode, we calculate the lit color immediately, and then apply some post-lighting effects here.
// in deferred mode the lit color and these effects will be calculated in the deferred lighting shader
var out: FragmentOutput;
if (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u {
out.color = apply_pbr_lighting(pbr_input);
} else {
out.color = pbr_input.material.base_color;
}
// apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr)
// note this does not include fullscreen postprocessing effects like bloom.
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
#endif
#ifdef OIT_ENABLED
let alpha_mode = pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS;
if alpha_mode != pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE {
// The fragments will only be drawn during the oit resolve pass.
oit_draw(in.position, out.color);
discard;
}
#endif // OIT_ENABLED
#ifdef FORWARD_DECAL
out.color.a = min(forward_decal_info.alpha, out.color.a);
#endif
return out;
}

View File

@@ -0,0 +1,29 @@
#define_import_path bevy_pbr::ambient
#import bevy_pbr::{
lighting::{EnvBRDFApprox, F_AB},
mesh_view_bindings::lights,
}
// A precomputed `NdotV` is provided because it is computed regardless,
// but `world_normal` and the view vector `V` are provided separately for more advanced uses.
fn ambient_light(
world_position: vec4<f32>,
world_normal: vec3<f32>,
V: vec3<f32>,
NdotV: f32,
diffuse_color: vec3<f32>,
specular_color: vec3<f32>,
perceptual_roughness: f32,
occlusion: vec3<f32>,
) -> vec3<f32> {
let diffuse_ambient = EnvBRDFApprox(diffuse_color, F_AB(1.0, NdotV));
let specular_ambient = EnvBRDFApprox(specular_color, F_AB(perceptual_roughness, NdotV));
// No real world material has specular values under 0.02, so we use this range as a
// "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control.
// See: https://google.github.io/filament/Filament.html#specularocclusion
let specular_occlusion = saturate(dot(specular_color, vec3(50.0 * 0.33)));
return (diffuse_ambient + specular_ambient * specular_occlusion) * lights.ambient_color.rgb * occlusion;
}

Some files were not shown because too many files have changed in this diff Show More