dialog WIP paths consolidation and rendering

This commit is contained in:
Jonas H
2026-03-28 10:34:19 +01:00
parent 4c3ebca96e
commit 11b31169b1
70 changed files with 2658 additions and 485 deletions

276
src/render/billboard.rs Normal file
View File

@@ -0,0 +1,276 @@
use crate::paths;
use bytemuck::{Pod, Zeroable};
use wgpu::util::DeviceExt;
const MAX_BUBBLES: usize = 8;
const UNIFORM_STRIDE: usize = 256;
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct BillboardVertex
{
pub position: [f32; 3],
pub uv: [f32; 2],
}
impl BillboardVertex
{
pub fn desc() -> wgpu::VertexBufferLayout<'static>
{
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<BillboardVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2,
},
],
}
}
}
/// Uniform block layout must match `bubble.wgsl` exactly (including implicit WGSL padding).
///
/// WGSL layout (offsets in bytes):
/// view_proj : mat4x4<f32> → offset 0, size 64
/// size : vec2<f32> → offset 64, size 8
/// body_frac : f32 → offset 72, size 4
/// corner_r : f32 → offset 76, size 4
/// border_w : f32 → offset 80, size 4
/// [12 bytes WGSL padding to align vec4]
/// fill_color : vec4<f32> → offset 96, size 16
/// border_color : vec4<f32> → offset 112, size 16
/// total WGSL struct: 128 bytes
///
/// Rust struct is padded to 256 bytes for the dynamic-offset alignment requirement.
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct BubbleUniforms
{
pub view_proj: [[f32; 4]; 4],
pub size: [f32; 2],
pub body_frac: f32,
pub corner_r: f32,
pub border_w: f32,
pub _pad1: [f32; 3],
pub fill_color: [f32; 4],
pub border_color: [f32; 4],
pub _pad2: [f32; 32],
}
const _: () = assert!(std::mem::size_of::<BubbleUniforms>() == UNIFORM_STRIDE);
pub struct BillboardDrawCall
{
/// Four corners in world space: top-left, top-right, bottom-right, bottom-left.
pub vertices: [BillboardVertex; 4],
pub uniforms: BubbleUniforms,
}
pub struct BillboardPipeline
{
pipeline: wgpu::RenderPipeline,
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
}
impl BillboardPipeline
{
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self
{
let shader_source =
std::fs::read_to_string(&paths::shaders::bubble()).expect("Failed to read bubble.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Bubble Shader"),
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Billboard Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: std::num::NonZeroU64::new(
std::mem::size_of::<BubbleUniforms>() as u64,
),
},
count: None,
}],
});
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Billboard Uniform Buffer"),
size: (MAX_BUBBLES * UNIFORM_STRIDE) as wgpu::BufferAddress,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Billboard Bind Group"),
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &uniform_buffer,
offset: 0,
size: std::num::NonZeroU64::new(std::mem::size_of::<BubbleUniforms>() as u64),
}),
}],
});
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Billboard Vertex Buffer"),
size: (MAX_BUBBLES * 4 * std::mem::size_of::<BillboardVertex>()) as wgpu::BufferAddress,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let indices: [u16; 6] = [0, 1, 2, 2, 3, 0];
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Billboard Index Buffer"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Billboard Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Billboard Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[BillboardVertex::desc()],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::LessEqual,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
});
Self {
pipeline,
vertex_buffer,
index_buffer,
uniform_buffer,
bind_group,
}
}
pub fn render(
&self,
encoder: &mut wgpu::CommandEncoder,
queue: &wgpu::Queue,
color_view: &wgpu::TextureView,
depth_view: &wgpu::TextureView,
calls: &[BillboardDrawCall],
)
{
if calls.is_empty()
{
return;
}
let n = calls.len().min(MAX_BUBBLES);
let mut all_vertices: Vec<BillboardVertex> = Vec::with_capacity(n * 4);
for call in calls.iter().take(n)
{
all_vertices.extend_from_slice(&call.vertices);
}
queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&all_vertices));
for (i, call) in calls.iter().take(n).enumerate()
{
queue.write_buffer(
&self.uniform_buffer,
(i * UNIFORM_STRIDE) as u64,
bytemuck::cast_slice(&[call.uniforms]),
);
}
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Billboard Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
for i in 0..n
{
let dynamic_offset = (i * UNIFORM_STRIDE) as u32;
pass.set_bind_group(0, &self.bind_group, &[dynamic_offset]);
pass.draw_indexed(0..6, (i * 4) as i32, 0..1);
}
}
}
}

View File

