use bytemuck::{Pod, Zeroable};
use glam::Vec3;
use crate::ecs::{Application, Component, Entity, Plugin, Query, ResMut, Resource, World};
use crate::render3d::DEPTH_FORMAT;
use crate::scene::SceneEntity;
use crate::sceneobjects::lights::{Light, LightShape, LocalLight, Suns};
use crate::ui::{Color, Widget, linear_rgba};
pub use crate::helpers::grid::{Grid, grid};
#[derive(Component, Clone, Debug)]
pub struct Wires {
pub segments: Vec<[Vec3; 2]>,
pub color: Color,
}
impl Wires {
pub fn new(color: Color) -> Self {
Self {
segments: Vec::new(),
color,
}
}
pub fn line(&mut self, from: Vec3, to: Vec3) -> &mut Self {
self.segments.push([from, to]);
self
}
pub fn ring(&mut self, middle: Vec3, across: Vec3, down: Vec3, sides: usize) -> &mut Self {
let point = |i: usize| {
let angle = i as f32 / sides as f32 * std::f32::consts::TAU;
middle + across * angle.cos() + down * angle.sin()
};
for i in 0..sides {
self.line(point(i), point((i + 1) % sides));
}
self
}
pub fn dashed(&mut self, from: Vec3, to: Vec3, dashes: usize) -> &mut Self {
for i in 0..dashes {
let start = i as f32 / dashes as f32;
let end = start + 0.55 / dashes as f32;
self.line(from.lerp(to, start), from.lerp(to, end));
}
self
}
}
impl Widget for Wires {
type Output = Entity;
fn spawn(self, world: &mut World, _screen_width: f32, _screen_height: f32) -> Entity {
world.spawn((self, SceneEntity)).id()
}
}
const RING_SIDES: usize = 24;
const RAYS: usize = 8;
const RAY_INNER: f32 = 1.35;
const RAY_OUTER: f32 = 2.1;
pub fn light(light: &Light, focus: Vec3, distance: f32) -> Wires {
let towards = light.direction.normalize_or(Vec3::Y);
let tint = light.color();
let mut wires = Wires::new(Color::linear_rgba(tint.x, tint.y, tint.z, 1.0));
let any = match towards.y.abs() > 0.99 {
true => Vec3::Z,
false => Vec3::Y,
};
let across = towards.cross(any).normalize_or(Vec3::X);
let down = towards.cross(across).normalize_or(Vec3::Z);
let at = focus + towards * distance;
let radius = distance * 0.16;
wires.ring(at, across * radius, down * radius, RING_SIDES);
for i in 0..RAYS {
let angle = i as f32 / RAYS as f32 * std::f32::consts::TAU;
let out = across * angle.cos() + down * angle.sin();
wires.line(at + out * radius * RAY_INNER, at + out * radius * RAY_OUTER);
}
wires.dashed(at - towards * radius * RAY_INNER, focus, 6);
wires
}
pub fn lights(suns: &Suns, focus: Vec3, distance: f32) -> Vec<Wires> {
[suns.shadowed, suns.unshadowed]
.into_iter()
.flatten()
.map(|sun| light(&sun, focus, distance))
.collect()
}
pub fn local_light(light: &LocalLight) -> Wires {
let tint = light.color();
let tint = tint / tint.max_element().max(1.0e-4);
let mut wires = Wires::new(Color::linear_rgba(tint.x, tint.y, tint.z, 1.0));
let at = light.position;
const RADIUS: f32 = 0.22;
let towards = match light.shape {
LightShape::Point => Vec3::NEG_Y,
LightShape::Spot { direction, .. } => direction.normalize_or(Vec3::NEG_Y),
};
let any = match towards.y.abs() > 0.99 {
true => Vec3::Z,
false => Vec3::Y,
};
let across = towards.cross(any).normalize_or(Vec3::X);
let down = towards.cross(across).normalize_or(Vec3::Z);
match light.shape {
LightShape::Point => {
wires.ring(at, across * RADIUS, down * RADIUS, RING_SIDES);
wires.ring(at, across * RADIUS, towards * RADIUS, RING_SIDES);
wires.ring(at, down * RADIUS, towards * RADIUS, RING_SIDES);
}
LightShape::Spot { .. } => {
wires.ring(at, across * RADIUS, down * RADIUS, RING_SIDES);
wires.dashed(at + towards * RADIUS, at + towards * RADIUS * 5.0, 4);
}
}
for i in 0..RAYS {
let angle = i as f32 / RAYS as f32 * std::f32::consts::TAU;
let out = across * angle.cos() + down * angle.sin();
wires.line(at + out * RADIUS * RAY_INNER, at + out * RADIUS * RAY_OUTER);
}
wires
}
#[derive(Resource, Default)]
pub struct WireDrawList(pub Vec<WireVertex>);
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct WireVertex {
position: [f32; 3],
color: [f32; 4],
}
pub fn collect_wires_system(mut list: ResMut<WireDrawList>, wires: Query<&Wires>) {
list.0.clear();
for drawing in &wires {
let color = linear_rgba(drawing.color);
for [from, to] in &drawing.segments {
list.0.push(WireVertex {
position: from.to_array(),
color,
});
list.0.push(WireVertex {
position: to.to_array(),
color,
});
}
}
}
#[derive(Resource, Clone, Copy, Default, Debug)]
pub struct ActiveGrid(pub Option<Grid>);
pub fn collect_grid_system(mut active: ResMut<ActiveGrid>, grids: Query<&Grid>) {
active.0 = grids.iter().next().copied();
}
pub struct WireRenderer {
pipeline: wgpu::RenderPipeline,
uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
vertices: wgpu::Buffer,
capacity: usize,
}
const WIRE_CAPACITY: usize = 1024;
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct WireUniform {
view_proj: [f32; 16],
}
impl WireRenderer {
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("wire shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("wire.wgsl").into()),
});
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("wire uniform"),
size: std::mem::size_of::<WireUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("wire bind group layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("wire bind group"),
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("wire pipeline layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
const ATTRS: [wgpu::VertexAttribute; 2] =
wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x4];
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("wire pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<WireVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &ATTRS,
})],
compilation_options: wgpu::PipelineCompilationOptions::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: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::LineList,
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: DEPTH_FORMAT,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: crate::render3d::SAMPLES,
..wgpu::MultisampleState::default()
},
multiview_mask: None,
cache: None,
});
let vertices = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("wire vertices"),
size: (WIRE_CAPACITY * std::mem::size_of::<WireVertex>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self {
pipeline,
uniform_buffer,
bind_group,
vertices,
capacity: WIRE_CAPACITY,
}
}
#[allow(clippy::too_many_arguments)]
pub fn render(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
view: &wgpu::TextureView,
resolve: &wgpu::TextureView,
depth: &wgpu::TextureView,
width: u32,
height: u32,
scene: &crate::views::View,
lines: &[WireVertex],
) {
if lines.is_empty() {
return;
}
if lines.len() > self.capacity {
self.capacity = lines.len().next_power_of_two();
self.vertices = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("wire vertices"),
size: (self.capacity * std::mem::size_of::<WireVertex>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
}
let uniform = WireUniform {
view_proj: scene
.camera
.view_proj(scene.aspect())
.to_cols_array(),
};
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
queue.write_buffer(&self.vertices, 0, bytemuck::cast_slice(lines));
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("wire render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: Some(resolve),
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth,
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,
});
crate::render3d::set_view(&mut pass, scene, (width, height));
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
pass.set_vertex_buffer(0, self.vertices.slice(..));
pass.draw(0..lines.len() as u32, 0..1);
}
}
pub struct GizmosPlugin;
impl Plugin for GizmosPlugin {
fn build(&self, app: &mut Application) {
app.insert_resource(ActiveGrid::default());
app.insert_resource(WireDrawList::default());
app.add_update_systems((collect_grid_system, collect_wires_system));
}
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct GridUniform {
view_proj: [f32; 16],
inv_view_proj: [f32; 16],
eye: [f32; 4],
params: [f32; 4],
line: [f32; 4],
major: [f32; 4],
x_axis: [f32; 4],
z_axis: [f32; 4],
}
pub struct GridRenderer {
pipeline: wgpu::RenderPipeline,
uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
}
impl GridRenderer {
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("grid shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("grid.wgsl").into()),
});
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("grid uniform"),
size: std::mem::size_of::<GridUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("grid bind group layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("grid bind group"),
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("grid pipeline layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("grid pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::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: wgpu::PipelineCompilationOptions::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: DEPTH_FORMAT,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: crate::render3d::SAMPLES,
..wgpu::MultisampleState::default()
},
multiview_mask: None,
cache: None,
});
Self {
pipeline,
uniform_buffer,
bind_group,
}
}
#[allow(clippy::too_many_arguments)]
pub fn render(
&mut self,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
view: &wgpu::TextureView,
resolve: &wgpu::TextureView,
depth: &wgpu::TextureView,
width: u32,
height: u32,
scene: &crate::views::View,
grid: &Grid,
) {
let view_proj = scene.camera.view_proj(scene.aspect());
let uniform = GridUniform {
view_proj: view_proj.to_cols_array(),
inv_view_proj: view_proj.inverse().to_cols_array(),
eye: [
scene.camera.eye.x,
scene.camera.eye.y,
scene.camera.eye.z,
grid.height,
],
params: [
grid.spacing.max(1e-4),
grid.major_every.max(1.0),
grid.fade_from,
grid.fade_to.max(grid.fade_from + 1e-3),
],
line: linear_rgba(grid.line),
major: linear_rgba(grid.major_line),
x_axis: linear_rgba(grid.x_axis),
z_axis: linear_rgba(grid.z_axis),
};
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("grid render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: Some(resolve),
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth,
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,
});
crate::render3d::set_view(&mut pass, scene, (width, height));
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
pass.draw(0..3, 0..1);
}
}
const _: () = assert!(std::mem::size_of::<GridUniform>() % 16 == 0);