From db18de85ad1d41bafde5db2aea61a8bb445a87a6 Mon Sep 17 00:00:00 2001 From: numzero Date: Mon, 20 Jul 2026 03:36:54 +0300 Subject: [PATCH] render the model --- Cargo.lock | 8 +- Cargo.toml | 3 +- src/camera.rs | 47 ++++++ src/lib.rs | 72 +++++++- src/mesh.wgsl | 27 +++ src/render.rs | 148 ++++++++++++++++ ui/src/api.hxx | 14 ++ ui/src/main_window.cxx | 14 +- ui/src/main_window.ui | 372 +++++++++++++++++++++++++++++++++++++++-- 9 files changed, 688 insertions(+), 17 deletions(-) create mode 100644 src/camera.rs create mode 100644 src/mesh.wgsl create mode 100644 src/render.rs diff --git a/Cargo.lock b/Cargo.lock index b7524ac..07cf9ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,9 +156,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", ] @@ -505,6 +505,9 @@ name = "glam" version = "0.30.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" +dependencies = [ + "bytemuck", +] [[package]] name = "glow" @@ -624,6 +627,7 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" name = "hyperboloid" version = "0.1.0" dependencies = [ + "bytemuck", "glam", "pollster", "wgpu", diff --git a/Cargo.toml b/Cargo.toml index c4bbdec..2bc6a2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,8 @@ debug = false opt-level = 3 [dependencies] -glam = { version = "0.30.9" } +bytemuck = { version = "1.25.2", features = ["derive"] } +glam = { version = "0.30.9", features = ["bytemuck"] } pollster = "0.4.0" wgpu = "27.0.1" winit = "0.30.12" diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..c55ae91 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,47 @@ +use glam::{Mat4, Vec3, vec3}; + +/// A camera always directed at the origin. +#[derive(Debug, Clone, Copy)] +pub struct OrbitalCamera { + /// Horizontal position (angle), in radians from +X towards +Y. + pub position_yaw: f32, + + /// Vertical position (angle), in radians from XY plane towards +Z. + pub position_pitch: f32, + + /// Distance from the origin. + pub distance: f32, +} + +impl OrbitalCamera { + pub fn position(&self) -> Vec3 { + let (y, x) = self.position_yaw.sin_cos(); + let (z, xy) = self.position_pitch.sin_cos(); + self.distance * vec3(xy * x, xy * y, z) + } + + pub fn direction(&self) -> Vec3 { + let (y, x) = self.position_yaw.sin_cos(); + let (z, xy) = self.position_pitch.sin_cos(); + -vec3(xy * x, xy * y, z) + } + + pub fn transform(&self) -> Mat4 { + // for yaw=0, pitch=0: + // X -> -Z + // Y -> -X + // Z -> Y + Mat4::from_translation(vec3(0., 0., self.distance)) + * Mat4::from_cols_array_2d(&[ + [0., 0., -1., 0.], + [-1., 0., 0., 0.], + [0., 1., 0., 0.], + [0., 0., 0., 1.], + ]) * Mat4::from_euler( + glam::EulerRot::ZYZ, + 0., + self.position_pitch, + -self.position_yaw, + ) + } +} diff --git a/src/lib.rs b/src/lib.rs index 7fdbe13..e43021b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,22 +1,56 @@ use std::error::Error; -use glam::{UVec2, Vec4, vec4}; +use glam::{Mat4, UVec2, Vec4, vec4}; +mod camera; mod mesh; +mod render; const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth16Unorm; const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct ShapeArgs { + pub sides: u32, + pub layers_up: u32, + pub layers_down: u32, + pub base_diameter: f32, + pub height: f32, +} + +impl Default for ShapeArgs { + fn default() -> Self { + Self { + sides: 24, + layers_up: 6, + layers_down: 6, + base_diameter: 36.0, + height: 120.0, + } + } +} + #[derive(Debug, Clone, Copy)] #[repr(C)] pub struct RedrawArgs { + pub shape: ShapeArgs, pub background: Vec4, + pub camera_yaw: f32, + pub camera_pitch: f32, + pub light_yaw: f32, + pub light_pitch: f32, } impl Default for RedrawArgs { fn default() -> Self { Self { + shape: ShapeArgs::default(), background: vec4(0.05, 0.20, 0.85, 1.00), + camera_yaw: std::f32::consts::FRAC_PI_2, + camera_pitch: std::f32::consts::FRAC_PI_3, + light_yaw: 0., + light_pitch: 2. * std::f32::consts::FRAC_PI_3, } } } @@ -33,6 +67,8 @@ pub struct Core { surface: wgpu::Surface<'static>, depth: wgpu::Texture, + + pipeline: render::Pipeline, } impl Core { @@ -48,21 +84,40 @@ impl Core { Self::configure_surface(&surface, &device, pixel_size); + let pipeline = render::Pipeline::new(&device); + Self { device, queue, surface, depth, + pipeline, } } fn render(&self, output: &wgpu::Texture, args: &RedrawArgs) { + let mesh = mesh::generate(); + let mesh = faceted_mesh(&mesh); + let mesh = render::Mesh::new(&self.device, &mesh); + let aspect = { let size = output.size(); let w = size.width as f32; let h = size.height as f32; w / h }; + let camera = camera::OrbitalCamera { + position_yaw: args.camera_yaw, + position_pitch: args.camera_pitch, + distance: 5. * args.shape.base_diameter, + }; + let perspective = Mat4::perspective_lh(std::f32::consts::FRAC_PI_3, aspect, 1e1, 1e3); + self.pipeline.set_look( + &self.queue, + render::LookParams { + m: perspective * camera.transform(), + }, + ); self.queue.submit([]); // flush buffer updates let view = output.create_view(&wgpu::TextureViewDescriptor::default()); @@ -98,6 +153,8 @@ impl Core { ..Default::default() }); + self.pipeline.render(&mut pass, [&mesh]); + drop(pass); self.queue.submit(std::iter::once(encoder.finish())); } @@ -177,3 +234,16 @@ pub async fn init_gpu_inner( surface, }) } + +fn faceted_mesh(mesh: &mesh::Mesh) -> Vec { + let mut ret = Vec::new(); + for face in &mesh.faces { + let face = face.map(|index| mesh.vertices[index]); + let u = face[1] - face[0]; + let v = face[2] - face[0]; + let normal = u.cross(v).normalize(); + let face = face.map(|position| render::Vertex { position, normal }); + ret.extend(face); + } + ret +} diff --git a/src/mesh.wgsl b/src/mesh.wgsl new file mode 100644 index 0000000..be4575f --- /dev/null +++ b/src/mesh.wgsl @@ -0,0 +1,27 @@ +struct LookParams { + m: mat4x4f, +} + +struct Vertex { + @location(0) position: vec3f, + @location(1) normal: vec3f, +} + +struct Varying { + @builtin(position) screen: vec4f, + @location(0) normal: vec3f, +} + +@group(0) @binding(0) var look: LookParams; + +@vertex +fn on_vertex(in: Vertex) -> Varying { + let pos = look.m * vec4f(in.position, 1.0); + let normal = in.normal; + return Varying(pos, normal); +} + +@fragment +fn on_fragment(in: Varying) -> @location(0) vec4f { + return vec4f(0.5 + 0.5 * in.normal, 1.0); +} diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..0b6228c --- /dev/null +++ b/src/render.rs @@ -0,0 +1,148 @@ +use std::mem::offset_of; + +use bytemuck::{Pod, Zeroable, bytes_of, cast_slice}; +use glam::{Mat4, Vec3}; +use wgpu::util::DeviceExt as _; + +use crate::{DEPTH_FORMAT, OUTPUT_FORMAT}; + +static MESH_SHADER: &str = include_str!("mesh.wgsl"); + +#[derive(Debug, Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct LookParams { + pub m: Mat4, +} + +#[derive(Debug, Clone, Copy, Zeroable, Pod)] +#[repr(C)] +pub struct Vertex { + pub position: Vec3, + pub normal: Vec3, +} + +pub struct Mesh { + vertex_buffer: wgpu::Buffer, + vertex_count: u32, +} + +impl Mesh { + pub fn new(device: &wgpu::Device, vertices: &[Vertex]) -> Self { + let vertex_count = vertices.len().try_into().expect("too many vertices"); + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: None, + usage: wgpu::BufferUsages::VERTEX, + contents: cast_slice(vertices), + }); + Self { + vertex_buffer, + vertex_count, + } + } +} + +pub struct Pipeline { + look_buf: wgpu::Buffer, + bindings: wgpu::BindGroup, + pipeline: wgpu::RenderPipeline, +} + +impl Pipeline { + pub fn new(device: &wgpu::Device) -> Self { + let look_buf = 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(MESH_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, normal) as u64, + format: wgpu::VertexFormat::Float32x3, + }, + ], + }], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + // cull_mode: Some(wgpu::Face::Front), + ..Default::default() + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: 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: 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: look_buf.as_entire_binding(), + }], + }); + Self { + look_buf, + bindings, + pipeline, + } + } + + pub fn set_look(&self, queue: &wgpu::Queue, look: LookParams) { + queue.write_buffer(&self.look_buf, 0, bytes_of(&look)); + } + + pub fn render<'a>( + &self, + pass: &mut wgpu::RenderPass, + meshes: impl IntoIterator, + ) { + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.bindings, &[]); + for mesh in meshes { + pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); + pass.draw(0..mesh.vertex_count, 0..1); + } + } +} diff --git a/ui/src/api.hxx b/ui/src/api.hxx index bb946e9..74db20b 100644 --- a/ui/src/api.hxx +++ b/ui/src/api.hxx @@ -11,11 +11,25 @@ struct alignas(16) Vec4 { namespace ffi { struct Core; +struct ShapeArgs { + std::uint32_t sides; + std::uint32_t layers_up; + std::uint32_t layers_down; + float base_diameter; + float height; +}; + struct RedrawArgs { + ShapeArgs shape; Vec4 background; + float camera_yaw; + float camera_pitch; + float light_yaw; + float light_pitch; }; } // namespace ffi +using ffi::ShapeArgs; using ffi::RedrawArgs; class MutCore { diff --git a/ui/src/main_window.cxx b/ui/src/main_window.cxx index 8c76f15..0bc77f0 100644 --- a/ui/src/main_window.cxx +++ b/ui/src/main_window.cxx @@ -13,8 +13,20 @@ Hyperboloid::~Hyperboloid() = default; void Hyperboloid::updateView() { const auto color = m_ui->inBackground->color(); - RedrawArgs args{ + const ShapeArgs shape { + .sides = (std::uint32_t)m_ui->sides->value(), + .layers_up = (std::uint32_t)m_ui->layersUp->value(), + .layers_down = (std::uint32_t)m_ui->layersDown->value(), + .base_diameter = (float)m_ui->dia->value(), + .height = (float)m_ui->height->value(), + }; + const RedrawArgs args{ + .shape = shape, .background = { color.redF(), color.greenF(), color.blueF(), 1.00 }, + .camera_yaw = qDegreesToRadians((float)m_ui->camYaw->value()), + .camera_pitch = qDegreesToRadians((float)m_ui->camPitch->value()), + .light_yaw = 0.0, + .light_pitch = qDegreesToRadians((float)m_ui->lightPitch->value()), }; m_ui->viewport->setView(args); } diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index 71e000c..c61cce4 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -41,21 +41,241 @@ - - - Background color + + + Camera + + + + + Yaw + + + + + + + -180 + + + 180 + + + 15 + + + 45 + + + Qt::Horizontal + + + + + + + Pitch + + + + + + + -90 + + + 90 + + + 15 + + + 30 + + + Qt::Horizontal + + + + - - - - 25 - 220 - 0 - + + + Light + + + + + Pitch + + + + + + + -90 + + + 90 + + + 15 + + + 60 + + + Qt::Horizontal + + + + + + + + + + Shape + + + + + + Sides + + + + + + + 3 + + + 120 + + + 24 + + + + + + + Layers up + + + + + + + 11 + + + + + + + Layers down + + + + + + + 11 + + + + + + + + + + Size + + + + + + Base diameter + + + + + + + mm + + + 4.000000000000000 + + + 100.000000000000000 + + + QAbstractSpinBox::AdaptiveDecimalStepType + + + 36.000000000000000 + + + + + + + Height + + + + + + + mm + + + 4.000000000000000 + + + 500.000000000000000 + + + 120.000000000000000 + + + + + + + + + + Appearance + + + + + + Background color + + + + + + + + 85 + 170 + 255 + + + + + @@ -97,8 +317,8 @@ updateView() - 1531 - 115 + 1585 + 803 799 @@ -106,6 +326,134 @@ + + camYaw + valueChanged(int) + MainWindow + updateView() + + + 1520 + 142 + + + 1449 + 133 + + + + + camPitch + valueChanged(int) + MainWindow + updateView() + + + 1550 + 192 + + + 1447 + 198 + + + + + lightPitch + valueChanged(int) + MainWindow + updateView() + + + 1565 + 286 + + + 1445 + 265 + + + + + sides + valueChanged(int) + MainWindow + updateView() + + + 1521 + 370 + + + 1448 + 378 + + + + + layersUp + valueChanged(int) + MainWindow + updateView() + + + 1490 + 436 + + + 1446 + 441 + + + + + layersDown + valueChanged(int) + MainWindow + updateView() + + + 1478 + 513 + + + 1446 + 515 + + + + + dia + valueChanged(double) + MainWindow + updateView() + + + 1472 + 611 + + + 1446 + 629 + + + + + height + valueChanged(double) + MainWindow + updateView() + + + 1476 + 686 + + + 1446 + 686 + + + updateView()