@@ -1,3 +1,4 @@
use crate::paths;
use crate::postprocess::ScreenVertex;
pub struct DebugOverlay
@@ -12,7 +13,7 @@ impl DebugOverlay
{
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self
{
let shader_source = std::fs::read_to_string("src/shaders/debug_overlay.wgsl")
let shader_source = std::fs::read_to_string(&paths::shaders::debug_overlay())
.expect("Failed to read debug_overlay.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {

View File

@@ -1,14 +1,17 @@
pub mod billboard;
mod bind_group;
mod debug_overlay;
mod pipeline;
mod shadow;
mod types;
pub use billboard::{BillboardDrawCall, BillboardPipeline};
pub use types::{DrawCall, Pipeline, Spotlight, SpotlightRaw, Uniforms, MAX_SPOTLIGHTS};
use crate::entity::EntityHandle;
use crate::debug::DebugMode;
use crate::paths;
use crate::postprocess::{create_blit_pipeline, create_fullscreen_quad, LowResFramebuffer};
use crate::texture::{DitherTextures, FlowmapTexture};
use pipeline::{
@@ -34,6 +37,7 @@ pub struct Renderer
wireframe_pipeline: Option<wgpu::RenderPipeline>,
debug_lines_pipeline: Option<wgpu::RenderPipeline>,
debug_overlay: Option<debug_overlay::DebugOverlay>,
billboard_pipeline: BillboardPipeline,
wireframe_supported: bool,
uniform_buffer: wgpu::Buffer,
@@ -162,7 +166,7 @@ impl Renderer
};
let flowmap_texture =
match FlowmapTexture::load(&device, &queue, "textures/terrain_flowmap.exr")
match FlowmapTexture::load(&device, &queue, &paths::textures::terrain_flowmap())
{
Ok(texture) =>
{
@@ -179,7 +183,7 @@ impl Renderer
}
};
let blue_noise_data = image::open("textures/blue_noise.png")
let blue_noise_data = image::open(&paths::textures::blue_noise())
.expect("Failed to load blue noise texture")
.to_luma8();
let blue_noise_size = blue_noise_data.dimensions();
@@ -490,6 +494,8 @@ impl Renderer
&bind_group_layout,
));
let billboard_pipeline = BillboardPipeline::new(&device, config.format);
let debug_overlay = Some(debug_overlay::DebugOverlay::new(&device, config.format));
let shadow_bind_group_layout =
@@ -518,6 +524,7 @@ impl Renderer
wireframe_pipeline,
debug_lines_pipeline,
debug_overlay,
billboard_pipeline,
wireframe_supported,
uniform_buffer,
bind_group_layout,
@@ -561,6 +568,7 @@ impl Renderer
camera_position: glam::Vec3,
player_position: glam::Vec3,
draw_calls: &[DrawCall],
billboard_calls: &[BillboardDrawCall],
time: f32,
delta_time: f32,
debug_mode: DebugMode,
@@ -939,6 +947,14 @@ impl Renderer
}
}
self.billboard_pipeline.render(
&mut encoder,
&self.queue,
&self.framebuffer.view,
&self.framebuffer.depth_view,
billboard_calls,
);
self.queue.submit(std::iter::once(encoder.finish()));
let frame = match self.surface.get_current_texture()
@@ -1049,7 +1065,7 @@ impl Renderer
match crate::texture::HeightmapTexture::load(
&self.device,
&self.queue,
"textures/terrain_heightmap.exr",
&paths::textures::terrain_heightmap(),
)
{
Ok(heightmap) =>
@@ -1151,6 +1167,7 @@ pub fn render(
camera_position: glam::Vec3,
player_position: glam::Vec3,
draw_calls: &[DrawCall],
billboard_calls: &[BillboardDrawCall],
time: f32,
delta_time: f32,
debug_mode: DebugMode,
@@ -1165,6 +1182,7 @@ pub fn render(
camera_position,
player_position,
draw_calls,
billboard_calls,
time,
delta_time,
debug_mode,

View File

@@ -1,3 +1,4 @@
use crate::paths;
use wesl::Wesl;
pub fn create_shadow_pipeline(
@@ -5,9 +6,9 @@ pub fn create_shadow_pipeline(
bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline
{
let compiler = Wesl::new("src/shaders");
let compiler = Wesl::new(&paths::SHADERS_DIR);
let shader_source = compiler
.compile(&"package::shadow".parse().unwrap())
.compile(&paths::shaders::SHADOW_PACKAGE.parse().unwrap())
.inspect_err(|e| eprintln!("WESL error: {e}"))
.unwrap()
.to_string();
@@ -70,9 +71,9 @@ pub fn create_main_pipeline(
label: &str,
) -> wgpu::RenderPipeline
{
let compiler = Wesl::new("src/shaders");
let compiler = Wesl::new(&paths::SHADERS_DIR);
let shader_source = compiler
.compile(&"package::main".parse().unwrap())
.compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
.inspect_err(|e| eprintln!("WESL error: {e}"))
.unwrap()
.to_string();
@@ -142,9 +143,9 @@ pub fn create_wireframe_pipeline(
bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline
{
let compiler = Wesl::new("src/shaders");
let compiler = Wesl::new(&paths::SHADERS_DIR);
let shader_source = compiler
.compile(&"package::main".parse().unwrap())
.compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
.inspect_err(|e| eprintln!("WESL error: {e}"))
.unwrap()
.to_string();
@@ -214,9 +215,9 @@ pub fn create_debug_lines_pipeline(
bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline
{
let compiler = Wesl::new("src/shaders");
let compiler = Wesl::new(&paths::SHADERS_DIR);
let shader_source = compiler
.compile(&"package::main".parse().unwrap())
.compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
.inspect_err(|e| eprintln!("WESL error: {e}"))
.unwrap()
.to_string();
@@ -286,9 +287,9 @@ pub fn create_snow_clipmap_pipeline(
main_bind_group_layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline
{
let compiler = Wesl::new("src/shaders");
let compiler = Wesl::new(&paths::SHADERS_DIR);
let shader_source = compiler
.compile(&"package::main".parse().unwrap())
.compile(&paths::shaders::MAIN_PACKAGE.parse().unwrap())
.inspect_err(|e| eprintln!("WESL error: {e}"))
.unwrap()
.to_string();