From 586d78b3b84a90dd54aeac953e999aefd1e11447 Mon Sep 17 00:00:00 2001 From: numzero Date: Wed, 5 Aug 2026 00:32:12 +0300 Subject: [PATCH] render a cube --- Cargo.lock | 8 +- Cargo.toml | 3 +- src/lib.rs | 66 +++++++++++-- src/render/mesh.rs | 209 +++++++++++++++++++++++++++++++++++++++++ src/render/mesh.wgsl | 36 +++++++ src/render/mod.rs | 4 + ui/src/api.hxx | 4 + ui/src/main_window.cxx | 4 + ui/src/main_window.ui | 186 +++++++++++++++++++++++++++++++++++- 9 files changed, 506 insertions(+), 14 deletions(-) create mode 100644 src/render/mesh.rs create mode 100644 src/render/mesh.wgsl create mode 100644 src/render/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 9db39c2..0c5bb18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,7 @@ version = 4 name = "PROJECT-NAME" version = "0.1.0" dependencies = [ + "bytemuck", "glam", "pollster", "wgpu", @@ -177,9 +178,9 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" dependencies = [ "bytemuck_derive", ] @@ -526,6 +527,9 @@ name = "glam" version = "0.30.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" +dependencies = [ + "bytemuck", +] [[package]] name = "glow" diff --git a/Cargo.toml b/Cargo.toml index 0d64950..c3bb8aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,8 @@ debug = false opt-level = 3 [dependencies] -glam = { version = "0.30.9" } +bytemuck = "1.25.2" +glam = { version = "0.30.9", features = ["bytemuck"] } pollster = "0.4.0" wgpu = "27.0.1" winit = "0.30.12" diff --git a/src/lib.rs b/src/lib.rs index 33f8d16..3d75395 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,22 +1,28 @@ use std::error::Error; -use glam::{UVec2, Vec4, vec4}; +use glam::{Mat4, UVec2, Vec3, Vec4, vec3, vec4}; mod camera; - -const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth16Unorm; -const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; +mod render; #[derive(Debug, Clone, Copy)] #[repr(C)] pub struct RedrawArgs { pub background: Vec4, + pub cam_yaw: f32, + pub cam_pitch: f32, + pub cam_distance: f32, + pub cam_fovy: f32, } impl Default for RedrawArgs { fn default() -> Self { Self { background: vec4(0.05, 0.20, 0.85, 1.00), + cam_yaw: std::f32::consts::FRAC_PI_4, + cam_pitch: std::f32::consts::FRAC_PI_6, + cam_distance: 3., + cam_fovy: std::f32::consts::FRAC_PI_2, } } } @@ -32,9 +38,35 @@ pub struct Core { queue: wgpu::Queue, surface: wgpu::Surface<'static>, + mesh_pipeline: render::mesh::Pipeline, + mesh1: render::mesh::GpuMeshIndexed, depth: wgpu::Texture, } +fn cube() -> render::mesh::CpuMeshIndexed { + let mut mesh = render::mesh::CpuMeshIndexed::default(); + for (u, v, w) in [ + (Vec3::X, Vec3::Y, Vec3::Z), + (Vec3::Y, Vec3::Z, Vec3::X), + (Vec3::Z, Vec3::X, Vec3::Y), + ] { + for (m, color) in [(1., w), (-1., 1. - w)] { + let base = mesh.vertices.len() as u16; + for offset in [m * v + u, -m * v + u, -m * v - u, m * v - u] { + let vertex = render::mesh::Vertex { + position: (m * w + offset), + color, + normal: m * w, + }; + mesh.vertices.push(vertex); + } + mesh.faces.push([base, base + 1, base + 2]); + mesh.faces.push([base, base + 2, base + 3]); + } + } + mesh +} + impl Core { pub fn new(gpu: Gpu, pixel_size: UVec2) -> Self { let Gpu { @@ -44,7 +76,9 @@ impl Core { } = gpu; let depth = Self::create_depth_buffer(&device, pixel_size); - queue.submit([]); // flush buffer updates + let mesh_pipeline = render::mesh::Pipeline::new(&device); + let mesh1 = cube(); + let mesh1 = render::mesh::GpuMeshIndexed::new(&device, &mesh1); Self::configure_surface(&surface, &device, pixel_size); @@ -52,6 +86,8 @@ impl Core { device, queue, surface, + mesh_pipeline, + mesh1, depth, } } @@ -63,6 +99,20 @@ impl Core { let h = size.height as f32; w / h }; + let view = Mat4::from(camera::view( + args.cam_yaw, + args.cam_pitch, + args.cam_distance, + )); + let proj = camera::projection(args.cam_fovy, aspect); + self.mesh_pipeline.set_params( + &self.queue, + render::mesh::Params { + mvp: proj * view, + light: vec3(1., 0.5, 0.5).normalize(), + _zero: 0., + }, + ); self.queue.submit([]); // flush buffer updates let view = output.create_view(&wgpu::TextureViewDescriptor::default()); @@ -97,7 +147,7 @@ impl Core { }), ..Default::default() }); - + self.mesh1.render(&self.mesh_pipeline, &mut pass); drop(pass); self.queue.submit(std::iter::once(encoder.finish())); } @@ -113,7 +163,7 @@ impl Core { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: DEPTH_FORMAT, + format: render::DEPTH_FORMAT, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, view_formats: &[], }) @@ -124,7 +174,7 @@ impl Core { device, &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST, - format: OUTPUT_FORMAT, + format: render::OUTPUT_FORMAT, width: pixel_size.x, height: pixel_size.y, present_mode: wgpu::PresentMode::Fifo, diff --git a/src/render/mesh.rs b/src/render/mesh.rs new file mode 100644 index 0000000..f1d0530 --- /dev/null +++ b/src/render/mesh.rs @@ -0,0 +1,209 @@ +use std::mem::offset_of; + +use bytemuck::{Pod, Zeroable, bytes_of, cast_slice}; +use glam::{Mat4, Vec3}; +use wgpu::util::DeviceExt; + +static SHADER: &str = include_str!("mesh.wgsl"); + +#[derive(Debug, Clone, Copy, Zeroable, Pod)] +#[repr(C)] +pub struct Vertex { + pub position: Vec3, + pub color: Vec3, + pub normal: Vec3, +} + +#[derive(Debug, Clone, Copy, Zeroable, Pod)] +#[repr(C)] +pub struct Params { + pub mvp: Mat4, + pub light: Vec3, + pub _zero: f32, +} + +#[derive(Debug, Clone, Default)] +pub struct CpuMesh { + pub faces: Vec<[Vertex; 3]>, +} + +#[derive(Debug, Clone, Default)] +pub struct CpuMeshIndexed { + pub vertices: Vec, + pub faces: Vec<[u16; 3]>, +} + +pub struct GpuMesh { + vertices: wgpu::Buffer, + vertex_count: u32, +} + +pub struct GpuMeshIndexed { + vertices: wgpu::Buffer, + indices: wgpu::Buffer, + index_count: u32, +} + +impl GpuMesh { + pub fn new(device: &wgpu::Device, data: &CpuMesh) -> Self { + let vertices = cast_slice::<_, Vertex>(&data.faces); + let vertex_count = vertices.len().try_into().expect("too many faces"); + let vertices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: None, + usage: wgpu::BufferUsages::VERTEX, + contents: cast_slice(&vertices), + }); + Self { + vertices, + vertex_count, + } + } +} + +impl GpuMeshIndexed { + pub fn new(device: &wgpu::Device, data: &CpuMeshIndexed) -> Self { + let vertex_count = data.vertices.len(); + if vertex_count >= 1 << 16 { + panic!("too many vertices"); + } + let indices = cast_slice::<_, u16>(&data.faces); + let index_count = indices.len().try_into().expect("too many faces"); + for index in indices { + if *index as usize >= vertex_count { + panic!("vertex index out of bounds: {index} out of {vertex_count}"); + } + } + let vertices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: None, + usage: wgpu::BufferUsages::VERTEX, + contents: cast_slice(&data.vertices), + }); + let indices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: None, + usage: wgpu::BufferUsages::INDEX, + contents: cast_slice(&indices), + }); + Self { + vertices, + indices, + index_count, + } + } +} + +pub struct Pipeline { + params: wgpu::Buffer, + bindings: wgpu::BindGroup, + pipeline: wgpu::RenderPipeline, +} + +impl Pipeline { + pub fn new(device: &wgpu::Device) -> Self { + let params = device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size: size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: None, + source: wgpu::ShaderSource::Wgsl(SHADER.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: None, + vertex: wgpu::VertexState { + module: &shader, + entry_point: None, + compilation_options: wgpu::PipelineCompilationOptions::default(), + buffers: &[wgpu::VertexBufferLayout { + array_stride: size_of::() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + shader_location: 0, + offset: offset_of!(Vertex, position) as u64, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + shader_location: 1, + offset: offset_of!(Vertex, color) as u64, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + shader_location: 2, + offset: offset_of!(Vertex, normal) as u64, + format: wgpu::VertexFormat::Float32x3, + }, + ], + }], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + cull_mode: Some(wgpu::Face::Back), + ..Default::default() + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: crate::render::DEPTH_FORMAT, + depth_write_enabled: true, + 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, + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: None, + compilation_options: wgpu::PipelineCompilationOptions::default(), + targets: &[Some(wgpu::ColorTargetState { + format: crate::render::OUTPUT_FORMAT, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + cache: None, + }); + let bindings = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &pipeline.get_bind_group_layout(0), + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: params.as_entire_binding(), + }], + }); + Self { + params, + bindings, + pipeline, + } + } + + pub fn set_params(&self, queue: &wgpu::Queue, params: Params) { + queue.write_buffer(&self.params, 0, bytes_of(¶ms)); + } +} + +impl GpuMesh { + pub fn render(&self, pipeline: &Pipeline, pass: &mut wgpu::RenderPass) { + pass.set_pipeline(&pipeline.pipeline); + pass.set_bind_group(0, &pipeline.bindings, &[]); + pass.set_vertex_buffer(0, self.vertices.slice(..)); + pass.draw(0..self.vertex_count, 0..1); + } +} + +impl GpuMeshIndexed { + pub fn render(&self, pipeline: &Pipeline, pass: &mut wgpu::RenderPass) { + pass.set_pipeline(&pipeline.pipeline); + pass.set_bind_group(0, &pipeline.bindings, &[]); + pass.set_vertex_buffer(0, self.vertices.slice(..)); + pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint16); + pass.draw_indexed(0..self.index_count, 0, 0..1); + } +} diff --git a/src/render/mesh.wgsl b/src/render/mesh.wgsl new file mode 100644 index 0000000..14b3acc --- /dev/null +++ b/src/render/mesh.wgsl @@ -0,0 +1,36 @@ +struct Params { + mvp: mat4x4f, + light: vec3f, + _zero: f32, +} + +struct Vertex { + @location(0) position: vec3f, + @location(1) color: vec3f, + @location(2) normal: vec3f, +} + +struct Varying { + @builtin(position) screen: vec4f, + @location(0) color: vec4f, + @location(1) normal: vec3f, +} + +@group(0) @binding(0) var params: Params; + +@vertex +fn on_vertex(in: Vertex) -> Varying { + let position = params.mvp * vec4f(in.position, 1.0); + let color = vec4f(in.color, 1.0); + let normal = in.normal; + return Varying(position, color, normal); +} + +@fragment +fn on_fragment(in: Varying) -> @location(0) vec4f { + let normal = normalize(in.normal); + let light = dot(params.light, normal); + let u = 0.5 + 0.5 * light; + let color = vec4f(u * in.color.xyz, in.color.w); + return color; +} diff --git a/src/render/mod.rs b/src/render/mod.rs new file mode 100644 index 0000000..92ed9e2 --- /dev/null +++ b/src/render/mod.rs @@ -0,0 +1,4 @@ +pub mod mesh; + +pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus; +pub const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; diff --git a/ui/src/api.hxx b/ui/src/api.hxx index bb946e9..c4cd232 100644 --- a/ui/src/api.hxx +++ b/ui/src/api.hxx @@ -13,6 +13,10 @@ struct Core; struct RedrawArgs { Vec4 background; + float cam_yaw; + float cam_pitch; + float cam_distance; + float cam_fovy; }; } // namespace ffi diff --git a/ui/src/main_window.cxx b/ui/src/main_window.cxx index 723dd57..5e6f71f 100644 --- a/ui/src/main_window.cxx +++ b/ui/src/main_window.cxx @@ -15,6 +15,10 @@ void PROJECTNAME::updateView() { const auto color = m_ui->inBackground->color(); RedrawArgs args{ .background = { color.redF(), color.greenF(), color.blueF(), 1.00 }, + .cam_yaw = qDegreesToRadians((float)m_ui->camYaw->value()), + .cam_pitch = qDegreesToRadians((float)m_ui->camPitch->value()), + .cam_distance = (float)m_ui->camDistance->value(), + .cam_fovy = qDegreesToRadians((float)m_ui->camFoV->value()), }; m_ui->viewport->setView(args); } diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index 36facd0..ee6bd40 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -40,6 +40,122 @@ + + + + Camera + + + + + + Yaw + + + + + + + ° + + + 1 + + + -180.000000000000000 + + + 180.000000000000000 + + + 5.000000000000000 + + + 45.000000000000000 + + + + + + + Pitch + + + + + + + ° + + + 1 + + + -90.000000000000000 + + + 90.000000000000000 + + + 5.000000000000000 + + + 30.000000000000000 + + + + + + + Distance + + + + + + + 10.000000000000000 + + + QAbstractSpinBox::StepType::AdaptiveDecimalStepType + + + 4.000000000000000 + + + + + + + Field of view + + + + + + + ° + + + 1 + + + 15.000000000000000 + + + 150.000000000000000 + + + 5.000000000000000 + + + 90.000000000000000 + + + + + + @@ -51,9 +167,9 @@ - 25 - 220 - 0 + 0 + 32 + 60 @@ -106,6 +222,70 @@ + + camYaw + valueChanged(double) + MainWindow + updateView() + + + 1507 + 136 + + + 1475 + 183 + + + + + camPitch + valueChanged(double) + MainWindow + updateView() + + + 1507 + 214 + + + 1479 + 280 + + + + + camDistance + valueChanged(double) + MainWindow + updateView() + + + 1536 + 260 + + + 1478 + 345 + + + + + camFoV + valueChanged(double) + MainWindow + updateView() + + + 1541 + 325 + + + 1476 + 432 + + + updateView()