Skip to main content

gizmo_renderer/
deferred.rs

1use crate::gpu_types::Vertex;
2use crate::pipeline::{load_shader, SceneState};
3
4/// G-Buffer textures, pipelines and bind groups for the deferred rendering path.
5pub struct DeferredState {
6    // G-buffer colour targets
7    pub albedo_metallic_tex: wgpu::Texture,
8    pub albedo_metallic_view: wgpu::TextureView,
9    pub normal_roughness_tex: wgpu::Texture,
10    pub normal_roughness_view: wgpu::TextureView,
11    pub world_position_tex: wgpu::Texture,
12    pub world_position_view: wgpu::TextureView,
13    pub world_tangent_tex: wgpu::Texture,
14    pub world_tangent_view: wgpu::TextureView,
15
16    // Geometry pass (writes to 4 MRTs)
17    pub gbuffer_pipeline: wgpu::RenderPipeline,
18
19    // Z-Prepass (Depth only)
20    pub z_prepass_pipeline: wgpu::RenderPipeline,
21
22    // Lighting pass (fullscreen triangle → HDR texture)
23    pub lighting_pipeline: wgpu::RenderPipeline,
24
25    // Bind group used by the lighting pass to read the G-buffers
26    pub gbuffer_bind_group_layout: wgpu::BindGroupLayout,
27    pub gbuffer_bind_group: wgpu::BindGroup,
28    pub gbuf_sampler: wgpu::Sampler,
29
30    pub width: u32,
31    pub height: u32,
32}
33
34impl DeferredState {
35    pub fn new(device: &wgpu::Device, scene: &SceneState, width: u32, height: u32) -> Self {
36        let (
37            albedo_metallic_tex,
38            albedo_metallic_view,
39            normal_roughness_tex,
40            normal_roughness_view,
41            world_position_tex,
42            world_position_view,
43            world_tangent_tex,
44            world_tangent_view,
45            gbuf_sampler,
46        ) = Self::create_gbuffer_textures(device, width, height);
47
48        let gbuffer_bind_group_layout = Self::create_gbuffer_layout(device);
49
50        let gbuffer_bind_group = Self::create_gbuffer_bind_group(
51            device,
52            &gbuffer_bind_group_layout,
53            &albedo_metallic_view,
54            &normal_roughness_view,
55            &world_position_view,
56            &world_tangent_view,
57            &gbuf_sampler,
58        );
59
60        let z_prepass_pipeline = Self::create_z_prepass_pipeline(device, scene);
61        let gbuffer_pipeline = Self::create_gbuffer_pipeline(device, scene);
62        let lighting_pipeline =
63            Self::create_lighting_pipeline(device, scene, &gbuffer_bind_group_layout);
64
65        Self {
66            albedo_metallic_tex,
67            albedo_metallic_view,
68            normal_roughness_tex,
69            normal_roughness_view,
70            world_position_tex,
71            world_position_view,
72            world_tangent_tex,
73            world_tangent_view,
74            gbuffer_pipeline,
75            z_prepass_pipeline,
76            lighting_pipeline,
77            gbuffer_bind_group_layout,
78            gbuffer_bind_group,
79            gbuf_sampler,
80            width,
81            height,
82        }
83    }
84
85    /// Recreate G-buffer textures and bind groups when the window is resized.
86    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
87        if self.width == width && self.height == height {
88            return;
89        }
90        let (
91            albedo_metallic_tex,
92            albedo_metallic_view,
93            normal_roughness_tex,
94            normal_roughness_view,
95            world_position_tex,
96            world_position_view,
97            world_tangent_tex,
98            world_tangent_view,
99            gbuf_sampler,
100        ) = Self::create_gbuffer_textures(device, width, height);
101
102        self.gbuffer_bind_group = Self::create_gbuffer_bind_group(
103            device,
104            &self.gbuffer_bind_group_layout,
105            &albedo_metallic_view,
106            &normal_roughness_view,
107            &world_position_view,
108            &world_tangent_view,
109            &gbuf_sampler,
110        );
111
112        self.albedo_metallic_tex = albedo_metallic_tex;
113        self.albedo_metallic_view = albedo_metallic_view;
114        self.normal_roughness_tex = normal_roughness_tex;
115        self.normal_roughness_view = normal_roughness_view;
116        self.world_position_tex = world_position_tex;
117        self.world_position_view = world_position_view;
118        self.world_tangent_tex = world_tangent_tex;
119        self.world_tangent_view = world_tangent_view;
120        self.gbuf_sampler = gbuf_sampler;
121        self.width = width;
122        self.height = height;
123    }
124
125    // ── helpers ─────────────────────────────────────────────────────────────
126
127    fn create_gbuffer_textures(
128        device: &wgpu::Device,
129        w: u32,
130        h: u32,
131    ) -> (
132        wgpu::Texture,
133        wgpu::TextureView,
134        wgpu::Texture,
135        wgpu::TextureView,
136        wgpu::Texture,
137        wgpu::TextureView,
138        wgpu::Texture,
139        wgpu::TextureView,
140        wgpu::Sampler,
141    ) {
142        let mk = |label: &str, fmt: wgpu::TextureFormat| {
143            let t = device.create_texture(&wgpu::TextureDescriptor {
144                label: Some(label),
145                size: wgpu::Extent3d {
146                    width: w,
147                    height: h,
148                    depth_or_array_layers: 1,
149                },
150                mip_level_count: 1,
151                sample_count: 1,
152                dimension: wgpu::TextureDimension::D2,
153                format: fmt,
154                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
155                    | wgpu::TextureUsages::TEXTURE_BINDING,
156                view_formats: &[],
157            });
158            let v = t.create_view(&wgpu::TextureViewDescriptor::default());
159            (t, v)
160        };
161
162        let (a, av) = mk("gbuf_albedo_metallic", wgpu::TextureFormat::Rgba8Unorm);
163        let (n, nv) = mk("gbuf_normal_roughness", wgpu::TextureFormat::Rgba16Float);
164        let (p, pv) = mk("gbuf_world_position", wgpu::TextureFormat::Rgba16Float);
165        let (t, tv) = mk("gbuf_world_tangent", wgpu::TextureFormat::Rgba16Float);
166
167        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
168            address_mode_u: wgpu::AddressMode::ClampToEdge,
169            address_mode_v: wgpu::AddressMode::ClampToEdge,
170            mag_filter: wgpu::FilterMode::Nearest,
171            min_filter: wgpu::FilterMode::Nearest,
172            ..Default::default()
173        });
174
175        (a, av, n, nv, p, pv, t, tv, sampler)
176    }
177
178    fn create_gbuffer_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
179        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
180            label: Some("gbuffer_bind_group_layout"),
181            entries: &[
182                // albedo_metallic
183                wgpu::BindGroupLayoutEntry {
184                    binding: 0,
185                    visibility: wgpu::ShaderStages::FRAGMENT,
186                    ty: wgpu::BindingType::Texture {
187                        multisampled: false,
188                        view_dimension: wgpu::TextureViewDimension::D2,
189                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
190                    },
191                    count: None,
192                },
193                // normal_roughness
194                wgpu::BindGroupLayoutEntry {
195                    binding: 1,
196                    visibility: wgpu::ShaderStages::FRAGMENT,
197                    ty: wgpu::BindingType::Texture {
198                        multisampled: false,
199                        view_dimension: wgpu::TextureViewDimension::D2,
200                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
201                    },
202                    count: None,
203                },
204                // world_position
205                wgpu::BindGroupLayoutEntry {
206                    binding: 2,
207                    visibility: wgpu::ShaderStages::FRAGMENT,
208                    ty: wgpu::BindingType::Texture {
209                        multisampled: false,
210                        view_dimension: wgpu::TextureViewDimension::D2,
211                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
212                    },
213                    count: None,
214                },
215                // nearest sampler
216                wgpu::BindGroupLayoutEntry {
217                    binding: 3,
218                    visibility: wgpu::ShaderStages::FRAGMENT,
219                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
220                    count: None,
221                },
222                // world_tangent
223                wgpu::BindGroupLayoutEntry {
224                    binding: 4,
225                    visibility: wgpu::ShaderStages::FRAGMENT,
226                    ty: wgpu::BindingType::Texture {
227                        multisampled: false,
228                        view_dimension: wgpu::TextureViewDimension::D2,
229                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
230                    },
231                    count: None,
232                },
233            ],
234        })
235    }
236
237    fn create_gbuffer_bind_group(
238        device: &wgpu::Device,
239        layout: &wgpu::BindGroupLayout,
240        albedo_v: &wgpu::TextureView,
241        normal_v: &wgpu::TextureView,
242        pos_v: &wgpu::TextureView,
243        tangent_v: &wgpu::TextureView,
244        sampler: &wgpu::Sampler,
245    ) -> wgpu::BindGroup {
246        device.create_bind_group(&wgpu::BindGroupDescriptor {
247            label: Some("gbuffer_bind_group"),
248            layout,
249            entries: &[
250                wgpu::BindGroupEntry {
251                    binding: 0,
252                    resource: wgpu::BindingResource::TextureView(albedo_v),
253                },
254                wgpu::BindGroupEntry {
255                    binding: 1,
256                    resource: wgpu::BindingResource::TextureView(normal_v),
257                },
258                wgpu::BindGroupEntry {
259                    binding: 2,
260                    resource: wgpu::BindingResource::TextureView(pos_v),
261                },
262                wgpu::BindGroupEntry {
263                    binding: 3,
264                    resource: wgpu::BindingResource::Sampler(sampler),
265                },
266                wgpu::BindGroupEntry {
267                    binding: 4,
268                    resource: wgpu::BindingResource::TextureView(tangent_v),
269                },
270            ],
271        })
272    }
273
274    fn create_gbuffer_pipeline(device: &wgpu::Device, scene: &SceneState) -> wgpu::RenderPipeline {
275        let shader = load_shader(
276            device,
277            "demo/assets/shaders/gbuffer.wgsl",
278            include_str!("shaders/gbuffer.wgsl"),
279            "GBuffer Shader",
280        );
281
282        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
283            label: Some("GBuffer Pipeline Layout"),
284            bind_group_layouts: &[
285                &scene.global_bind_group_layout,   // 0: SceneUniforms
286                &scene.texture_bind_group_layout,  // 1: albedo texture
287                &scene.shadow_bind_group_layout, // 2: shadow (unused in G-pass but slot must exist)
288                &scene.skeleton_bind_group_layout, // 3: skeleton
289                &scene.instance_bind_group_layout, // 4: instances
290            ],
291            push_constant_ranges: &[],
292        });
293
294        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
295            label: Some("GBuffer Pipeline"),
296            layout: Some(&layout),
297            vertex: wgpu::VertexState {
298                module: &shader,
299                entry_point: "vs_main",
300                compilation_options: Default::default(),
301                buffers: &[Vertex::desc()],
302            },
303            fragment: Some(wgpu::FragmentState {
304                module: &shader,
305                entry_point: "fs_main",
306                compilation_options: Default::default(),
307                targets: &[
308                    // RT0: albedo_metallic  Rgba8Unorm
309                    Some(wgpu::ColorTargetState {
310                        format: wgpu::TextureFormat::Rgba8Unorm,
311                        blend: None,
312                        write_mask: wgpu::ColorWrites::ALL,
313                    }),
314                    // RT1: normal_roughness Rgba16Float
315                    Some(wgpu::ColorTargetState {
316                        format: wgpu::TextureFormat::Rgba16Float,
317                        blend: None,
318                        write_mask: wgpu::ColorWrites::ALL,
319                    }),
320                    // RT2: world_position   Rgba16Float
321                    Some(wgpu::ColorTargetState {
322                        format: wgpu::TextureFormat::Rgba16Float,
323                        blend: None,
324                        write_mask: wgpu::ColorWrites::ALL,
325                    }),
326                    // RT3: world_tangent    Rgba16Float
327                    Some(wgpu::ColorTargetState {
328                        format: wgpu::TextureFormat::Rgba16Float,
329                        blend: None,
330                        write_mask: wgpu::ColorWrites::ALL,
331                    }),
332                ],
333            }),
334            primitive: wgpu::PrimitiveState {
335                topology: wgpu::PrimitiveTopology::TriangleList,
336                front_face: wgpu::FrontFace::Ccw,
337                cull_mode: Some(wgpu::Face::Back),
338                ..Default::default()
339            },
340            depth_stencil: Some(wgpu::DepthStencilState {
341                format: wgpu::TextureFormat::Depth32Float,
342                depth_write_enabled: false,
343                depth_compare: wgpu::CompareFunction::LessEqual,
344                stencil: wgpu::StencilState::default(),
345                bias: wgpu::DepthBiasState::default(),
346            }),
347            multisample: wgpu::MultisampleState::default(),
348            multiview: None,
349        })
350    }
351
352    fn create_z_prepass_pipeline(
353        device: &wgpu::Device,
354        scene: &SceneState,
355    ) -> wgpu::RenderPipeline {
356        let shader = load_shader(
357            device,
358            "demo/assets/shaders/gbuffer.wgsl",
359            include_str!("shaders/gbuffer.wgsl"),
360            "Z-Prepass Shader",
361        );
362
363        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
364            label: Some("Z-Prepass Pipeline Layout"),
365            bind_group_layouts: &[
366                &scene.global_bind_group_layout,   // 0: SceneUniforms
367                &scene.texture_bind_group_layout, // 1: albedo texture (unused but required by shader layout)
368                &scene.shadow_bind_group_layout,  // 2: shadow
369                &scene.skeleton_bind_group_layout, // 3: skeleton
370                &scene.instance_bind_group_layout, // 4: instances
371            ],
372            push_constant_ranges: &[],
373        });
374
375        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
376            label: Some("Z-Prepass Pipeline"),
377            layout: Some(&layout),
378            vertex: wgpu::VertexState {
379                module: &shader,
380                entry_point: "vs_main",
381                compilation_options: Default::default(),
382                buffers: &[Vertex::desc()],
383            },
384            fragment: None, // NO COLOR TARGETS!
385            primitive: wgpu::PrimitiveState {
386                topology: wgpu::PrimitiveTopology::TriangleList,
387                front_face: wgpu::FrontFace::Ccw,
388                cull_mode: Some(wgpu::Face::Back),
389                ..Default::default()
390            },
391            depth_stencil: Some(wgpu::DepthStencilState {
392                format: wgpu::TextureFormat::Depth32Float,
393                depth_write_enabled: true,
394                depth_compare: wgpu::CompareFunction::Less,
395                stencil: wgpu::StencilState::default(),
396                bias: wgpu::DepthBiasState::default(),
397            }),
398            multisample: wgpu::MultisampleState::default(),
399            multiview: None,
400        })
401    }
402
403    fn create_lighting_pipeline(
404        device: &wgpu::Device,
405        scene: &SceneState,
406        gbuffer_layout: &wgpu::BindGroupLayout,
407    ) -> wgpu::RenderPipeline {
408        let shader = load_shader(
409            device,
410            "demo/assets/shaders/deferred_lighting.wgsl",
411            include_str!("shaders/deferred_lighting.wgsl"),
412            "Deferred Lighting Shader",
413        );
414
415        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
416            label: Some("Deferred Lighting Layout"),
417            bind_group_layouts: &[
418                &scene.global_bind_group_layout, // 0: SceneUniforms
419                &scene.shadow_bind_group_layout, // 1: shadow CSM
420                gbuffer_layout,                  // 2: G-buffers
421            ],
422            push_constant_ranges: &[],
423        });
424
425        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
426            label: Some("Deferred Lighting Pipeline"),
427            layout: Some(&layout),
428            vertex: wgpu::VertexState {
429                module: &shader,
430                entry_point: "vs_main",
431                compilation_options: Default::default(),
432                buffers: &[], // fullscreen triangle — no vertex buffer
433            },
434            fragment: Some(wgpu::FragmentState {
435                module: &shader,
436                entry_point: "fs_main",
437                compilation_options: Default::default(),
438                targets: &[Some(wgpu::ColorTargetState {
439                    format: wgpu::TextureFormat::Rgba16Float,
440                    blend: None,
441                    write_mask: wgpu::ColorWrites::ALL,
442                })],
443            }),
444            primitive: wgpu::PrimitiveState {
445                topology: wgpu::PrimitiveTopology::TriangleList,
446                cull_mode: None,
447                ..Default::default()
448            },
449            depth_stencil: None, // no depth write in lighting pass
450            multisample: wgpu::MultisampleState::default(),
451            multiview: None,
452        })
453    }
454}