Skip to main content

libdqg/renderer/
extras.rs

1use crate::renderer::DrawPass;
2use crate::renderer::model::{Model, ModelUniform};
3use crate::types::Color;
4
5const VS_SRC: &str = include_str!("../../shaders/shape_pipeline_vs.wgsl");
6const FS_SRC: &str = include_str!("../../shaders/shape_pipeline_fs.wgsl");
7
8const SPRITE_VS_SRC: &str = include_str!("../../shaders/sprite_pipeline_vs.wgsl");
9const SPRITE_FS_SRC: &str = include_str!("../../shaders/sprite_pipeline_fs.wgsl");
10
11const WORLD_SPRITE_VS_SRC: &str = include_str!("../../shaders/world_sprite_pipeline_vs.wgsl");
12const WORLD_SPRITE_FS_SRC: &str = include_str!("../../shaders/world_sprite_pipeline_fs.wgsl");
13
14const WORLD_SPRITE_OUTLINE_VS_SRC: &str = include_str!("../../shaders/world_sprite_outline_vs.wgsl");
15
16const MODEL_VS_SRC: &str = include_str!("../../shaders/model_pipeline_vs.wgsl");
17const MODEL_FS_SRC: &str = include_str!("../../shaders/model_pipeline_fs.wgsl");
18
19const MODEL_OUTLINE_VS_SRC: &str = include_str!("../../shaders/model_outline_vs.wgsl");
20const MODEL_OUTLINE_FS_SRC: &str = include_str!("../../shaders/model_outline_fs.wgsl");
21
22/// Format of the renderer's depth buffer. Every pipeline drawn in the main pass must declare a
23/// depth-stencil state using this format.
24pub(crate) const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
25
26/// Depth state for screen-space drawing: never tested, never written, so UI paints in call
27/// order on top of whatever the world left behind.
28pub(crate) fn overlay_depth_state() -> wgpu::DepthStencilState {
29    wgpu::DepthStencilState {
30        format: DEPTH_FORMAT,
31        depth_write_enabled: Some(false),
32        depth_compare: Some(wgpu::CompareFunction::Always),
33        stencil: wgpu::StencilState::default(),
34        bias: wgpu::DepthBiasState::default(),
35    }
36}
37
38/// Depth state for world-space drawing: nearer fragments win and update the buffer.
39pub(crate) fn world_depth_state() -> wgpu::DepthStencilState {
40    wgpu::DepthStencilState {
41        format: DEPTH_FORMAT,
42        depth_write_enabled: Some(true),
43        depth_compare: Some(wgpu::CompareFunction::Less),
44        stencil: wgpu::StencilState::default(),
45        bias: wgpu::DepthBiasState::default(),
46    }
47}
48
49/// Depth state for the model outline pass: tested (so the expanded shell's back faces stay
50/// hidden behind the real, un-expanded mesh everywhere but the silhouette rim) but never written,
51/// so the outline never occludes anything else drawn afterward.
52pub(crate) fn outline_depth_state() -> wgpu::DepthStencilState {
53    wgpu::DepthStencilState {
54        format: DEPTH_FORMAT,
55        depth_write_enabled: Some(false),
56        depth_compare: Some(wgpu::CompareFunction::LessEqual),
57        stencil: wgpu::StencilState::default(),
58        bias: wgpu::DepthBiasState::default(),
59    }
60}
61
62trait Vertex {
63    fn desc() -> wgpu::VertexBufferLayout<'static>;
64}
65
66/// Vertex format for immediate-mode drawing of colored shapes (rects, lines, ellipses).
67#[repr(C)]
68struct ImmediateVertex {
69    pos: [f32; 2],
70    color: [f32; 4],
71}
72
73impl Vertex for ImmediateVertex {
74    fn desc() -> wgpu::VertexBufferLayout<'static> {
75        wgpu::VertexBufferLayout {
76            array_stride: std::mem::size_of::<ImmediateVertex>() as u64,
77            step_mode: wgpu::VertexStepMode::Vertex,
78            attributes: &[
79                wgpu::VertexAttribute {
80                    format: wgpu::VertexFormat::Float32x2,
81                    offset: 0,
82                    shader_location: 0,
83                },
84                wgpu::VertexAttribute {
85                    format: wgpu::VertexFormat::Float32x4,
86                    offset: std::mem::size_of::<f32>() as u64 * 2,
87                    shader_location: 1,
88                },
89            ],
90        }
91    }
92}
93
94/// Vertex format for drawing textured sprites in screen space (UI/HUD elements).
95#[repr(C)]
96struct SpriteVertex {
97    pos: [f32; 2],
98    uv: [f32; 2],
99    color: [f32; 4],
100}
101
102impl Vertex for SpriteVertex {
103    fn desc() -> wgpu::VertexBufferLayout<'static> {
104        wgpu::VertexBufferLayout {
105            array_stride: std::mem::size_of::<SpriteVertex>() as u64,
106            step_mode: wgpu::VertexStepMode::Vertex,
107            attributes: &[
108                wgpu::VertexAttribute {
109                    format: wgpu::VertexFormat::Float32x2,
110                    offset: 0,
111                    shader_location: 0,
112                },
113                wgpu::VertexAttribute {
114                    format: wgpu::VertexFormat::Float32x2,
115                    offset: std::mem::size_of::<f32>() as u64 * 2,
116                    shader_location: 1,
117                },
118                wgpu::VertexAttribute {
119                    format: wgpu::VertexFormat::Float32x4,
120                    offset: std::mem::size_of::<f32>() as u64 * 4,
121                    shader_location: 2,
122                },
123            ],
124        }
125    }
126}
127
128/// Vertex format for drawing textured sprites in world space (3D scene elements). `normal` and
129/// `highlight` are both already in their final form (computed once per draw call from the
130/// sprite's transform / `Sprite::highlight`, since — unlike `Model` — world sprites bake their
131/// transform into vertex positions on the CPU rather than carrying a model-matrix uniform), so
132/// the shader just passes them through.
133#[repr(C)]
134struct WorldSpriteVertex {
135    pos: [f32; 3],
136    uv: [f32; 2],
137    color: [f32; 4],
138    normal: [f32; 3],
139    highlight: [f32; 4],
140}
141
142impl Vertex for WorldSpriteVertex {
143    fn desc() -> wgpu::VertexBufferLayout<'static> {
144        wgpu::VertexBufferLayout {
145            array_stride: std::mem::size_of::<WorldSpriteVertex>() as u64,
146            step_mode: wgpu::VertexStepMode::Vertex,
147            attributes: &[
148                wgpu::VertexAttribute {
149                    format: wgpu::VertexFormat::Float32x3,
150                    offset: 0,
151                    shader_location: 0,
152                },
153                wgpu::VertexAttribute {
154                    format: wgpu::VertexFormat::Float32x2,
155                    offset: std::mem::size_of::<f32>() as u64 * 3,
156                    shader_location: 1,
157                },
158                wgpu::VertexAttribute {
159                    format: wgpu::VertexFormat::Float32x4,
160                    offset: std::mem::size_of::<f32>() as u64 * 5,
161                    shader_location: 2,
162                },
163                wgpu::VertexAttribute {
164                    format: wgpu::VertexFormat::Float32x3,
165                    offset: std::mem::size_of::<f32>() as u64 * 9,
166                    shader_location: 3,
167                },
168                wgpu::VertexAttribute {
169                    format: wgpu::VertexFormat::Float32x4,
170                    offset: std::mem::size_of::<f32>() as u64 * 12,
171                    shader_location: 4,
172                },
173            ],
174        }
175    }
176}
177
178/// Vertex format for the world-sprite outline pass: plain world-space positions, already baked
179/// on the CPU (see `DrawPass::draw_world_sprite_outline`) — no uv/color/normal needed since the
180/// outline is a flat, solid, unlit color.
181#[repr(C)]
182struct OutlineVertex {
183    pos: [f32; 3],
184}
185
186impl Vertex for OutlineVertex {
187    fn desc() -> wgpu::VertexBufferLayout<'static> {
188        wgpu::VertexBufferLayout {
189            array_stride: std::mem::size_of::<OutlineVertex>() as u64,
190            step_mode: wgpu::VertexStepMode::Vertex,
191            attributes: &[wgpu::VertexAttribute {
192                format: wgpu::VertexFormat::Float32x3,
193                offset: 0,
194                shader_location: 0,
195            }],
196        }
197    }
198}
199
200/// Vertex format for drawing 3D models with textures and normals.
201#[repr(C)]
202pub(crate) struct ModelVertex {
203    pub(crate) position: [f32; 3],
204    pub(crate) tex_coords: [f32; 2],
205    pub(crate) normal: [f32; 3],
206}
207
208impl Vertex for ModelVertex {
209    fn desc() -> wgpu::VertexBufferLayout<'static> {
210        wgpu::VertexBufferLayout {
211            array_stride: std::mem::size_of::<ModelVertex>() as u64,
212            step_mode: wgpu::VertexStepMode::Vertex,
213            attributes: &[
214                wgpu::VertexAttribute {
215                    format: wgpu::VertexFormat::Float32x3,
216                    offset: 0,
217                    shader_location: 0,
218                },
219                wgpu::VertexAttribute {
220                    format: wgpu::VertexFormat::Float32x2,
221                    offset: std::mem::size_of::<f32>() as u64 * 3,
222                    shader_location: 1,
223                },
224                wgpu::VertexAttribute {
225                    format: wgpu::VertexFormat::Float32x3,
226                    offset: std::mem::size_of::<f32>() as u64 * 5,
227                    shader_location: 2,
228                },
229            ],
230        }
231    }
232}
233
234pub fn create_shape_pipeline(device: &wgpu::Device, format: wgpu::TextureFormat) -> wgpu::RenderPipeline {
235    let vs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
236        label: Some("Shape Shader VS"),
237        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(VS_SRC)),
238    });
239    let fs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
240        label: Some("Shape Shader FS"),
241        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(FS_SRC)),
242    });
243
244    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
245        label: Some("Shape Pipeline Layout"),
246        bind_group_layouts: &[],
247        immediate_size: 0,
248    });
249
250    let wgpu_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
251        label: Some("Shape Pipeline"),
252        layout: Some(&layout),
253        vertex: wgpu::VertexState {
254            module: &vs_module,
255            entry_point: Some("vs_main"),
256            buffers: &[Some(wgpu::VertexBufferLayout {
257                array_stride: std::mem::size_of::<ImmediateVertex>() as u64,
258                step_mode: wgpu::VertexStepMode::Vertex,
259                attributes: &[
260                    wgpu::VertexAttribute {
261                        format: wgpu::VertexFormat::Float32x2,
262                        offset: 0,
263                        shader_location: 0,
264                    },
265                    wgpu::VertexAttribute {
266                        format: wgpu::VertexFormat::Float32x4,
267                        offset: std::mem::size_of::<f32>() as u64 * 2,
268                        shader_location: 1,
269                    },
270                ],
271            })],
272            compilation_options: wgpu::PipelineCompilationOptions::default(),
273        },
274        primitive: wgpu::PrimitiveState {
275            topology: wgpu::PrimitiveTopology::TriangleList,
276            strip_index_format: None,
277            front_face: wgpu::FrontFace::Ccw,
278            cull_mode: None,
279            unclipped_depth: false,
280            polygon_mode: wgpu::PolygonMode::Fill,
281            conservative: false,
282        },
283        depth_stencil: Some(overlay_depth_state()),
284        multisample: wgpu::MultisampleState {
285            count: 1,
286            mask: !0,
287            alpha_to_coverage_enabled: false,
288        },
289        fragment: Some(wgpu::FragmentState {
290            module: &fs_module,
291            entry_point: Some("fs_main"),
292            targets: &[Some(wgpu::ColorTargetState {
293                format,
294                blend: Some(wgpu::BlendState {
295                    color: wgpu::BlendComponent {
296                        src_factor: wgpu::BlendFactor::SrcAlpha,
297                        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
298                        operation: wgpu::BlendOperation::Add,
299                    },
300                    alpha: wgpu::BlendComponent {
301                        src_factor: wgpu::BlendFactor::SrcAlpha,
302                        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
303                        operation: wgpu::BlendOperation::Add,
304                    },
305                }),
306                write_mask: wgpu::ColorWrites::ALL,
307            })],
308            compilation_options: wgpu::PipelineCompilationOptions::default(),
309        }),
310        multiview_mask: None,
311        cache: None,
312    });
313    wgpu_pipeline
314}
315
316pub fn create_sprite_pipeline(device: &wgpu::Device, format: wgpu::TextureFormat, texture_layout: &wgpu::BindGroupLayout) -> wgpu::RenderPipeline {
317    let vs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
318        label: Some("Sprite Shader VS"),
319        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(SPRITE_VS_SRC)),
320    });
321    let fs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
322        label: Some("Sprite Shader FS"),
323        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(SPRITE_FS_SRC)),
324    });
325
326    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
327        label: Some("Sprite Pipeline Layout"),
328        bind_group_layouts: &[Some(texture_layout)],
329        immediate_size: 0,
330    });
331
332    let wgpu_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
333        label: Some("Sprite Pipeline"),
334        layout: Some(&layout),
335        vertex: wgpu::VertexState {
336            module: &vs_module,
337            entry_point: Some("vs_main"),
338            buffers: &[Some(SpriteVertex::desc())],
339            compilation_options: wgpu::PipelineCompilationOptions::default(),
340        },
341        primitive: wgpu::PrimitiveState {
342            topology: wgpu::PrimitiveTopology::TriangleList,
343            strip_index_format: None,
344            front_face: wgpu::FrontFace::Ccw,
345            cull_mode: None,
346            unclipped_depth: false,
347            polygon_mode: wgpu::PolygonMode::Fill,
348            conservative: false,
349        },
350        depth_stencil: Some(overlay_depth_state()),
351        multisample: wgpu::MultisampleState {
352            count: 1,
353            mask: !0,
354            alpha_to_coverage_enabled: false,
355        },
356        fragment: Some(wgpu::FragmentState {
357            module: &fs_module,
358            entry_point: Some("fs_main"),
359            targets: &[Some(wgpu::ColorTargetState {
360                format,
361                blend: Some(wgpu::BlendState {
362                    color: wgpu::BlendComponent {
363                        src_factor: wgpu::BlendFactor::SrcAlpha,
364                        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
365                        operation: wgpu::BlendOperation::Add,
366                    },
367                    alpha: wgpu::BlendComponent {
368                        src_factor: wgpu::BlendFactor::SrcAlpha,
369                        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
370                        operation: wgpu::BlendOperation::Add,
371                    },
372                }),
373                write_mask: wgpu::ColorWrites::ALL,
374            })],
375            compilation_options: wgpu::PipelineCompilationOptions::default(),
376        }),
377        multiview_mask: None,
378        cache: None,
379    });
380    wgpu_pipeline
381}
382
383pub fn create_world_sprite_pipeline(
384    device: &wgpu::Device,
385    format: wgpu::TextureFormat,
386    texture_layout: &wgpu::BindGroupLayout,
387    camera_layout: &wgpu::BindGroupLayout,
388) -> wgpu::RenderPipeline {
389    let vs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
390        label: Some("World Sprite Shader VS"),
391        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(WORLD_SPRITE_VS_SRC)),
392    });
393    let fs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
394        label: Some("World Sprite Shader FS"),
395        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(WORLD_SPRITE_FS_SRC)),
396    });
397
398    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
399        label: Some("World Sprite Pipeline Layout"),
400        bind_group_layouts: &[Some(texture_layout), Some(camera_layout)],
401        immediate_size: 0,
402    });
403
404    let wgpu_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
405        label: Some("World Sprite Pipeline"),
406        layout: Some(&layout),
407        vertex: wgpu::VertexState {
408            module: &vs_module,
409            entry_point: Some("vs_main"),
410            buffers: &[Some(WorldSpriteVertex::desc())],
411            compilation_options: wgpu::PipelineCompilationOptions::default(),
412        },
413        primitive: wgpu::PrimitiveState {
414            topology: wgpu::PrimitiveTopology::TriangleList,
415            strip_index_format: None,
416            front_face: wgpu::FrontFace::Ccw,
417            cull_mode: None,
418            unclipped_depth: false,
419            polygon_mode: wgpu::PolygonMode::Fill,
420            conservative: false,
421        },
422        depth_stencil: Some(world_depth_state()),
423        multisample: wgpu::MultisampleState {
424            count: 1,
425            mask: !0,
426            alpha_to_coverage_enabled: false,
427        },
428        fragment: Some(wgpu::FragmentState {
429            module: &fs_module,
430            entry_point: Some("fs_main"),
431            targets: &[Some(wgpu::ColorTargetState {
432                format,
433                blend: Some(wgpu::BlendState {
434                    color: wgpu::BlendComponent {
435                        src_factor: wgpu::BlendFactor::SrcAlpha,
436                        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
437                        operation: wgpu::BlendOperation::Add,
438                    },
439                    alpha: wgpu::BlendComponent {
440                        src_factor: wgpu::BlendFactor::SrcAlpha,
441                        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
442                        operation: wgpu::BlendOperation::Add,
443                    },
444                }),
445                write_mask: wgpu::ColorWrites::ALL,
446            })],
447            compilation_options: wgpu::PipelineCompilationOptions::default(),
448        }),
449        multiview_mask: None,
450        cache: None,
451    });
452    wgpu_pipeline
453}
454
455/// Pipeline for the world-sprite selection outline: a flat, enlarged, solid-color quad drawn
456/// behind the real sprite (see `DrawPass::draw_world_sprite_outline`). Unlike the model outline's
457/// "expand along normal" shell trick — which doesn't help a flat quad, since every vertex shares
458/// the same normal — the enlargement happens entirely on the CPU by widening the quad's local
459/// extents, so this pipeline only needs plain world-space positions. Reuses
460/// `model_outline_fs.wgsl`'s fixed solid-blue output.
461pub fn create_world_sprite_outline_pipeline(
462    device: &wgpu::Device,
463    format: wgpu::TextureFormat,
464    camera_layout: &wgpu::BindGroupLayout,
465) -> wgpu::RenderPipeline {
466    let vs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
467        label: Some("World Sprite Outline Shader VS"),
468        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(WORLD_SPRITE_OUTLINE_VS_SRC)),
469    });
470    let fs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
471        label: Some("World Sprite Outline Shader FS"),
472        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(MODEL_OUTLINE_FS_SRC)),
473    });
474
475    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
476        label: Some("World Sprite Outline Pipeline Layout"),
477        bind_group_layouts: &[None, Some(camera_layout)],
478        immediate_size: 0,
479    });
480
481    let wgpu_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
482        label: Some("World Sprite Outline Pipeline"),
483        layout: Some(&layout),
484        vertex: wgpu::VertexState {
485            module: &vs_module,
486            entry_point: Some("vs_main"),
487            buffers: &[Some(OutlineVertex::desc())],
488            compilation_options: wgpu::PipelineCompilationOptions::default(),
489        },
490        primitive: wgpu::PrimitiveState {
491            topology: wgpu::PrimitiveTopology::TriangleList,
492            strip_index_format: None,
493            front_face: wgpu::FrontFace::Ccw,
494            cull_mode: None,
495            unclipped_depth: false,
496            polygon_mode: wgpu::PolygonMode::Fill,
497            conservative: false,
498        },
499        depth_stencil: Some(outline_depth_state()),
500        multisample: wgpu::MultisampleState {
501            count: 1,
502            mask: !0,
503            alpha_to_coverage_enabled: false,
504        },
505        fragment: Some(wgpu::FragmentState {
506            module: &fs_module,
507            entry_point: Some("fs_main"),
508            targets: &[Some(wgpu::ColorTargetState {
509                format,
510                blend: Some(wgpu::BlendState {
511                    color: wgpu::BlendComponent {
512                        src_factor: wgpu::BlendFactor::SrcAlpha,
513                        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
514                        operation: wgpu::BlendOperation::Add,
515                    },
516                    alpha: wgpu::BlendComponent {
517                        src_factor: wgpu::BlendFactor::SrcAlpha,
518                        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
519                        operation: wgpu::BlendOperation::Add,
520                    },
521                }),
522                write_mask: wgpu::ColorWrites::ALL,
523            })],
524            compilation_options: wgpu::PipelineCompilationOptions::default(),
525        }),
526        multiview_mask: None,
527        cache: None,
528    });
529    wgpu_pipeline
530}
531
532pub fn create_model_pipeline(
533    device: &wgpu::Device,
534    format: wgpu::TextureFormat,
535    texture_layout: &wgpu::BindGroupLayout,
536    camera_layout: &wgpu::BindGroupLayout,
537    model_transform_layout: &wgpu::BindGroupLayout,
538) -> wgpu::RenderPipeline {
539    let vs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
540        label: Some("Model Shader VS"),
541        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(MODEL_VS_SRC)),
542    });
543    let fs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
544        label: Some("Model Shader FS"),
545        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(MODEL_FS_SRC)),
546    });
547
548    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
549        label: Some("Model Pipeline Layout"),
550        bind_group_layouts: &[Some(texture_layout), Some(camera_layout), Some(model_transform_layout)],
551        immediate_size: 0,
552    });
553
554    let wgpu_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
555        label: Some("Model Pipeline"),
556        layout: Some(&layout),
557        vertex: wgpu::VertexState {
558            module: &vs_module,
559            entry_point: Some("vs_main"),
560            buffers: &[Some(ModelVertex::desc())],
561            compilation_options: wgpu::PipelineCompilationOptions::default(),
562        },
563        primitive: wgpu::PrimitiveState {
564            topology: wgpu::PrimitiveTopology::TriangleList,
565            strip_index_format: None,
566            front_face: wgpu::FrontFace::Ccw,
567            cull_mode: Some(wgpu::Face::Back),
568            unclipped_depth: false,
569            polygon_mode: wgpu::PolygonMode::Fill,
570            conservative: false,
571        },
572        depth_stencil: Some(world_depth_state()),
573        multisample: wgpu::MultisampleState {
574            count: 1,
575            mask: !0,
576            alpha_to_coverage_enabled: false,
577        },
578        fragment: Some(wgpu::FragmentState {
579            module: &fs_module,
580            entry_point: Some("fs_main"),
581            targets: &[Some(wgpu::ColorTargetState {
582                format,
583                blend: Some(wgpu::BlendState {
584                    color: wgpu::BlendComponent {
585                        src_factor: wgpu::BlendFactor::SrcAlpha,
586                        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
587                        operation: wgpu::BlendOperation::Add,
588                    },
589                    alpha: wgpu::BlendComponent {
590                        src_factor: wgpu::BlendFactor::SrcAlpha,
591                        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
592                        operation: wgpu::BlendOperation::Add,
593                    },
594                }),
595                write_mask: wgpu::ColorWrites::ALL,
596            })],
597            compilation_options: wgpu::PipelineCompilationOptions::default(),
598        }),
599        multiview_mask: None,
600        cache: None,
601    });
602    wgpu_pipeline
603}
604
605/// Pipeline for the selection-outline pass: expands each vertex outward along its normal (see
606/// `model_outline_vs.wgsl`) and rasterizes only the expanded shell's back faces (`cull_mode:
607/// Front`), which are hidden behind the real mesh everywhere except right at the silhouette rim.
608/// No texture binding is needed, so group 0 is skipped, matching how the immediate/shape
609/// pipelines skip unused groups.
610pub fn create_model_outline_pipeline(
611    device: &wgpu::Device,
612    format: wgpu::TextureFormat,
613    camera_layout: &wgpu::BindGroupLayout,
614    model_transform_layout: &wgpu::BindGroupLayout,
615) -> wgpu::RenderPipeline {
616    let vs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
617        label: Some("Model Outline Shader VS"),
618        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(MODEL_OUTLINE_VS_SRC)),
619    });
620    let fs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
621        label: Some("Model Outline Shader FS"),
622        source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(MODEL_OUTLINE_FS_SRC)),
623    });
624
625    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
626        label: Some("Model Outline Pipeline Layout"),
627        bind_group_layouts: &[None, Some(camera_layout), Some(model_transform_layout)],
628        immediate_size: 0,
629    });
630
631    let wgpu_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
632        label: Some("Model Outline Pipeline"),
633        layout: Some(&layout),
634        vertex: wgpu::VertexState {
635            module: &vs_module,
636            entry_point: Some("vs_main"),
637            buffers: &[Some(ModelVertex::desc())],
638            compilation_options: wgpu::PipelineCompilationOptions::default(),
639        },
640        primitive: wgpu::PrimitiveState {
641            topology: wgpu::PrimitiveTopology::TriangleList,
642            strip_index_format: None,
643            front_face: wgpu::FrontFace::Ccw,
644            cull_mode: Some(wgpu::Face::Front),
645            unclipped_depth: false,
646            polygon_mode: wgpu::PolygonMode::Fill,
647            conservative: false,
648        },
649        depth_stencil: Some(outline_depth_state()),
650        multisample: wgpu::MultisampleState {
651            count: 1,
652            mask: !0,
653            alpha_to_coverage_enabled: false,
654        },
655        fragment: Some(wgpu::FragmentState {
656            module: &fs_module,
657            entry_point: Some("fs_main"),
658            targets: &[Some(wgpu::ColorTargetState {
659                format,
660                blend: Some(wgpu::BlendState {
661                    color: wgpu::BlendComponent {
662                        src_factor: wgpu::BlendFactor::SrcAlpha,
663                        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
664                        operation: wgpu::BlendOperation::Add,
665                    },
666                    alpha: wgpu::BlendComponent {
667                        src_factor: wgpu::BlendFactor::SrcAlpha,
668                        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
669                        operation: wgpu::BlendOperation::Add,
670                    },
671                }),
672                write_mask: wgpu::ColorWrites::ALL,
673            })],
674            compilation_options: wgpu::PipelineCompilationOptions::default(),
675        }),
676        multiview_mask: None,
677        cache: None,
678    });
679    wgpu_pipeline
680}
681
682impl DrawPass<'_> {
683    pub fn draw_rect(&mut self, x: f32, y: f32, w: f32, h: f32, thickness: f32, color: Color) {
684        let c = color.as_wgpu_color();
685        let rgba = [c.r as f32, c.g as f32, c.b as f32, c.a as f32];
686        let mut verts: Vec<ImmediateVertex> = Vec::new();
687
688        if thickness <= 0.0 {
689            let x2 = x + w;
690            let y2 = y + h;
691            let p0 = self.to_clip(x, y);
692            let p1 = self.to_clip(x2, y);
693            let p2 = self.to_clip(x, y2);
694            let p3 = self.to_clip(x2, y2);
695
696            verts.push(ImmediateVertex { pos: p0, color: rgba });
697            verts.push(ImmediateVertex { pos: p1, color: rgba });
698            verts.push(ImmediateVertex { pos: p2, color: rgba });
699            verts.push(ImmediateVertex { pos: p1, color: rgba });
700            verts.push(ImmediateVertex { pos: p3, color: rgba });
701            verts.push(ImmediateVertex { pos: p2, color: rgba });
702        } else {
703            let th = thickness;
704            for &(px, py) in &[
705                (x, y), (x + w, y), (x, y + th),
706                (x + w, y), (x + w, y + th), (x, y + th),
707            ] {
708                verts.push(ImmediateVertex { pos: self.to_clip(px, py), color: rgba });
709            }
710            for &(px, py) in &[
711                (x, y + h - th), (x + w, y + h - th), (x, y + h),
712                (x + w, y + h - th), (x + w, y + h), (x, y + h),
713            ] {
714                verts.push(ImmediateVertex { pos: self.to_clip(px, py), color: rgba });
715            }
716            for &(px, py) in &[
717                (x, y), (x + th, y), (x, y + h),
718                (x + th, y), (x + th, y + h), (x, y + h),
719            ] {
720                verts.push(ImmediateVertex { pos: self.to_clip(px, py), color: rgba });
721            }
722            for &(px, py) in &[
723                (x + w - th, y), (x + w, y), (x + w - th, y + h),
724                (x + w, y), (x + w, y + h), (x + w - th, y + h),
725            ] {
726                verts.push(ImmediateVertex { pos: self.to_clip(px, py), color: rgba });
727            }
728        }
729
730        self.upload_and_draw(&verts);
731    }
732
733    pub fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: Color) {
734        let dx = x2 - x1;
735        let dy = y2 - y1;
736        let len = (dx * dx + dy * dy).sqrt();
737        if len == 0.0 {
738            return;
739        }
740
741        let c = color.as_wgpu_color();
742        let rgba = [c.r as f32, c.g as f32, c.b as f32, c.a as f32];
743
744        let hw = thickness * 0.5;
745        let px = -dy / len * hw;
746        let py = dx / len * hw;
747
748        let p0 = self.to_clip(x1 + px, y1 + py);
749        let p1 = self.to_clip(x1 - px, y1 - py);
750        let p2 = self.to_clip(x2 + px, y2 + py);
751        let p3 = self.to_clip(x2 - px, y2 - py);
752
753        let verts = vec![
754            ImmediateVertex { pos: p0, color: rgba },
755            ImmediateVertex { pos: p1, color: rgba },
756            ImmediateVertex { pos: p2, color: rgba },
757            ImmediateVertex { pos: p1, color: rgba },
758            ImmediateVertex { pos: p3, color: rgba },
759            ImmediateVertex { pos: p2, color: rgba },
760        ];
761
762        self.upload_and_draw(&verts);
763    }
764
765    pub fn draw_ellipse(&mut self, cx: f32, cy: f32, rx: f32, ry: f32, segments: u32, thickness: f32, color: Color) {
766        if segments < 3 {
767            return;
768        }
769
770        let c = color.as_wgpu_color();
771        let rgba = [c.r as f32, c.g as f32, c.b as f32, c.a as f32];
772
773        let step = std::f32::consts::TAU / segments as f32;
774        let mut verts = Vec::new();
775
776        if thickness <= 0.0 {
777            let center = self.to_clip(cx, cy);
778            for i in 0..segments {
779                let a1 = i as f32 * step;
780                let a2 = (i + 1) as f32 * step;
781                let p1 = self.to_clip(cx + rx * a1.cos(), cy + ry * a1.sin());
782                let p2 = self.to_clip(cx + rx * a2.cos(), cy + ry * a2.sin());
783                verts.push(ImmediateVertex { pos: center, color: rgba });
784                verts.push(ImmediateVertex { pos: p1, color: rgba });
785                verts.push(ImmediateVertex { pos: p2, color: rgba });
786            }
787        } else {
788            let hw = thickness * 0.5;
789            for i in 0..segments {
790                let a1 = i as f32 * step;
791                let a2 = (i + 1) as f32 * step;
792                let cos1 = a1.cos();
793                let sin1 = a1.sin();
794                let cos2 = a2.cos();
795                let sin2 = a2.sin();
796
797                let p_o1 = self.to_clip(cx + (rx + hw) * cos1, cy + (ry + hw) * sin1);
798                let p_i1 = self.to_clip(cx + (rx - hw) * cos1, cy + (ry - hw) * sin1);
799                let p_o2 = self.to_clip(cx + (rx + hw) * cos2, cy + (ry + hw) * sin2);
800                let p_i2 = self.to_clip(cx + (rx - hw) * cos2, cy + (ry - hw) * sin2);
801
802                verts.push(ImmediateVertex { pos: p_o1, color: rgba });
803                verts.push(ImmediateVertex { pos: p_i1, color: rgba });
804                verts.push(ImmediateVertex { pos: p_o2, color: rgba });
805                verts.push(ImmediateVertex { pos: p_i1, color: rgba });
806                verts.push(ImmediateVertex { pos: p_i2, color: rgba });
807                verts.push(ImmediateVertex { pos: p_o2, color: rgba });
808            }
809        }
810
811        self.upload_and_draw(&verts);
812    }
813
814    /// A line between two world-space points, projected through `camera`. Unlike `draw_model`/
815    /// `draw_world_sprite`, not depth-tested — for editor-only gizmos that should always draw on
816    /// top. No-ops if an endpoint is behind the camera (`w <= 0`) rather than drawing a
817    /// folded-back bogus line.
818    pub fn draw_world_line(&mut self, camera: &crate::camera::Camera, p0: glam::Vec3, p1: glam::Vec3, thickness: f32, color: Color) {
819        let view_proj = camera.build_view_projection_matrix();
820        let (Some(a), Some(b)) = (
821            Self::project_to_screen(view_proj, p0, self.screen_w, self.screen_h),
822            Self::project_to_screen(view_proj, p1, self.screen_w, self.screen_h),
823        ) else {
824            return;
825        };
826        self.draw_line(a.0, a.1, b.0, b.1, thickness, color);
827    }
828
829    fn project_to_screen(view_proj: glam::Mat4, point: glam::Vec3, screen_w: u32, screen_h: u32) -> Option<(f32, f32)> {
830        let clip = view_proj * point.extend(1.0);
831        if clip.w <= 1e-4 {
832            return None;
833        }
834        let ndc = clip.truncate() / clip.w;
835        let w = screen_w.max(1) as f32;
836        let h = screen_h.max(1) as f32;
837        Some(((ndc.x * 0.5 + 0.5) * w, (1.0 - (ndc.y * 0.5 + 0.5)) * h))
838    }
839
840    fn to_clip(&self, x: f32, y: f32) -> [f32; 2] {
841        let w = (self.screen_w.max(1)) as f32;
842        let h = (self.screen_h.max(1)) as f32;
843        [(x / w) * 2.0 - 1.0, -((y / h) * 2.0 - 1.0)]
844    }
845
846    fn upload_and_draw(&mut self, verts: &[ImmediateVertex]) {
847        if verts.is_empty() {
848            return;
849        }
850        let data = crate::util::slice_to_bytes(verts);
851        let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
852            label: Some("Shape Vert Buffer"),
853            size: data.len() as u64,
854            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
855            mapped_at_creation: false,
856        });
857        self.queue.write_buffer(&buffer, 0, data);
858        self.pass.set_pipeline(&self.immediate_pipeline);
859        self.pass.set_vertex_buffer(0, buffer.slice(..data.len() as u64));
860        self.pass.draw(0..verts.len() as u32, 0..1);
861    }
862
863    /// Draws a sprite in screen space (pixel coordinates), ignoring the camera. Use for UI/HUD elements.
864    ///
865    /// `model` transforms the quad in the sprite's local space, where the origin is the
866    /// top-left corner and the quad spans `(0, 0)..(w, h)`. The result is then offset by
867    /// `(x, y)`. Only the XY components of the transform are used.
868    pub fn draw_ui_sprite(
869        &mut self,
870        x: f32, y: f32, w: f32, h: f32,
871        src_x: f32, src_y: f32, src_w: f32, src_h: f32, tex_w: f32, tex_h: f32,
872        color: Color,
873        model: glam::Mat4,
874        bind_group: &wgpu::BindGroup,
875    ) {
876        let c = color.as_wgpu_color();
877        let rgba = [c.r as f32, c.g as f32, c.b as f32, c.a as f32];
878
879        let corner = |lx: f32, ly: f32| {
880            let p = model.transform_point3(glam::Vec3::new(lx, ly, 0.0));
881            self.to_clip(x + p.x, y + p.y)
882        };
883
884        let p0 = corner(0.0, 0.0);
885        let p1 = corner(w, 0.0);
886        let p2 = corner(0.0, h);
887        let p3 = corner(w, h);
888
889        let uv0 = [src_x / tex_w, src_y / tex_h];
890        let uv1 = [(src_x + src_w) / tex_w, src_y / tex_h];
891        let uv2 = [src_x / tex_w, (src_y + src_h) / tex_h];
892        let uv3 = [(src_x + src_w) / tex_w, (src_y + src_h) / tex_h];
893
894        let verts = vec![
895            SpriteVertex { pos: p0, uv: uv0, color: rgba },
896            SpriteVertex { pos: p1, uv: uv1, color: rgba },
897            SpriteVertex { pos: p2, uv: uv2, color: rgba },
898            SpriteVertex { pos: p1, uv: uv1, color: rgba },
899            SpriteVertex { pos: p3, uv: uv3, color: rgba },
900            SpriteVertex { pos: p2, uv: uv2, color: rgba },
901        ];
902
903        let data = crate::util::slice_to_bytes(&verts);
904        let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
905            label: Some("Sprite Vert Buffer"),
906            size: data.len() as u64,
907            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
908            mapped_at_creation: false,
909        });
910        self.queue.write_buffer(&buffer, 0, data);
911        self.pass.set_pipeline(&self.sprite_pipeline);
912        self.pass.set_bind_group(0, bind_group, &[]);
913        self.pass.set_vertex_buffer(0, buffer.slice(..data.len() as u64));
914        self.pass.draw(0..verts.len() as u32, 0..1);
915    }
916
917    /// Draws a sprite as a quad in world space (on the XY plane at depth `z`), transformed by the camera's view-projection matrix.
918    ///
919    /// `model` transforms the quad in the sprite's local space, where the origin is the
920    /// bottom-left corner and the quad spans `(0, 0)..(w, h)`. The result is then offset by
921    /// `(x, y, z)`, so a rotation about Z spins the quad in place rather than orbiting the
922    /// world origin.
923    pub fn draw_world_sprite(
924        &mut self,
925        x: f32, y: f32, z: f32, w: f32, h: f32,
926        src_x: f32, src_y: f32, src_w: f32, src_h: f32, tex_w: f32, tex_h: f32,
927        color: Color,
928        highlight: Color,
929        model: glam::Mat4,
930        bind_group: &wgpu::BindGroup,
931    ) {
932        let c = color.as_wgpu_color();
933        let rgba = [c.r as f32, c.g as f32, c.b as f32, c.a as f32];
934        let hl = highlight.as_wgpu_color();
935        let highlight_rgba = [hl.r as f32, hl.g as f32, hl.b as f32, hl.a as f32];
936
937        let origin = glam::Vec3::new(x, y, z);
938        let corner = |lx: f32, ly: f32| {
939            let p = origin + model.transform_point3(glam::Vec3::new(lx, ly, 0.0));
940            [p.x, p.y, p.z]
941        };
942
943        // The quad faces +Z in local space; `transform_vector3` applies only the transform's
944        // linear part (no translation), which is what a normal needs. Same value for all six
945        // vertices since the quad is flat and rigid.
946        let n = model.transform_vector3(glam::Vec3::Z).normalize_or_zero();
947        let normal = [n.x, n.y, n.z];
948
949        let p0 = corner(0.0, 0.0);
950        let p1 = corner(w, 0.0);
951        let p2 = corner(0.0, h);
952        let p3 = corner(w, h);
953
954        // World space is Y-up but texture space is Y-down, so the quad's top
955        // edge (y + h) samples the top of the source rect, not the bottom.
956        let u_left = src_x / tex_w;
957        let u_right = (src_x + src_w) / tex_w;
958        let v_top = src_y / tex_h;
959        let v_bottom = (src_y + src_h) / tex_h;
960
961        let uv0 = [u_left, v_bottom];
962        let uv1 = [u_right, v_bottom];
963        let uv2 = [u_left, v_top];
964        let uv3 = [u_right, v_top];
965
966        let verts = vec![
967            WorldSpriteVertex { pos: p0, uv: uv0, color: rgba, normal, highlight: highlight_rgba },
968            WorldSpriteVertex { pos: p1, uv: uv1, color: rgba, normal, highlight: highlight_rgba },
969            WorldSpriteVertex { pos: p2, uv: uv2, color: rgba, normal, highlight: highlight_rgba },
970            WorldSpriteVertex { pos: p1, uv: uv1, color: rgba, normal, highlight: highlight_rgba },
971            WorldSpriteVertex { pos: p3, uv: uv3, color: rgba, normal, highlight: highlight_rgba },
972            WorldSpriteVertex { pos: p2, uv: uv2, color: rgba, normal, highlight: highlight_rgba },
973        ];
974
975        let data = crate::util::slice_to_bytes(&verts);
976        let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
977            label: Some("World Sprite Vert Buffer"),
978            size: data.len() as u64,
979            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
980            mapped_at_creation: false,
981        });
982        self.queue.write_buffer(&buffer, 0, data);
983        self.pass.set_pipeline(&self.world_sprite_pipeline);
984        self.pass.set_bind_group(0, bind_group, &[]);
985        self.pass.set_bind_group(1, self.camera_bind_group, &[]);
986        self.pass.set_vertex_buffer(0, buffer.slice(..data.len() as u64));
987        self.pass.draw(0..verts.len() as u32, 0..1);
988    }
989
990    /// Draws a solid-blue selection outline behind a world sprite: a flat quad matching
991    /// `draw_world_sprite`'s own quad but enlarged (in the sprite's local XY plane) by a margin
992    /// proportional to its size. Coplanar with the real sprite, so it relies on draw order —
993    /// call this *before* [`DrawPass::draw_world_sprite`] for the same sprite — rather than depth
994    /// testing to stay confined to a border: the real sprite drawn afterward fully covers its own
995    /// footprint (and partially-transparent texture pixels let a thin rim of the outline show
996    /// through at the edges).
997    pub fn draw_world_sprite_outline(&mut self, x: f32, y: f32, z: f32, w: f32, h: f32, model: glam::Mat4) {
998        const OUTLINE_MARGIN_FRACTION: f32 = 0.02;
999        let margin = OUTLINE_MARGIN_FRACTION * w.max(h);
1000
1001        let origin = glam::Vec3::new(x, y, z);
1002        let corner = |lx: f32, ly: f32| {
1003            let p = origin + model.transform_point3(glam::Vec3::new(lx, ly, 0.0));
1004            [p.x, p.y, p.z]
1005        };
1006
1007        let p0 = corner(-margin, -margin);
1008        let p1 = corner(w + margin, -margin);
1009        let p2 = corner(-margin, h + margin);
1010        let p3 = corner(w + margin, h + margin);
1011
1012        let verts = vec![
1013            OutlineVertex { pos: p0 },
1014            OutlineVertex { pos: p1 },
1015            OutlineVertex { pos: p2 },
1016            OutlineVertex { pos: p1 },
1017            OutlineVertex { pos: p3 },
1018            OutlineVertex { pos: p2 },
1019        ];
1020
1021        let data = crate::util::slice_to_bytes(&verts);
1022        let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1023            label: Some("World Sprite Outline Vert Buffer"),
1024            size: data.len() as u64,
1025            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1026            mapped_at_creation: false,
1027        });
1028        self.queue.write_buffer(&buffer, 0, data);
1029        self.pass.set_pipeline(self.world_sprite_outline_pipeline);
1030        self.pass.set_bind_group(1, self.camera_bind_group, &[]);
1031        self.pass.set_vertex_buffer(0, buffer.slice(..data.len() as u64));
1032        self.pass.draw(0..verts.len() as u32, 0..1);
1033    }
1034
1035    /// Draws a loaded [`Model`] in world space, transformed by the camera and the model's own
1036    /// transform. Each mesh is drawn with its assigned material's texture bound.
1037    pub fn draw_model(&mut self, model: &Model) {
1038        self.write_model_uniform(model);
1039
1040        self.pass.set_pipeline(self.model_pipeline);
1041        self.pass.set_bind_group(1, self.camera_bind_group, &[]);
1042        self.pass.set_bind_group(2, &model.transform_bind_group, &[]);
1043
1044        for mesh in &model.meshes {
1045            let material = &model.materials[mesh.material];
1046            self.pass.set_bind_group(0, &material.diffuse_texture.bind_group, &[]);
1047            self.pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1048            self.pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1049            self.pass.draw_indexed(0..mesh.num_indices, 0, 0..1);
1050        }
1051    }
1052
1053    /// Draws a solid-blue selection outline around `model` (see [`create_model_outline_pipeline`]
1054    /// for the technique). Intended to be called right after [`DrawPass::draw_model`] for the
1055    /// same model, e.g. by editor-style tooling highlighting the selected entity.
1056    pub fn draw_model_outline(&mut self, model: &Model) {
1057        self.write_model_uniform(model);
1058
1059        self.pass.set_pipeline(self.model_outline_pipeline);
1060        self.pass.set_bind_group(1, self.camera_bind_group, &[]);
1061        self.pass.set_bind_group(2, &model.transform_bind_group, &[]);
1062
1063        for mesh in &model.meshes {
1064            self.pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1065            self.pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1066            self.pass.draw_indexed(0..mesh.num_indices, 0, 0..1);
1067        }
1068    }
1069
1070    fn write_model_uniform(&self, model: &Model) {
1071        let highlight = model.highlight.as_wgpu_color();
1072        let uniform = ModelUniform {
1073            model: model.transform.to_cols_array_2d(),
1074            highlight: [highlight.r as f32, highlight.g as f32, highlight.b as f32, highlight.a as f32],
1075        };
1076        self.queue.write_buffer(&model.transform_buffer, 0, crate::util::slice_to_bytes(&[uniform]));
1077    }
1078}