use crate::gpu_types::Vertex;
use crate::pipeline::{load_shader, load_shader_composed, SceneState};
pub const GBUFFER_ALBEDO_METALLIC_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
pub const GBUFFER_NORMAL_ROUGHNESS_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
pub const GBUFFER_WORLD_POSITION_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
pub const GBUFFER_WORLD_TANGENT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
pub struct DeferredState {
pub albedo_metallic_tex: wgpu::Texture,
pub albedo_metallic_view: wgpu::TextureView,
pub normal_roughness_tex: wgpu::Texture,
pub normal_roughness_view: wgpu::TextureView,
pub world_position_tex: wgpu::Texture,
pub world_position_view: wgpu::TextureView,
pub world_tangent_tex: wgpu::Texture,
pub world_tangent_view: wgpu::TextureView,
pub gbuffer_pipeline: wgpu::RenderPipeline,
pub gbuffer_double_sided_pipeline: wgpu::RenderPipeline,
pub z_prepass_pipeline: wgpu::RenderPipeline,
pub z_prepass_double_sided_pipeline: wgpu::RenderPipeline,
pub lighting_pipeline: wgpu::RenderPipeline,
pub gbuffer_bind_group_layout: wgpu::BindGroupLayout,
pub gbuffer_bind_group: wgpu::BindGroup,
pub gbuf_sampler: wgpu::Sampler,
pub width: u32,
pub height: u32,
}
impl DeferredState {
pub fn new(device: &wgpu::Device, scene: &SceneState, width: u32, height: u32) -> Self {
let (
albedo_metallic_tex,
albedo_metallic_view,
normal_roughness_tex,
normal_roughness_view,
world_position_tex,
world_position_view,
world_tangent_tex,
world_tangent_view,
gbuf_sampler,
) = Self::create_gbuffer_textures(device, width, height);
let gbuffer_bind_group_layout = Self::create_gbuffer_layout(device);
let gbuffer_bind_group = Self::create_gbuffer_bind_group(
device,
&gbuffer_bind_group_layout,
&albedo_metallic_view,
&normal_roughness_view,
&world_position_view,
&world_tangent_view,
&gbuf_sampler,
);
let z_prepass_pipeline =
Self::create_z_prepass_pipeline(device, scene, Some(wgpu::Face::Back), "Z-Prepass Pipeline");
let z_prepass_double_sided_pipeline =
Self::create_z_prepass_pipeline(device, scene, None, "Z-Prepass TwoSided Pipeline");
let gbuffer_pipeline =
Self::create_gbuffer_pipeline(device, scene, Some(wgpu::Face::Back), "GBuffer Pipeline");
let gbuffer_double_sided_pipeline =
Self::create_gbuffer_pipeline(device, scene, None, "GBuffer TwoSided Pipeline");
let lighting_pipeline =
Self::create_lighting_pipeline(device, scene, &gbuffer_bind_group_layout);
Self {
albedo_metallic_tex,
albedo_metallic_view,
normal_roughness_tex,
normal_roughness_view,
world_position_tex,
world_position_view,
world_tangent_tex,
world_tangent_view,
gbuffer_pipeline,
gbuffer_double_sided_pipeline,
z_prepass_pipeline,
z_prepass_double_sided_pipeline,
lighting_pipeline,
gbuffer_bind_group_layout,
gbuffer_bind_group,
gbuf_sampler,
width,
height,
}
}
pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
if self.width == width && self.height == height {
return;
}
let (
albedo_metallic_tex,
albedo_metallic_view,
normal_roughness_tex,
normal_roughness_view,
world_position_tex,
world_position_view,
world_tangent_tex,
world_tangent_view,
gbuf_sampler,
) = Self::create_gbuffer_textures(device, width, height);
self.gbuffer_bind_group = Self::create_gbuffer_bind_group(
device,
&self.gbuffer_bind_group_layout,
&albedo_metallic_view,
&normal_roughness_view,
&world_position_view,
&world_tangent_view,
&gbuf_sampler,
);
self.albedo_metallic_tex = albedo_metallic_tex;
self.albedo_metallic_view = albedo_metallic_view;
self.normal_roughness_tex = normal_roughness_tex;
self.normal_roughness_view = normal_roughness_view;
self.world_position_tex = world_position_tex;
self.world_position_view = world_position_view;
self.world_tangent_tex = world_tangent_tex;
self.world_tangent_view = world_tangent_view;
self.gbuf_sampler = gbuf_sampler;
self.width = width;
self.height = height;
}
fn create_gbuffer_textures(
device: &wgpu::Device,
w: u32,
h: u32,
) -> (
wgpu::Texture,
wgpu::TextureView,
wgpu::Texture,
wgpu::TextureView,
wgpu::Texture,
wgpu::TextureView,
wgpu::Texture,
wgpu::TextureView,
wgpu::Sampler,
) {
let mk = |label: &str, fmt: wgpu::TextureFormat| {
let t = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: fmt,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let v = t.create_view(&wgpu::TextureViewDescriptor::default());
(t, v)
};
let (a, av) = mk("gbuf_albedo_metallic", GBUFFER_ALBEDO_METALLIC_FORMAT);
let (n, nv) = mk("gbuf_normal_roughness", GBUFFER_NORMAL_ROUGHNESS_FORMAT);
let (p, pv) = mk("gbuf_world_position", GBUFFER_WORLD_POSITION_FORMAT);
let (t, tv) = mk("gbuf_world_tangent", GBUFFER_WORLD_TANGENT_FORMAT);
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
(a, av, n, nv, p, pv, t, tv, sampler)
}
fn create_gbuffer_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("gbuffer_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
],
})
}
fn create_gbuffer_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
albedo_v: &wgpu::TextureView,
normal_v: &wgpu::TextureView,
pos_v: &wgpu::TextureView,
tangent_v: &wgpu::TextureView,
sampler: &wgpu::Sampler,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("gbuffer_bind_group"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(albedo_v),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(normal_v),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(pos_v),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(tangent_v),
},
],
})
}
fn create_gbuffer_pipeline(
device: &wgpu::Device,
scene: &SceneState,
cull_mode: Option<wgpu::Face>,
label: &str,
) -> wgpu::RenderPipeline {
let shader = load_shader(
device,
"demo/assets/shaders/gbuffer.wgsl",
include_str!("shaders/gbuffer.wgsl"),
"GBuffer Shader",
);
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("GBuffer Pipeline Layout"),
bind_group_layouts: &[
Some(&scene.global_bind_group_layout), Some(&scene.texture_bind_group_layout), Some(&scene.shadow_bind_group_layout), Some(&scene.skeleton_bind_group_layout), Some(&scene.instance_bind_group_layout), ],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[Vertex::desc()],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[
Some(wgpu::ColorTargetState {
format: GBUFFER_ALBEDO_METALLIC_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
Some(wgpu::ColorTargetState {
format: GBUFFER_NORMAL_ROUGHNESS_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
Some(wgpu::ColorTargetState {
format: GBUFFER_WORLD_POSITION_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
Some(wgpu::ColorTargetState {
format: GBUFFER_WORLD_TANGENT_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Ccw,
cull_mode,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
}
fn create_z_prepass_pipeline(
device: &wgpu::Device,
scene: &SceneState,
cull_mode: Option<wgpu::Face>,
label: &str,
) -> wgpu::RenderPipeline {
let shader = load_shader(
device,
"demo/assets/shaders/gbuffer.wgsl",
include_str!("shaders/gbuffer.wgsl"),
"Z-Prepass Shader",
);
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Z-Prepass Pipeline Layout"),
bind_group_layouts: &[
Some(&scene.global_bind_group_layout), Some(&scene.texture_bind_group_layout), Some(&scene.shadow_bind_group_layout), Some(&scene.skeleton_bind_group_layout), Some(&scene.instance_bind_group_layout), ],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[Vertex::desc()],
},
fragment: None, primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Ccw,
cull_mode,
..Default::default()
},
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: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
}
fn create_lighting_pipeline(
device: &wgpu::Device,
scene: &SceneState,
gbuffer_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = load_shader_composed(
device,
"demo/assets/shaders/deferred_lighting.wgsl",
include_str!("shaders/deferred_lighting.wgsl"),
"Deferred Lighting Shader",
);
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Deferred Lighting Layout"),
bind_group_layouts: &[
Some(&scene.global_bind_group_layout), Some(&scene.shadow_bind_group_layout), Some(gbuffer_layout), ],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Deferred Lighting Pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[], },
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba16Float,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: None, multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
}
}
#[cfg(test)]
mod gbuffer_packing_tests {
fn pack(subsurface: f32, anisotropy: f32) -> f32 {
(0.5 + 0.49 * anisotropy) + (100.0 * subsurface).floor()
}
fn unpack(w: f32) -> (f32, f32) {
let subsurface = w.floor() / 100.0;
let anisotropy = ((w - w.floor() - 0.5) / 0.49).clamp(0.0, 1.0);
(subsurface, anisotropy)
}
#[test]
fn subsurface_and_anisotropy_survive_the_round_trip() {
for &s in &[0.0f32, 0.07, 0.234, 0.5, 0.777, 0.999, 1.0] {
for &a in &[0.0f32, 0.25, 0.5, 1.0] {
let (ds, da) = unpack(pack(s, a));
assert!(
(ds - s).abs() <= 0.011,
"subsurface {s} came back as {ds} (anisotropy {a})"
);
assert!(
(da - a).abs() <= 0.02,
"anisotropy {a} came back as {da} — subsurface {s} is leaking into its slot"
);
}
}
}
}