gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ main llimphi-3d/src/lines.rs Rust · 244 líneas · 8.5 KB Historia Quién tocó qué crudo

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

1//! `Lines3d` — líneas 3D como `LineList`, depth-test pero **sin escribir**
2//! profundidad, con alpha-blend y un atenuado por profundidad opcional para dar
3//! volumen (el frente brilla, el fondo se apaga). Llena el hueco del crate, que
4//! sólo tenía triángulos: sirve para anillos (círculos máximos), rejillas,
5//! ejes y figuras de constelaciones — cualquier "alambre" 3D.
6//!
7//! Se dibuja en un pase ya abierto con depth attachment (el de
8//! [`PostFx::scene_pass`](crate::PostFx) o [`Scene3d`](crate::Scene3d)), DESPUÉS
9//! de la geometría sólida que deba ocluirlo:
10//!
11//! ```ignore
12//! lines.set_lines(device, &verts);     // pares de vértices = segmentos
13//! lines.upload(queue, view_proj);
14//! lines.draw(&mut pass);
15//! ```
16
17use glam::Mat4;
18
19use crate::scene::DEPTH_FORMAT;
20
21/// Vértice de línea: posición en mundo + color RGBA lineal.
22#[derive(Debug, Clone, Copy)]
23pub struct LineVertex {
24    pub pos: [f32; 3],
25    pub color: [f32; 4],
26}
27
28impl LineVertex {
29    /// `pos.xyz (12) + color.rgba (16)`.
30    pub const STRIDE: usize = 7 * 4;
31
32    fn write_to(&self, out: &mut Vec<u8>) {
33        for v in self.pos {
34            out.extend_from_slice(&v.to_ne_bytes());
35        }
36        for v in self.color {
37            out.extend_from_slice(&v.to_ne_bytes());
38        }
39    }
40}
41
42/// Renderer de líneas 3D reutilizable. Sin vértices, [`Self::draw`] es no-op.
43pub struct Lines3d {
44    pipeline: wgpu::RenderPipeline,
45    uniform_buf: wgpu::Buffer,
46    uniform_bg: wgpu::BindGroup,
47    verts: Option<wgpu::Buffer>,
48    count: u32,
49}
50
51impl Lines3d {
52    pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
53        let uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
54            label: Some("llimphi-3d-lines-uniform-layout"),
55            entries: &[wgpu::BindGroupLayoutEntry {
56                binding: 0,
57                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
58                ty: wgpu::BindingType::Buffer {
59                    ty: wgpu::BufferBindingType::Uniform,
60                    has_dynamic_offset: false,
61                    min_binding_size: None,
62                },
63                count: None,
64            }],
65        });
66        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
67            label: Some("llimphi-3d-lines-shader"),
68            source: wgpu::ShaderSource::Wgsl(LINES_WGSL.into()),
69        });
70        let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
71            label: Some("llimphi-3d-lines-pl"),
72            bind_group_layouts: &[Some(&uniform_layout)],
73            immediate_size: 0,
74        });
75        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
76            label: Some("llimphi-3d-lines-pipeline"),
77            layout: Some(&pl),
78            vertex: wgpu::VertexState {
79                module: &shader,
80                entry_point: Some("vs"),
81                compilation_options: Default::default(),
82                buffers: &[wgpu::VertexBufferLayout {
83                    array_stride: LineVertex::STRIDE as u64,
84                    step_mode: wgpu::VertexStepMode::Vertex,
85                    attributes: &[
86                        wgpu::VertexAttribute {
87                            format: wgpu::VertexFormat::Float32x3,
88                            offset: 0,
89                            shader_location: 0,
90                        },
91                        wgpu::VertexAttribute {
92                            format: wgpu::VertexFormat::Float32x4,
93                            offset: 12,
94                            shader_location: 1,
95                        },
96                    ],
97                }],
98            },
99            primitive: wgpu::PrimitiveState {
100                topology: wgpu::PrimitiveTopology::LineList,
101                cull_mode: None,
102                ..Default::default()
103            },
104            depth_stencil: Some(wgpu::DepthStencilState {
105                format: DEPTH_FORMAT,
106                depth_write_enabled: Some(false),
107                depth_compare: Some(wgpu::CompareFunction::LessEqual),
108                stencil: Default::default(),
109                bias: Default::default(),
110            }),
111            multisample: Default::default(),
112            fragment: Some(wgpu::FragmentState {
113                module: &shader,
114                entry_point: Some("fs"),
115                compilation_options: Default::default(),
116                targets: &[Some(wgpu::ColorTargetState {
117                    format: color_format,
118                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
119                    write_mask: wgpu::ColorWrites::ALL,
120                })],
121            }),
122            multiview_mask: None,
123            cache: None,
124        });
125        let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
126            label: Some("llimphi-3d-lines-uniform"),
127            size: 64, // view_proj mat4
128            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
129            mapped_at_creation: false,
130        });
131        let uniform_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
132            label: Some("llimphi-3d-lines-uniform-bg"),
133            layout: &uniform_layout,
134            entries: &[wgpu::BindGroupEntry {
135                binding: 0,
136                resource: uniform_buf.as_entire_binding(),
137            }],
138        });
139        Self {
140            pipeline,
141            uniform_buf,
142            uniform_bg,
143            verts: None,
144            count: 0,
145        }
146    }
147
148    /// Reemplaza los vértices. Cada **par** consecutivo es un segmento; para una
149    /// polilínea, repetir el vértice interior (a,b, b,c, c,d…).
150    pub fn set_lines(&mut self, device: &wgpu::Device, verts: &[LineVertex]) {
151        if verts.is_empty() {
152            self.count = 0;
153            self.verts = None;
154            return;
155        }
156        let mut bytes = Vec::with_capacity(verts.len() * LineVertex::STRIDE);
157        for v in verts {
158            v.write_to(&mut bytes);
159        }
160        let buf = device.create_buffer(&wgpu::BufferDescriptor {
161            label: Some("llimphi-3d-lines-vbuf"),
162            size: bytes.len() as u64,
163            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
164            mapped_at_creation: true,
165        });
166        buf.slice(..).get_mapped_range_mut().copy_from_slice(&bytes);
167        buf.unmap();
168        self.verts = Some(buf);
169        self.count = verts.len() as u32;
170    }
171
172    /// Sube la `view_proj` del frame. Llamar antes de [`Self::draw`].
173    pub fn upload(&self, queue: &wgpu::Queue, view_proj: Mat4) {
174        let mut b = Vec::with_capacity(64);
175        for v in view_proj.to_cols_array() {
176            b.extend_from_slice(&v.to_ne_bytes());
177        }
178        queue.write_buffer(&self.uniform_buf, 0, &b);
179    }
180
181    /// Dibuja en un pase ya abierto (con depth attachment). No-op sin vértices.
182    pub fn draw<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
183        let Some(vb) = self.verts.as_ref() else {
184            return;
185        };
186        if self.count < 2 {
187            return;
188        }
189        pass.set_pipeline(&self.pipeline);
190        pass.set_bind_group(0, &self.uniform_bg, &[]);
191        pass.set_vertex_buffer(0, vb.slice(..));
192        pass.draw(0..self.count, 0..1);
193    }
194}
195
196/// Atenúa el alpha por profundidad NDC (el fondo de la esfera se apaga → da
197/// volumen). El color va tal cual; sólo el alpha se modula.
198const LINES_WGSL: &str = r#"
199struct U { view_proj: mat4x4<f32> };
200@group(0) @binding(0) var<uniform> u: U;
201
202struct VIn {
203    @location(0) pos: vec3<f32>,
204    @location(1) color: vec4<f32>,
205};
206struct VOut {
207    @builtin(position) clip: vec4<f32>,
208    @location(0) color: vec4<f32>,
209    @location(1) ndc_z: f32,
210};
211
212@vertex
213fn vs(in: VIn) -> VOut {
214    var o: VOut;
215    o.clip = u.view_proj * vec4<f32>(in.pos, 1.0);
216    o.color = in.color;
217    o.ndc_z = o.clip.z / max(o.clip.w, 1e-6);
218    return o;
219}
220
221@fragment
222fn fs(in: VOut) -> @location(0) vec4<f32> {
223    // ndc_z ~0 = cerca, ~1 = lejos. El fondo se apaga hasta 0.55 (da volumen
224    // sin que el hemisferio trasero desaparezca).
225    let depth_fade = mix(1.0, 0.55, clamp(in.ndc_z, 0.0, 1.0));
226    return vec4<f32>(in.color.rgb, in.color.a * depth_fade);
227}
228"#;
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn lines_wgsl_valida() {
236        let module = naga::front::wgsl::parse_str(LINES_WGSL).expect("LINES_WGSL no parsea");
237        naga::valid::Validator::new(
238            naga::valid::ValidationFlags::all(),
239            naga::valid::Capabilities::all(),
240        )
241        .validate(&module)
242        .expect("LINES_WGSL no valida");
243    }
244}