tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main llimphi-3d/src/glow.rs Rust · 378 lines · 14.1 KB History Who touched what raw

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! `Glows` — billboards **aditivos**: como [`Billboards`](crate::Billboards)
2//! (quads de cara a la cámara desde un atlas) pero con **blend aditivo**, sin
3//! escribir profundidad y **sin recorte de alpha** — para halos suaves y
4//! luminosos: estrellas que brillan, atmósferas, auras de cuerpos, glows que el
5//! bloom de [`PostFx`](crate::PostFx) infla. El `Billboards` opaco recorta el
6//! borde (disco nítido); éste suma luz (sin borde, se funde).
7//!
8//! Comparte el tipo de instancia [`Billboard`](crate::Billboard). Se dibuja en
9//! un pase con depth attachment, **después** de la geometría opaca (así el mundo
10//! sólido ocluye los glows que quedan detrás; los de adelante suman luz):
11//!
12//! ```ignore
13//! let mut g = Glows::new(&device, fmt);
14//! g.set_atlas(&device, &queue, w, h, &soft_radial_rgba);
15//! g.set_glows(&device, &[Billboard { .. }]);
16//! g.upload(&queue, aspect, &camera);
17//! g.draw(&mut pass);
18//! ```
19
20use crate::billboard::Billboard;
21use crate::camera::Camera3d;
22use crate::scene::DEPTH_FORMAT;
23
24struct Atlas {
25    bind_group: wgpu::BindGroup,
26}
27
28/// Renderer de glows aditivos. Misma plomería que `Billboards`, distinto blend.
29pub struct Glows {
30    pipeline: wgpu::RenderPipeline,
31    uniform_buf: wgpu::Buffer,
32    uniform_bg: wgpu::BindGroup,
33    tex_layout: wgpu::BindGroupLayout,
34    sampler: wgpu::Sampler,
35    atlas: Option<Atlas>,
36    instances: Option<wgpu::Buffer>,
37    count: u32,
38}
39
40impl Glows {
41    pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
42        let uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
43            label: Some("llimphi-3d-glow-uniform-layout"),
44            entries: &[wgpu::BindGroupLayoutEntry {
45                binding: 0,
46                visibility: wgpu::ShaderStages::VERTEX,
47                ty: wgpu::BindingType::Buffer {
48                    ty: wgpu::BufferBindingType::Uniform,
49                    has_dynamic_offset: false,
50                    min_binding_size: None,
51                },
52                count: None,
53            }],
54        });
55        let tex_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
56            label: Some("llimphi-3d-glow-tex-layout"),
57            entries: &[
58                wgpu::BindGroupLayoutEntry {
59                    binding: 0,
60                    visibility: wgpu::ShaderStages::FRAGMENT,
61                    ty: wgpu::BindingType::Texture {
62                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
63                        view_dimension: wgpu::TextureViewDimension::D2,
64                        multisampled: false,
65                    },
66                    count: None,
67                },
68                wgpu::BindGroupLayoutEntry {
69                    binding: 1,
70                    visibility: wgpu::ShaderStages::FRAGMENT,
71                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
72                    count: None,
73                },
74            ],
75        });
76        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
77            label: Some("llimphi-3d-glow-shader"),
78            source: wgpu::ShaderSource::Wgsl(GLOW_WGSL.into()),
79        });
80        let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
81            label: Some("llimphi-3d-glow-pl"),
82            bind_group_layouts: &[Some(&uniform_layout), Some(&tex_layout)],
83            immediate_size: 0,
84        });
85        // Blend aditivo: la luz se suma al fondo (src*1 + dst*1).
86        let additive = wgpu::BlendState {
87            color: wgpu::BlendComponent {
88                src_factor: wgpu::BlendFactor::One,
89                dst_factor: wgpu::BlendFactor::One,
90                operation: wgpu::BlendOperation::Add,
91            },
92            alpha: wgpu::BlendComponent {
93                src_factor: wgpu::BlendFactor::One,
94                dst_factor: wgpu::BlendFactor::One,
95                operation: wgpu::BlendOperation::Add,
96            },
97        };
98        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
99            label: Some("llimphi-3d-glow-pipeline"),
100            layout: Some(&pl),
101            vertex: wgpu::VertexState {
102                module: &shader,
103                entry_point: Some("vs"),
104                compilation_options: Default::default(),
105                buffers: &[wgpu::VertexBufferLayout {
106                    array_stride: Billboard::STRIDE as u64,
107                    step_mode: wgpu::VertexStepMode::Instance,
108                    attributes: &[
109                        wgpu::VertexAttribute {
110                            format: wgpu::VertexFormat::Float32x3,
111                            offset: 0,
112                            shader_location: 0,
113                        },
114                        wgpu::VertexAttribute {
115                            format: wgpu::VertexFormat::Float32x2,
116                            offset: 12,
117                            shader_location: 1,
118                        },
119                        wgpu::VertexAttribute {
120                            format: wgpu::VertexFormat::Float32x2,
121                            offset: 20,
122                            shader_location: 2,
123                        },
124                        wgpu::VertexAttribute {
125                            format: wgpu::VertexFormat::Float32x2,
126                            offset: 28,
127                            shader_location: 3,
128                        },
129                        wgpu::VertexAttribute {
130                            format: wgpu::VertexFormat::Float32x4,
131                            offset: 36,
132                            shader_location: 4,
133                        },
134                    ],
135                }],
136            },
137            primitive: wgpu::PrimitiveState {
138                cull_mode: None,
139                ..Default::default()
140            },
141            depth_stencil: Some(wgpu::DepthStencilState {
142                format: DEPTH_FORMAT,
143                depth_write_enabled: Some(false),
144                depth_compare: Some(wgpu::CompareFunction::LessEqual),
145                stencil: Default::default(),
146                bias: Default::default(),
147            }),
148            multisample: Default::default(),
149            fragment: Some(wgpu::FragmentState {
150                module: &shader,
151                entry_point: Some("fs"),
152                compilation_options: Default::default(),
153                targets: &[Some(wgpu::ColorTargetState {
154                    format: color_format,
155                    blend: Some(additive),
156                    write_mask: wgpu::ColorWrites::ALL,
157                })],
158            }),
159            multiview_mask: None,
160            cache: None,
161        });
162        let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
163            label: Some("llimphi-3d-glow-uniform"),
164            size: 96, // view_proj(64) + right(16) + up(16)
165            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
166            mapped_at_creation: false,
167        });
168        let uniform_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
169            label: Some("llimphi-3d-glow-uniform-bg"),
170            layout: &uniform_layout,
171            entries: &[wgpu::BindGroupEntry {
172                binding: 0,
173                resource: uniform_buf.as_entire_binding(),
174            }],
175        });
176        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
177            label: Some("llimphi-3d-glow-sampler"),
178            address_mode_u: wgpu::AddressMode::ClampToEdge,
179            address_mode_v: wgpu::AddressMode::ClampToEdge,
180            address_mode_w: wgpu::AddressMode::ClampToEdge,
181            mag_filter: wgpu::FilterMode::Linear,
182            min_filter: wgpu::FilterMode::Linear,
183            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
184            ..Default::default()
185        });
186        Self {
187            pipeline,
188            uniform_buf,
189            uniform_bg,
190            tex_layout,
191            sampler,
192            atlas: None,
193            instances: None,
194            count: 0,
195        }
196    }
197
198    /// Sube/reemplaza el atlas (RGBA8, `w×h`). Para glows conviene un degradé
199    /// radial suave (gaussiano), sin núcleo duro.
200    pub fn set_atlas(
201        &mut self,
202        device: &wgpu::Device,
203        queue: &wgpu::Queue,
204        w: u32,
205        h: u32,
206        data: &[u8],
207    ) {
208        assert_eq!(data.len(), (w * h * 4) as usize, "RGBA8 w*h*4 esperado");
209        let tex = device.create_texture(&wgpu::TextureDescriptor {
210            label: Some("llimphi-3d-glow-atlas"),
211            size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
212            mip_level_count: 1,
213            sample_count: 1,
214            dimension: wgpu::TextureDimension::D2,
215            format: wgpu::TextureFormat::Rgba8Unorm,
216            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
217            view_formats: &[],
218        });
219        queue.write_texture(
220            wgpu::TexelCopyTextureInfo {
221                texture: &tex,
222                mip_level: 0,
223                origin: wgpu::Origin3d::ZERO,
224                aspect: wgpu::TextureAspect::All,
225            },
226            data,
227            wgpu::TexelCopyBufferLayout {
228                offset: 0,
229                bytes_per_row: Some(w * 4),
230                rows_per_image: Some(h),
231            },
232            wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
233        );
234        let view = tex.create_view(&Default::default());
235        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
236            label: Some("llimphi-3d-glow-atlas-bg"),
237            layout: &self.tex_layout,
238            entries: &[
239                wgpu::BindGroupEntry {
240                    binding: 0,
241                    resource: wgpu::BindingResource::TextureView(&view),
242                },
243                wgpu::BindGroupEntry {
244                    binding: 1,
245                    resource: wgpu::BindingResource::Sampler(&self.sampler),
246                },
247            ],
248        });
249        self.atlas = Some(Atlas { bind_group });
250    }
251
252    /// Reemplaza la lista de glows (recrea el buffer de instancias). Con blend
253    /// aditivo el orden no importa (la suma es conmutativa).
254    pub fn set_glows(&mut self, device: &wgpu::Device, items: &[Billboard]) {
255        self.count = items.len() as u32;
256        if items.is_empty() {
257            self.instances = None;
258            return;
259        }
260        let mut bytes = Vec::with_capacity(items.len() * Billboard::STRIDE);
261        for b in items {
262            b.write_to(&mut bytes);
263        }
264        let buf = device.create_buffer(&wgpu::BufferDescriptor {
265            label: Some("llimphi-3d-glow-instances"),
266            size: bytes.len() as u64,
267            usage: wgpu::BufferUsages::VERTEX,
268            mapped_at_creation: true,
269        });
270        buf.slice(..).get_mapped_range_mut().copy_from_slice(&bytes);
271        buf.unmap();
272        self.instances = Some(buf);
273    }
274
275    /// Sube el uniform del frame: `view_proj` + ejes `right`/`up` de la cámara.
276    pub fn upload(&self, queue: &wgpu::Queue, aspect: f32, camera: &Camera3d) {
277        let view_proj = camera.view_proj(aspect);
278        let forward = (camera.target - camera.eye).normalize_or_zero();
279        let right = forward.cross(camera.up).normalize_or_zero();
280        let up = right.cross(forward);
281        let mut b = Vec::with_capacity(96);
282        for v in view_proj.to_cols_array() {
283            b.extend_from_slice(&v.to_ne_bytes());
284        }
285        for v in [right.x, right.y, right.z, 0.0] {
286            b.extend_from_slice(&v.to_ne_bytes());
287        }
288        for v in [up.x, up.y, up.z, 0.0] {
289            b.extend_from_slice(&v.to_ne_bytes());
290        }
291        queue.write_buffer(&self.uniform_buf, 0, &b);
292    }
293
294    /// Dibuja los glows en un pase ya abierto (con depth). No-op si falta algo.
295    pub fn draw<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
296        let (Some(atlas), Some(inst)) = (self.atlas.as_ref(), self.instances.as_ref()) else {
297            return;
298        };
299        if self.count == 0 {
300            return;
301        }
302        pass.set_pipeline(&self.pipeline);
303        pass.set_bind_group(0, &self.uniform_bg, &[]);
304        pass.set_bind_group(1, &atlas.bind_group, &[]);
305        pass.set_vertex_buffer(0, inst.slice(..));
306        pass.draw(0..6, 0..self.count);
307    }
308}
309
310/// Quad de cara a la cámara; el fragment SUMA luz (sin discard). La salida es
311/// premultiplicada (`rgb*a`) para que el blend aditivo dé un halo suave.
312const GLOW_WGSL: &str = r#"
313struct U {
314    view_proj: mat4x4<f32>,
315    right: vec4<f32>,
316    up: vec4<f32>,
317};
318@group(0) @binding(0) var<uniform> u: U;
319@group(1) @binding(0) var tex: texture_2d<f32>;
320@group(1) @binding(1) var samp: sampler;
321
322struct VIn {
323    @location(0) center: vec3<f32>,
324    @location(1) size: vec2<f32>,
325    @location(2) uv_min: vec2<f32>,
326    @location(3) uv_max: vec2<f32>,
327    @location(4) tint: vec4<f32>,
328};
329struct VOut {
330    @builtin(position) clip: vec4<f32>,
331    @location(0) uv: vec2<f32>,
332    @location(1) tint: vec4<f32>,
333};
334
335@vertex
336fn vs(@builtin(vertex_index) vi: u32, in: VIn) -> VOut {
337    var cs = array<vec2<f32>, 6>(
338        vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0),
339        vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 1.0), vec2<f32>(0.0, 1.0),
340    );
341    let q = cs[vi];
342    let off = q - vec2<f32>(0.5, 0.5);
343    let world = in.center
344        + u.right.xyz * (off.x * in.size.x)
345        + u.up.xyz * (off.y * in.size.y);
346    var o: VOut;
347    o.clip = u.view_proj * vec4<f32>(world, 1.0);
348    o.uv = vec2<f32>(
349        mix(in.uv_min.x, in.uv_max.x, q.x),
350        mix(in.uv_min.y, in.uv_max.y, 1.0 - q.y),
351    );
352    o.tint = in.tint;
353    return o;
354}
355
356@fragment
357fn fs(in: VOut) -> @location(0) vec4<f32> {
358    let c = textureSample(tex, samp, in.uv) * in.tint;
359    let a = c.a;
360    return vec4<f32>(c.rgb * a, a);
361}
362"#;
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn glow_wgsl_valida() {
370        let module = naga::front::wgsl::parse_str(GLOW_WGSL).expect("GLOW_WGSL no parsea");
371        naga::valid::Validator::new(
372            naga::valid::ValidationFlags::all(),
373            naga::valid::Capabilities::all(),
374        )
375        .validate(&module)
376        .expect("GLOW_WGSL no valida");
377    }
378}