Skip to main content

lumen_engine/node/processing/
shadow.rs

1use crate::node::{NodeId, NodeProperty, PortRef};
2
3use crate::gpu::{
4    BoundFrame, CompiledOutput, FrameBindContext, FrameBinding, GpuCompileNode, GpuFrameBindNode,
5    RasterHandle, compiler,
6};
7
8pub(crate) const SHADER: &str = include_str!("shadow.wgsl");
9
10/// Composites a blurred alpha shadow behind a raster.
11#[derive(Debug, Clone, lumen_macros::Node)]
12#[node(kind = "shadow", name = "Shadow", category = "processing")]
13pub struct Shadow {
14    pub id: NodeId,
15    /// Horizontal shadow offset in pixels.
16    #[property(kind = "float", name = "Offset X", step = 1)]
17    pub offset_x: NodeProperty,
18    /// Vertical shadow offset in pixels.
19    #[property(kind = "float", name = "Offset Y", step = 1)]
20    pub offset_y: NodeProperty,
21    /// Shadow blur radius in pixels.
22    #[property(kind = "float", name = "Blur radius", min = 0, step = 0.5)]
23    pub radius: NodeProperty,
24    /// Shadow color.
25    #[property(kind = "color")]
26    pub color: NodeProperty,
27    /// Shadow opacity.
28    #[property(kind = "float", min = 0, max = 1, step = 0.05)]
29    pub opacity: NodeProperty,
30    #[input()]
31    pub source: PortRef,
32}
33
34impl Default for Shadow {
35    fn default() -> Self {
36        Self {
37            id: NodeId::new(0),
38            offset_x: NodeProperty::Float(8.0),
39            offset_y: NodeProperty::Float(8.0),
40            radius: NodeProperty::Float(8.0),
41            color: NodeProperty::Color([0, 0, 0, 255]),
42            opacity: NodeProperty::Float(0.5),
43            source: PortRef::empty(),
44        }
45    }
46}
47
48impl GpuCompileNode for Shadow {
49    fn compile_gpu(
50        &self,
51        ctx: &mut crate::gpu::CompileContext<'_>,
52        port: &PortRef,
53    ) -> crate::Result<CompiledOutput> {
54        if port.port != "output" {
55            return Err(ctx.missing_output(self.id, &port.port));
56        }
57
58        let source = ctx
59            .compile_port(&self.source)?
60            .into_raster(self.source.id, &self.source.port)?;
61        let size = source.domain.storage_size;
62        let temp = ctx.builder_mut().texture_for(
63            lumen_gpu::NodeKey(self.id.0),
64            Some(format!("shadow:{}:horizontal", self.id.0)),
65            lumen_gpu::TextureDesc::storage(size, lumen_gpu::wgpu::TextureFormat::Rgba8Unorm),
66        );
67        let texture = ctx.builder_mut().texture_for(
68            lumen_gpu::NodeKey(self.id.0),
69            Some(format!("shadow:{}:output", self.id.0)),
70            lumen_gpu::TextureDesc::storage(size, lumen_gpu::wgpu::TextureFormat::Rgba8Unorm),
71        );
72        let params = ctx.builder_mut().buffer_for(
73            lumen_gpu::NodeKey(self.id.0),
74            Some(format!("shadow:{}:params", self.id.0)),
75            lumen_gpu::BufferDesc::uniform(std::mem::size_of::<compiler::ShadowParams>() as u64),
76        );
77        let bind_groups = lumen_gpu::BindGroupLayoutSpec::single(vec![
78            lumen_gpu::BindingLayoutEntry::texture(0, lumen_gpu::wgpu::ShaderStages::COMPUTE),
79            lumen_gpu::BindingLayoutEntry::texture(1, lumen_gpu::wgpu::ShaderStages::COMPUTE),
80            lumen_gpu::BindingLayoutEntry::uniform(2, lumen_gpu::wgpu::ShaderStages::COMPUTE),
81            lumen_gpu::BindingLayoutEntry::storage_texture(
82                3,
83                lumen_gpu::wgpu::ShaderStages::COMPUTE,
84                lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
85                lumen_gpu::wgpu::StorageTextureAccess::WriteOnly,
86            ),
87        ]);
88        let horizontal = ctx.builder_mut().program_for(
89            lumen_gpu::NodeKey(self.id.0),
90            lumen_gpu::ProgramDesc::Compute(lumen_gpu::ComputeProgramDesc {
91                label: Some("shadow-horizontal".to_string()),
92                shader: SHADER.to_string(),
93                entry: "horizontal_main".to_string(),
94                bind_groups: bind_groups.clone(),
95            }),
96        );
97        let vertical = ctx.builder_mut().program_for(
98            lumen_gpu::NodeKey(self.id.0),
99            lumen_gpu::ProgramDesc::Compute(lumen_gpu::ComputeProgramDesc {
100                label: Some("shadow-vertical".to_string()),
101                shader: SHADER.to_string(),
102                entry: "vertical_main".to_string(),
103                bind_groups,
104            }),
105        );
106        ctx.builder_mut().compute_pass(lumen_gpu::ComputePassDesc {
107            label: Some(format!("shadow:{}:horizontal", self.id.0)),
108            owner: Some(lumen_gpu::NodeKey(self.id.0)),
109            program: horizontal,
110            bindings: vec![
111                lumen_gpu::Binding::sampled_texture(0, 0, source.texture),
112                lumen_gpu::Binding::sampled_texture(0, 1, source.texture),
113                lumen_gpu::Binding::uniform(0, 2, params),
114                lumen_gpu::Binding::storage_texture(0, 3, temp),
115            ],
116            dispatch: compiler::dispatch_for(size).into(),
117        });
118        ctx.builder_mut().compute_pass(lumen_gpu::ComputePassDesc {
119            label: Some(format!("shadow:{}:vertical", self.id.0)),
120            owner: Some(lumen_gpu::NodeKey(self.id.0)),
121            program: vertical,
122            bindings: vec![
123                lumen_gpu::Binding::sampled_texture(0, 0, source.texture),
124                lumen_gpu::Binding::sampled_texture(0, 1, temp),
125                lumen_gpu::Binding::uniform(0, 2, params),
126                lumen_gpu::Binding::storage_texture(0, 3, texture),
127            ],
128            dispatch: compiler::dispatch_for(size).into(),
129        });
130        ctx.builder_mut().param(
131            lumen_gpu::ParamKey {
132                owner: lumen_gpu::NodeKey(self.id.0),
133                slot: 0,
134            },
135            lumen_gpu::ParamTarget::Buffer(params),
136        );
137        ctx.push_frame_binding(FrameBinding::Shadow {
138            node_id: self.id,
139            offset_x: self.offset_x.clone(),
140            offset_y: self.offset_y.clone(),
141            radius: self.radius.clone(),
142            color: self.color.clone(),
143            opacity: self.opacity.clone(),
144            buffer: params,
145        });
146        Ok(CompiledOutput::Raster(RasterHandle {
147            texture,
148            domain: source.domain,
149            metadata: source.metadata,
150        }))
151    }
152}
153
154impl GpuFrameBindNode for Shadow {
155    fn bind_gpu_frame(
156        &self,
157        ctx: &FrameBindContext<'_>,
158        binding: &FrameBinding,
159        bound: &mut BoundFrame,
160    ) -> crate::Result<()> {
161        let FrameBinding::Shadow {
162            node_id,
163            offset_x,
164            offset_y,
165            radius,
166            color,
167            opacity,
168            buffer,
169        } = binding
170        else {
171            return Ok(());
172        };
173        let color = color.resolve_color(*node_id, "color", &ctx.expr_context(*node_id, "color"))?;
174        let color = compiler::ColorParams::from_rgba8(color).color;
175        let params = compiler::ShadowParams {
176            color,
177            values: [
178                offset_x.resolve_float(
179                    *node_id,
180                    "offset_x",
181                    &ctx.expr_context(*node_id, "offset_x"),
182                )? as f32,
183                offset_y.resolve_float(
184                    *node_id,
185                    "offset_y",
186                    &ctx.expr_context(*node_id, "offset_y"),
187                )? as f32,
188                radius
189                    .resolve_float(*node_id, "radius", &ctx.expr_context(*node_id, "radius"))?
190                    .round()
191                    .clamp(0.0, 32.0) as f32,
192                opacity.resolve_float(
193                    *node_id,
194                    "opacity",
195                    &ctx.expr_context(*node_id, "opacity"),
196                )? as f32,
197            ],
198        };
199        bound.write_buffer(*buffer, 0, bytemuck::bytes_of(&params));
200        Ok(())
201    }
202}