use crate::camera::Camera3d;
use crate::context::Context;
use crate::light::{CollectedLight, LightCollection, LightType, MAX_LIGHTS};
use crate::scene::SceneNode3d;
use bytemuck::{Pod, Zeroable};
use glamx::{Mat4, Vec3};
pub const MAX_SHADOW_VIEWS: usize = 16;
pub const MAX_SHADOW_LIGHTS: usize = MAX_LIGHTS + MAX_SHADOW_VIEWS;
pub const MAX_CASCADES: u32 = 4;
fn shadow_depth_bias() -> wgpu::DepthBiasState {
wgpu::DepthBiasState {
constant: 1,
slope_scale: 1.75,
clamp: 0.0,
}
}
fn light_near_far(light_pos: Vec3, aabb_min: Vec3, aabb_max: Vec3) -> (f32, f32) {
let nearest = light_pos.clamp(aabb_min, aabb_max);
let near_d = (nearest - light_pos).length();
let mut far_d: f32 = 0.0;
for &cx in &[aabb_min.x, aabb_max.x] {
for &cy in &[aabb_min.y, aabb_max.y] {
for &cz in &[aabb_min.z, aabb_max.z] {
far_d = far_d.max((Vec3::new(cx, cy, cz) - light_pos).length());
}
}
}
let near = (near_d * 0.9).max(0.02);
let far = (far_d * 1.1).max(near + 0.1);
(near, far)
}
fn fit_near_far(caster_aabb: Option<(Vec3, Vec3)>, light_pos: Vec3, radius: f32) -> (f32, f32) {
match caster_aabb {
Some((min, max)) => {
let (near, far) = light_near_far(light_pos, min, max);
(near, far.min(radius))
}
None => (0.05, radius),
}
}
fn near_corner_scale(fov: f32) -> f32 {
let t = (fov * 0.5).tan();
1.0 / (1.0 + 2.0 * t * t).sqrt()
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
struct GpuLightShadow {
base_view: u32,
num_views: u32,
light_type: u32,
enabled: f32,
light_pos: [f32; 3],
far_plane: f32,
}
impl Default for GpuLightShadow {
fn default() -> Self {
Self {
base_view: u32::MAX,
num_views: 0,
light_type: 0,
enabled: 0.0,
light_pos: [0.0; 3],
far_plane: 1.0,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
struct ShadowUniforms {
view_proj: [[[f32; 4]; 4]; MAX_SHADOW_VIEWS],
lights: [GpuLightShadow; MAX_SHADOW_LIGHTS],
shadows_enabled: f32,
texel_size: f32,
depth_bias: f32,
transmittance_enabled: f32,
softness: f32,
_pad0: f32,
_pad1: f32,
_pad2: f32,
cascade_splits: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
struct ShadowViewUniforms {
view_proj: [[f32; 4]; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
struct ShadowModelUniforms {
transform: [[f32; 4]; 4],
scale: [[f32; 4]; 3], color: [f32; 4],
}
const SHADOW_VIEW_STRIDE: u64 = 256;
const OPAQUE_ALPHA_THRESHOLD: f32 = 0.999;
const TRANSMITTANCE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
struct ShadowView {
layer: u32,
view_proj: Mat4,
}
pub struct ShadowMapper {
enabled: bool,
resolution: u32,
depth_bias: f32,
softness: f32,
num_cascades: u32,
shadow_distance: f32,
first_cascade_far_bound: f32,
atlas: wgpu::Texture,
layer_views: Vec<wgpu::TextureView>,
array_view: wgpu::TextureView,
compare_sampler: wgpu::Sampler,
transmittance_atlas: wgpu::Texture,
transmittance_layer_views: Vec<wgpu::TextureView>,
transmittance_array_view: wgpu::TextureView,
transmittance_sampler: wgpu::Sampler,
transmittance_pipeline: wgpu::RenderPipeline,
transmittance_tex_bgl: wgpu::BindGroupLayout,
uniform_buffer: wgpu::Buffer,
bind_group_layout: wgpu::BindGroupLayout,
bind_group: wgpu::BindGroup,
depth_pipeline: wgpu::RenderPipeline,
deform_depth_pipeline: Option<wgpu::RenderPipeline>,
deform_transmittance_pipeline: Option<wgpu::RenderPipeline>,
view_bind_group_layout: wgpu::BindGroupLayout,
view_uniform_buffer: wgpu::Buffer,
view_capacity: u64,
view_bind_group: wgpu::BindGroup,
model_bind_group_layout: wgpu::BindGroupLayout,
model_uniform_buffer: wgpu::Buffer,
model_capacity: u64,
model_bind_group: wgpu::BindGroup,
last_shadow_slots: Vec<u32>,
}
impl ShadowMapper {
pub fn new(resolution: u32) -> Self {
let ctxt = Context::get();
let resolution = resolution.max(1);
let (atlas, layer_views, array_view) = Self::create_atlas(&ctxt, resolution);
let (transmittance_atlas, transmittance_layer_views, transmittance_array_view) =
Self::create_transmittance_atlas(&ctxt, resolution);
let compare_sampler = ctxt.create_sampler(&wgpu::SamplerDescriptor {
label: Some("shadow_compare_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
compare: Some(wgpu::CompareFunction::LessEqual),
..Default::default()
});
let transmittance_sampler = ctxt.create_sampler(&wgpu::SamplerDescriptor {
label: Some("shadow_transmittance_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
label: Some("shadow_uniform_buffer"),
size: std::mem::size_of::<ShadowUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group_layout = shadow_bind_group_layout(&ctxt);
let bind_group = Self::create_bind_group(
&ctxt,
&bind_group_layout,
&array_view,
&compare_sampler,
&uniform_buffer,
&transmittance_array_view,
&transmittance_sampler,
);
let view_bind_group_layout =
ctxt.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("shadow_view_bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<
ShadowViewUniforms,
>()
as u64),
},
count: None,
}],
});
let model_bind_group_layout =
ctxt.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("shadow_model_bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<
ShadowModelUniforms,
>()
as u64),
},
count: None,
}],
});
let transmittance_tex_bgl =
ctxt.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("shadow_transmittance_tex_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let depth_pipeline =
Self::create_depth_pipeline(&ctxt, &view_bind_group_layout, &model_bind_group_layout);
let transmittance_pipeline = Self::create_transmittance_pipeline(
&ctxt,
&view_bind_group_layout,
&model_bind_group_layout,
&transmittance_tex_bgl,
);
let (deform_depth_pipeline, deform_transmittance_pipeline) = {
let deform_layout = crate::builtin::deform::deform_bind_group_layout();
let depth = Self::create_depth_pipeline_deform(
&ctxt,
&view_bind_group_layout,
&model_bind_group_layout,
&deform_layout,
);
let transmittance = Self::create_transmittance_pipeline_deform(
&ctxt,
&view_bind_group_layout,
&model_bind_group_layout,
&deform_layout,
&transmittance_tex_bgl,
);
(Some(depth), Some(transmittance))
};
let view_capacity = MAX_SHADOW_VIEWS as u64;
let view_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
label: Some("shadow_view_uniform_buffer"),
size: SHADOW_VIEW_STRIDE * view_capacity,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let view_bind_group = ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("shadow_view_bind_group"),
layout: &view_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &view_uniform_buffer,
offset: 0,
size: std::num::NonZeroU64::new(
std::mem::size_of::<ShadowViewUniforms>() as u64
),
}),
}],
});
let model_capacity = 64u64;
let model_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
label: Some("shadow_model_uniform_buffer"),
size: SHADOW_VIEW_STRIDE * model_capacity,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let model_bind_group = ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("shadow_model_bind_group"),
layout: &model_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &model_uniform_buffer,
offset: 0,
size: std::num::NonZeroU64::new(
std::mem::size_of::<ShadowModelUniforms>() as u64
),
}),
}],
});
Self {
enabled: true,
resolution,
depth_bias: 0.0012,
softness: 1.0,
num_cascades: 4,
shadow_distance: f32::INFINITY,
first_cascade_far_bound: 12.0,
atlas,
layer_views,
array_view,
compare_sampler,
transmittance_atlas,
transmittance_layer_views,
transmittance_array_view,
transmittance_sampler,
transmittance_pipeline,
transmittance_tex_bgl,
uniform_buffer,
bind_group_layout,
bind_group,
depth_pipeline,
deform_depth_pipeline,
deform_transmittance_pipeline,
view_bind_group_layout,
view_uniform_buffer,
view_capacity,
view_bind_group,
model_bind_group_layout,
model_uniform_buffer,
model_capacity,
model_bind_group,
last_shadow_slots: Vec::new(),
}
}
pub(crate) fn shadow_slots(&self) -> &[u32] {
&self.last_shadow_slots
}
fn shadow_view_count(&self, light: &CollectedLight) -> usize {
match light.light_type {
LightType::Point { .. } => 6,
LightType::Directional(_) => self.num_cascades as usize,
LightType::Spot { .. } => 1,
}
}
#[allow(clippy::too_many_arguments)]
fn build_light_shadow(
&self,
light: &CollectedLight,
camera: &dyn Camera3d,
base_view: u32,
splits: &[f32],
caster_aabb: Option<(Vec3, Vec3)>,
view_proj: &mut [[[f32; 4]; 4]; MAX_SHADOW_VIEWS],
views: &mut Vec<ShadowView>,
) -> GpuLightShadow {
let needed = self.shadow_view_count(light);
let light_type;
let mut far_plane = 1.0;
match light.light_type {
LightType::Directional(_) => {
light_type = 1;
let dir = light.world_direction.normalize_or(Vec3::NEG_Z);
for c in 0..self.num_cascades {
let vp = calculate_cascade(
dir,
camera,
splits[c as usize],
splits[c as usize + 1],
self.resolution,
);
let layer = base_view + c;
view_proj[layer as usize] = vp.to_cols_array_2d();
views.push(ShadowView {
layer,
view_proj: vp,
});
}
}
LightType::Spot {
outer_cone_angle,
attenuation_radius,
..
} => {
light_type = 2;
let radius = attenuation_radius.max(1.0);
let (near, far) = fit_near_far(caster_aabb, light.world_position, radius);
far_plane = far;
let dir = light.world_direction.normalize_or(Vec3::NEG_Z);
let fov = (outer_cone_angle * 2.0).clamp(0.1, std::f32::consts::PI - 0.05);
let near = near * near_corner_scale(fov);
let vp = perspective_view_proj(
light.world_position,
light.world_position + dir,
fov,
near,
far,
);
view_proj[base_view as usize] = vp.to_cols_array_2d();
views.push(ShadowView {
layer: base_view,
view_proj: vp,
});
}
LightType::Point { attenuation_radius } => {
light_type = 0;
let radius = attenuation_radius.max(1.0);
let (near, far) = fit_near_far(caster_aabb, light.world_position, radius);
far_plane = far;
let near = near * near_corner_scale(std::f32::consts::FRAC_PI_2);
let faces = cube_face_view_projs(light.world_position, near, far);
for (face_idx, vp) in faces.iter().enumerate() {
let layer = base_view + face_idx as u32;
view_proj[layer as usize] = vp.to_cols_array_2d();
views.push(ShadowView {
layer,
view_proj: *vp,
});
}
}
}
GpuLightShadow {
base_view,
num_views: needed as u32,
light_type,
enabled: 1.0,
light_pos: light.world_position.into(),
far_plane,
}
}
fn create_atlas(
ctxt: &Context,
resolution: u32,
) -> (wgpu::Texture, Vec<wgpu::TextureView>, wgpu::TextureView) {
let atlas = ctxt.create_texture(&wgpu::TextureDescriptor {
label: Some("shadow_atlas"),
size: wgpu::Extent3d {
width: resolution,
height: resolution,
depth_or_array_layers: MAX_SHADOW_VIEWS as u32,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let layer_views = (0..MAX_SHADOW_VIEWS as u32)
.map(|layer| {
atlas.create_view(&wgpu::TextureViewDescriptor {
label: Some("shadow_atlas_layer"),
dimension: Some(wgpu::TextureViewDimension::D2),
base_array_layer: layer,
array_layer_count: Some(1),
..Default::default()
})
})
.collect();
let array_view = atlas.create_view(&wgpu::TextureViewDescriptor {
label: Some("shadow_atlas_array_view"),
dimension: Some(wgpu::TextureViewDimension::D2Array),
..Default::default()
});
(atlas, layer_views, array_view)
}
fn create_transmittance_atlas(
ctxt: &Context,
resolution: u32,
) -> (wgpu::Texture, Vec<wgpu::TextureView>, wgpu::TextureView) {
let atlas = ctxt.create_texture(&wgpu::TextureDescriptor {
label: Some("shadow_transmittance_atlas"),
size: wgpu::Extent3d {
width: resolution,
height: resolution,
depth_or_array_layers: MAX_SHADOW_VIEWS as u32,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TRANSMITTANCE_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let layer_views = (0..MAX_SHADOW_VIEWS as u32)
.map(|layer| {
atlas.create_view(&wgpu::TextureViewDescriptor {
label: Some("shadow_transmittance_layer"),
dimension: Some(wgpu::TextureViewDimension::D2),
base_array_layer: layer,
array_layer_count: Some(1),
..Default::default()
})
})
.collect();
let array_view = atlas.create_view(&wgpu::TextureViewDescriptor {
label: Some("shadow_transmittance_array_view"),
dimension: Some(wgpu::TextureViewDimension::D2Array),
..Default::default()
});
(atlas, layer_views, array_view)
}
#[allow(clippy::too_many_arguments)]
fn create_bind_group(
ctxt: &Context,
layout: &wgpu::BindGroupLayout,
array_view: &wgpu::TextureView,
sampler: &wgpu::Sampler,
uniform_buffer: &wgpu::Buffer,
transmittance_view: &wgpu::TextureView,
transmittance_sampler: &wgpu::Sampler,
) -> wgpu::BindGroup {
ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("shadow_bind_group"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(array_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(transmittance_view),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(transmittance_sampler),
},
],
})
}
fn create_depth_pipeline(
ctxt: &Context,
view_bind_group_layout: &wgpu::BindGroupLayout,
model_bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = ctxt.create_shader_module(
Some("shadow_depth_shader"),
&crate::builtin::compile_wesl(
&[("package::shadow_depth", crate::builtin::SHADOW_DEPTH_WESL)],
"package::shadow_depth",
&[("skinned", false)],
),
);
let layout = ctxt.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("shadow_depth_pipeline_layout"),
bind_group_layouts: &[Some(view_bind_group_layout), Some(model_bind_group_layout)],
immediate_size: 0,
});
let vertex_buffer_layouts = [
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}],
},
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
}],
},
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 9]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 2,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 12,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 24,
shader_location: 4,
format: wgpu::VertexFormat::Float32x3,
},
],
},
];
ctxt.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("shadow_depth_pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &vertex_buffer_layouts,
compilation_options: Default::default(),
},
fragment: None,
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(),
bias: shadow_depth_bias(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}
fn create_depth_pipeline_deform(
ctxt: &Context,
view_bind_group_layout: &wgpu::BindGroupLayout,
model_bind_group_layout: &wgpu::BindGroupLayout,
deform_bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = ctxt.create_shader_module(
Some("shadow_depth_deform_shader"),
&crate::builtin::compile_wesl(
&[("package::shadow_depth", crate::builtin::SHADOW_DEPTH_WESL)],
"package::shadow_depth",
&[("skinned", true)],
),
);
let layout = ctxt.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("shadow_depth_deform_pipeline_layout"),
bind_group_layouts: &[
Some(view_bind_group_layout),
Some(model_bind_group_layout),
Some(deform_bind_group_layout),
],
immediate_size: 0,
});
let vertex_buffer_layouts = [
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}],
},
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
}],
},
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 9]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 2,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 12,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 24,
shader_location: 4,
format: wgpu::VertexFormat::Float32x3,
},
],
},
];
ctxt.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("shadow_depth_deform_pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &vertex_buffer_layouts,
compilation_options: Default::default(),
},
fragment: None,
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(),
bias: shadow_depth_bias(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}
fn create_transmittance_pipeline_deform(
ctxt: &Context,
view_bind_group_layout: &wgpu::BindGroupLayout,
model_bind_group_layout: &wgpu::BindGroupLayout,
deform_bind_group_layout: &wgpu::BindGroupLayout,
tex_bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = ctxt.create_shader_module(
Some("shadow_transmittance_deform_shader"),
&crate::builtin::compile_wesl(
&[(
"package::shadow_transmittance",
crate::builtin::SHADOW_TRANSMITTANCE_WESL,
)],
"package::shadow_transmittance",
&[("skinned", true)],
),
);
let layout = ctxt.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("shadow_transmittance_deform_pipeline_layout"),
bind_group_layouts: &[
Some(view_bind_group_layout),
Some(model_bind_group_layout),
Some(deform_bind_group_layout),
Some(tex_bind_group_layout),
],
immediate_size: 0,
});
let vertex_buffer_layouts = [
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}],
},
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
}],
},
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 9]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 2,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 12,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 24,
shader_location: 4,
format: wgpu::VertexFormat::Float32x3,
},
],
},
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 5,
format: wgpu::VertexFormat::Float32x2,
}],
},
];
let mult_blend = wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Dst,
dst_factor: wgpu::BlendFactor::Zero,
operation: wgpu::BlendOperation::Add,
};
ctxt.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("shadow_transmittance_skinned_pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &vertex_buffer_layouts,
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: TRANSMITTANCE_FORMAT,
blend: Some(wgpu::BlendState {
color: mult_blend,
alpha: mult_blend,
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}
fn create_transmittance_pipeline(
ctxt: &Context,
view_bind_group_layout: &wgpu::BindGroupLayout,
model_bind_group_layout: &wgpu::BindGroupLayout,
tex_bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = ctxt.create_shader_module(
Some("shadow_transmittance_shader"),
&crate::builtin::compile_wesl(
&[(
"package::shadow_transmittance",
crate::builtin::SHADOW_TRANSMITTANCE_WESL,
)],
"package::shadow_transmittance",
&[("skinned", false)],
),
);
let layout = ctxt.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("shadow_transmittance_pipeline_layout"),
bind_group_layouts: &[
Some(view_bind_group_layout),
Some(model_bind_group_layout),
Some(tex_bind_group_layout),
],
immediate_size: 0,
});
let vertex_buffer_layouts = [
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}],
},
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
}],
},
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 9]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 2,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 12,
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: 24,
shader_location: 4,
format: wgpu::VertexFormat::Float32x3,
},
],
},
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
offset: 0,
shader_location: 7,
format: wgpu::VertexFormat::Float32x2,
}],
},
];
let mult_blend = wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Dst,
dst_factor: wgpu::BlendFactor::Zero,
operation: wgpu::BlendOperation::Add,
};
ctxt.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("shadow_transmittance_pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &vertex_buffer_layouts,
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: TRANSMITTANCE_FORMAT,
blend: Some(wgpu::BlendState {
color: mult_blend,
alpha: mult_blend,
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}
pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
&self.bind_group_layout
}
pub fn bind_group(&self) -> &wgpu::BindGroup {
&self.bind_group
}
pub fn resources(&self) -> crate::resource::ShadowResources {
crate::resource::ShadowResources {
atlas: self.array_view.clone(),
compare_sampler: self.compare_sampler.clone(),
uniform: self.uniform_buffer.clone(),
transmittance: self.transmittance_array_view.clone(),
transmittance_sampler: self.transmittance_sampler.clone(),
}
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn softness(&self) -> f32 {
self.softness
}
pub fn set_softness(&mut self, softness: f32) {
self.softness = softness.max(0.0);
}
pub fn resolution(&self) -> u32 {
self.resolution
}
pub fn num_cascades(&self) -> u32 {
self.num_cascades
}
pub fn set_num_cascades(&mut self, num_cascades: u32) {
self.num_cascades = num_cascades.clamp(1, MAX_CASCADES);
}
pub fn shadow_distance(&self) -> f32 {
self.shadow_distance
}
pub fn set_shadow_distance(&mut self, distance: f32) {
self.shadow_distance = distance.max(0.0);
}
pub fn set_first_cascade_far_bound(&mut self, bound: f32) {
self.first_cascade_far_bound = bound.max(0.01);
}
pub fn set_resolution(&mut self, resolution: u32) {
let resolution = resolution.max(1);
if resolution == self.resolution {
return;
}
let ctxt = Context::get();
self.resolution = resolution;
let (atlas, layer_views, array_view) = Self::create_atlas(&ctxt, resolution);
self.atlas = atlas;
self.layer_views = layer_views;
self.array_view = array_view;
let (t_atlas, t_layer_views, t_array_view) =
Self::create_transmittance_atlas(&ctxt, resolution);
self.transmittance_atlas = t_atlas;
self.transmittance_layer_views = t_layer_views;
self.transmittance_array_view = t_array_view;
self.bind_group = Self::create_bind_group(
&ctxt,
&self.bind_group_layout,
&self.array_view,
&self.compare_sampler,
&self.uniform_buffer,
&self.transmittance_array_view,
&self.transmittance_sampler,
);
}
pub(crate) fn render(
&mut self,
scene: &mut SceneNode3d,
camera: &dyn Camera3d,
lights: &LightCollection,
encoder: &mut wgpu::CommandEncoder,
gpu: &mut crate::renderer::timings::GpuTimer,
) {
let ctxt = Context::get();
let mut uniforms = ShadowUniforms {
view_proj: [[[0.0; 4]; 4]; MAX_SHADOW_VIEWS],
lights: [GpuLightShadow::default(); MAX_SHADOW_LIGHTS],
shadows_enabled: 0.0,
texel_size: 1.0 / self.resolution as f32,
depth_bias: self.depth_bias,
transmittance_enabled: 0.0,
softness: self.softness,
_pad0: 0.0,
_pad1: 0.0,
_pad2: 0.0,
cascade_splits: [f32::MAX; 4],
};
if !self.enabled {
ctxt.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
return;
}
let splits = cascade_splits(
camera,
self.shadow_distance,
self.first_cascade_far_bound,
self.num_cascades,
);
let n = self.num_cascades.min(4) as usize;
uniforms.cascade_splits[..n].copy_from_slice(&splits[1..=n]);
let mut views: Vec<ShadowView> = Vec::new();
let mut next_layer = 0u32;
let needs_aabb = lights.lights.iter().any(|l| {
l.casts_shadows
&& matches!(
l.light_type,
LightType::Point { .. } | LightType::Spot { .. }
)
});
let caster_aabb = if needs_aabb {
scene.data().shadow_casters_world_aabb()
} else {
None
};
let (primary, clustered) = lights.split_primary_clustered();
let mut shadow_slots = vec![u32::MAX; lights.lights.len()];
for (slot, &li) in primary.iter().enumerate() {
let light = &lights.lights[li];
if !light.casts_shadows {
continue;
}
let needed = self.shadow_view_count(light);
if next_layer as usize + needed > MAX_SHADOW_VIEWS {
continue;
}
uniforms.lights[slot] = self.build_light_shadow(
light,
camera,
next_layer,
&splits,
caster_aabb,
&mut uniforms.view_proj,
&mut views,
);
shadow_slots[li] = slot as u32;
next_layer += needed as u32;
}
let mut slot = MAX_LIGHTS;
for &li in &clustered {
if slot >= MAX_SHADOW_LIGHTS {
break;
}
let light = &lights.lights[li];
if !light.casts_shadows {
continue;
}
let needed = self.shadow_view_count(light);
if next_layer as usize + needed > MAX_SHADOW_VIEWS {
continue;
}
uniforms.lights[slot] = self.build_light_shadow(
light,
camera,
next_layer,
&splits,
caster_aabb,
&mut uniforms.view_proj,
&mut views,
);
shadow_slots[li] = slot as u32;
next_layer += needed as u32;
slot += 1;
}
self.last_shadow_slots = shadow_slots;
if views.is_empty() {
ctxt.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
return;
}
uniforms.shadows_enabled = 1.0;
let mut models: Vec<ShadowModelUniforms> = Vec::new();
let mut has_transparent = false;
scene
.data()
.collect_shadow_models(&mut |transform, scale, color| {
has_transparent |= color.a < OPAQUE_ALPHA_THRESHOLD;
let scale_mat = glamx::Mat3::from_diagonal(scale).to_cols_array_2d();
models.push(ShadowModelUniforms {
transform: transform.to_mat4().to_cols_array_2d(),
scale: [
[scale_mat[0][0], scale_mat[0][1], scale_mat[0][2], 0.0],
[scale_mat[1][0], scale_mat[1][1], scale_mat[1][2], 0.0],
[scale_mat[2][0], scale_mat[2][1], scale_mat[2][2], 0.0],
],
color: [color.r, color.g, color.b, color.a],
});
});
if models.is_empty() {
uniforms.shadows_enabled = 0.0;
ctxt.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
return;
}
self.ensure_view_capacity(views.len() as u64);
for view in &views {
let view_uniforms = ShadowViewUniforms {
view_proj: view.view_proj.to_cols_array_2d(),
};
ctxt.write_buffer(
&self.view_uniform_buffer,
view.layer as u64 * SHADOW_VIEW_STRIDE,
bytemuck::bytes_of(&view_uniforms),
);
}
self.ensure_model_capacity(models.len() as u64);
for (idx, model) in models.iter().enumerate() {
ctxt.write_buffer(
&self.model_uniform_buffer,
idx as u64 * SHADOW_VIEW_STRIDE,
bytemuck::bytes_of(model),
);
}
for view in &views {
let depth_view = &self.layer_views[view.layer as usize];
let offset = (view.layer as u64 * SHADOW_VIEW_STRIDE) as u32;
{
let depth_ts = gpu.render_scope("shadows");
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("shadow_depth_pass"),
color_attachments: &[],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: depth_ts,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&self.depth_pipeline);
pass.set_bind_group(0, &self.view_bind_group, &[offset]);
let deform = self.deform_depth_pipeline.as_ref();
let mut object_index = 0u32;
scene.data_mut().render_shadow_casters(
&mut pass,
&self.depth_pipeline,
deform,
None, &self.model_bind_group,
SHADOW_VIEW_STRIDE as u32,
&mut object_index,
false,
OPAQUE_ALPHA_THRESHOLD,
);
}
if has_transparent {
let transmittance_view = &self.transmittance_layer_views[view.layer as usize];
let transmittance_ts = gpu.render_scope("shadows");
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("shadow_transmittance_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: transmittance_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: transmittance_ts,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&self.transmittance_pipeline);
pass.set_bind_group(0, &self.view_bind_group, &[offset]);
let deform = self.deform_transmittance_pipeline.as_ref();
let mut object_index = 0u32;
scene.data_mut().render_shadow_casters(
&mut pass,
&self.transmittance_pipeline,
deform,
Some(&self.transmittance_tex_bgl), &self.model_bind_group,
SHADOW_VIEW_STRIDE as u32,
&mut object_index,
true,
OPAQUE_ALPHA_THRESHOLD,
);
}
}
uniforms.transmittance_enabled = if has_transparent { 1.0 } else { 0.0 };
ctxt.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniforms));
}
fn ensure_model_capacity(&mut self, needed: u64) {
if needed <= self.model_capacity {
return;
}
let ctxt = Context::get();
let new_capacity = needed.next_power_of_two();
self.model_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
label: Some("shadow_model_uniform_buffer"),
size: SHADOW_VIEW_STRIDE * new_capacity,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.model_bind_group = ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("shadow_model_bind_group"),
layout: &self.model_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &self.model_uniform_buffer,
offset: 0,
size: std::num::NonZeroU64::new(
std::mem::size_of::<ShadowModelUniforms>() as u64
),
}),
}],
});
self.model_capacity = new_capacity;
}
fn ensure_view_capacity(&mut self, needed: u64) {
if needed <= self.view_capacity {
return;
}
let ctxt = Context::get();
let new_capacity = needed.next_power_of_two();
self.view_uniform_buffer = ctxt.create_buffer(&wgpu::BufferDescriptor {
label: Some("shadow_view_uniform_buffer"),
size: SHADOW_VIEW_STRIDE * new_capacity,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.view_bind_group = ctxt.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("shadow_view_bind_group"),
layout: &self.view_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &self.view_uniform_buffer,
offset: 0,
size: std::num::NonZeroU64::new(
std::mem::size_of::<ShadowViewUniforms>() as u64
),
}),
}],
});
self.view_capacity = new_capacity;
}
}
pub fn shadow_uniforms_size() -> u64 {
std::mem::size_of::<ShadowUniforms>() as u64
}
pub fn shadow_bind_group_layout(ctxt: &Context) -> wgpu::BindGroupLayout {
ctxt.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("shadow_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2Array,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2Array,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
})
}
fn frustum_rays(camera: &dyn Camera3d) -> (Vec3, [Vec3; 4], Vec3) {
let unit = glamx::Vec2::ONE;
let dirs = [
camera.unproject(glamx::Vec2::new(0.0, 0.0), unit).1,
camera.unproject(glamx::Vec2::new(1.0, 0.0), unit).1,
camera.unproject(glamx::Vec2::new(0.0, 1.0), unit).1,
camera.unproject(glamx::Vec2::new(1.0, 1.0), unit).1,
];
let forward = camera.unproject(glamx::Vec2::splat(0.5), unit).1;
(camera.eye(), dirs, forward)
}
fn cascade_splits(
camera: &dyn Camera3d,
distance_cap: f32,
first_bound: f32,
num_cascades: u32,
) -> Vec<f32> {
let (znear, zfar) = camera.clip_planes();
let near_d = znear.max(1e-3);
let far_d = zfar.min(distance_cap).max(near_d * 2.0);
if num_cascades <= 1 {
return vec![near_d, far_d];
}
let first = first_bound.clamp(near_d * 2.0, far_d);
let ratio = far_d / first;
let n1 = (num_cascades - 1) as f32;
let mut splits = Vec::with_capacity(num_cascades as usize + 1);
splits.push(near_d);
splits.push(first);
for i in 1..num_cascades {
splits.push(first * ratio.powf(i as f32 / n1));
}
splits
}
fn calculate_cascade(
dir: Vec3,
camera: &dyn Camera3d,
near_depth: f32,
far_depth: f32,
resolution: u32,
) -> Mat4 {
let dir = dir.normalize_or(Vec3::NEG_Z);
let (eye, dirs, forward) = frustum_rays(camera);
let mut corners = [Vec3::ZERO; 8];
for (i, d) in dirs.iter().enumerate() {
let cos = d.dot(forward).max(1e-3);
corners[i] = eye + *d * (near_depth / cos);
corners[i + 4] = eye + *d * (far_depth / cos);
}
let center = corners.iter().copied().fold(Vec3::ZERO, |a, c| a + c) / 8.0;
let mut max_r = 0.05_f32;
for c in &corners {
max_r = max_r.max((*c - center).length());
}
let radius = (2.0 * max_r).ceil() * 0.5;
let up = if dir.abs().dot(Vec3::Y) > 0.99 {
Vec3::X
} else {
Vec3::Y
};
let margin = 0.5 * radius;
let light_eye = center - dir * (radius + margin);
let view = Mat4::look_at_rh(light_eye, center, up);
let near = 0.0_f32;
let far = 2.0 * radius + 2.0 * margin;
let proj = Mat4::orthographic_rh(-radius, radius, -radius, radius, near, far);
let view_proj = proj * view;
let half_res = resolution.max(1) as f32 * 0.5;
let origin = view_proj * glamx::Vec4::new(0.0, 0.0, 0.0, 1.0);
let rounded_x = (origin.x * half_res).round() / half_res;
let rounded_y = (origin.y * half_res).round() / half_res;
let snap = Mat4::from_translation(Vec3::new(rounded_x - origin.x, rounded_y - origin.y, 0.0));
snap * view_proj
}
fn perspective_view_proj(eye: Vec3, target: Vec3, fov: f32, near: f32, far: f32) -> Mat4 {
let fwd = (target - eye).normalize_or(Vec3::NEG_Z);
let up = if fwd.abs().dot(Vec3::Y) > 0.99 {
Vec3::X
} else {
Vec3::Y
};
let view = Mat4::look_at_rh(eye, eye + fwd, up);
let proj = Mat4::perspective_rh(fov, 1.0, near, far);
proj * view
}
fn cube_face_view_projs(eye: Vec3, near: f32, far: f32) -> [Mat4; 6] {
let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, near, far);
let dirs_ups = [
(Vec3::X, Vec3::NEG_Y),
(Vec3::NEG_X, Vec3::NEG_Y),
(Vec3::Y, Vec3::Z),
(Vec3::NEG_Y, Vec3::NEG_Z),
(Vec3::Z, Vec3::NEG_Y),
(Vec3::NEG_Z, Vec3::NEG_Y),
];
let mut out = [Mat4::IDENTITY; 6];
for (i, (dir, up)) in dirs_ups.iter().enumerate() {
let view = Mat4::look_at_rh(eye, eye + *dir, *up);
out[i] = proj * view;
}
out
}