Skip to main content

lumen_engine/node/processing/
shadow.rs

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