1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
//! Transparent pass KvasirNode — renders transparent 3D meshes with proper
//! back-to-front sorting using view_depth.
use crate::kvasir::nodes::PassId;
use crate::kvasir::{ExecutionContext, KvasirNode, ResourceId};
use crate::passes::shadow::GpuMesh3d;
/// Transparent 3D render pass node — renders transparent meshes with alpha blending.
/// Uses view_depth for proper back-to-front sorting.
pub struct TransparentNode {
/// GPU-ready mesh instances to render (sorted by view_depth descending).
pub mesh_instances: Vec<GpuMesh3d>,
/// Shadow map resource for darkening transparent surfaces.
pub shadow_map: ResourceId,
/// Camera position for view_depth calculation.
pub camera_pos: glam::Vec3,
}
impl KvasirNode for TransparentNode {
fn label(&self) -> &'static str {
"Transparent3d"
}
fn inputs(&self) -> &[ResourceId] {
std::slice::from_ref(&self.shadow_map)
}
fn outputs(&self) -> &[ResourceId] {
&[]
}
fn pass_id(&self) -> PassId {
PassId::Transparent3d
}
fn execute(&self, ctx: &mut ExecutionContext) {
tracing::debug!(
"TransparentNode::execute — instances={}, shadow_map={:?}",
self.mesh_instances.len(),
self.shadow_map
);
if self.mesh_instances.is_empty() {
return;
}
// Use the main scene render target (RES_SCENE) for color output.
let scene_view = match ctx
.registry
.get_texture_view(crate::kvasir::nodes::RES_SCENE)
{
Some(v) => v,
None => {
tracing::error!("TransparentNode: missing scene color target");
return;
}
};
let depth_view = ctx.depth_view;
// Get shadow resources and create bind group 3
let shadow_bind_group = match ctx.registry.get_texture_view(self.shadow_map) {
Some(shadow_view) => {
let shadow_sampler = match ctx.renderer.shadow_sampler.as_ref() {
Some(s) => s,
None => {
tracing::warn!("TransparentNode: missing shadow sampler");
return;
}
};
let ibl_view_owned = ctx
.registry
.get_texture_view(crate::kvasir::nodes::RES_BLUR_A);
let ibl_view = match &ibl_view_owned {
Some(view) => view,
None => &ctx.renderer.dummy_view,
};
Some(ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("TransparentNode PBR Material Bind Group"),
layout: &ctx.renderer.pbr_material_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&shadow_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(shadow_sampler),
},
wgpu::BindGroupEntry {
binding: 8,
resource: wgpu::BindingResource::TextureView(ibl_view),
},
wgpu::BindGroupEntry {
binding: 9,
resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::TextureView(&ctx.renderer.dummy_view),
},
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
},
],
}))
}
None => None,
};
// Set up transparent render pass with alpha blending.
let mut pass = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Transparent3d Pass (PBR + Shadows)"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &scene_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load, // Load existing (opaque) content
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, // Load existing depth
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
// Bind the transparent PBR pipeline and required bind groups.
pass.set_pipeline(&ctx.renderer.transparent_pipeline);
pass.set_bind_group(2, &ctx.renderer.berserker_bind_group, &[]);
if let Some(ref bg) = shadow_bind_group {
pass.set_bind_group(3, bg, &[]);
}
if let Some(ref inst_buffer) = ctx.renderer.instance_buffer_3d {
pass.set_vertex_buffer(1, inst_buffer.slice(..));
}
// Render meshes sorted by view_depth (back-to-front for transparency).
// TODO: Once SkinnedOutput is extended to include UV/color/tangent (matching Vertex3D layout),
// enable skinned_buffer binding here. Currently SkinnedOutput only has position+normal,
// which causes PBR shaders to read garbage for UVs/colors/tangents.
for mesh in self.mesh_instances.iter() {
// if let Some(skinned) = &mesh.skinned_buffer {
// pass.set_vertex_buffer(0, skinned.slice(..));
// } else {
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
// }
pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
let inst = mesh.instance_index;
pass.draw_indexed(0..mesh.index_count, 0, inst..(inst + 1));
}
}
}