From 759b6a4211a0344787fb82cbaa48c435baf76cfe Mon Sep 17 00:00:00 2001 From: numzero Date: Tue, 4 Aug 2026 23:21:11 +0300 Subject: [PATCH 1/5] add camera calculations --- src/camera.rs | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 ++ 2 files changed, 80 insertions(+) create mode 100644 src/camera.rs diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..f54059b --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,78 @@ +use glam::{Affine3A, Mat3, Mat4, vec3}; + +pub(crate) fn rotation(yaw: f32, pitch: f32) -> Mat3 { + let (ys, yc) = yaw.sin_cos(); + let (ps, pc) = pitch.sin_cos(); + Mat3::from_cols_array_2d(&[[0., 0., -1.], [-1., 0., 0.], [0., 1., 0.]]) + * Mat3::from_cols_array_2d(&[[pc, 0., -ps], [0., 1., 0.], [ps, 0., pc]]) + * Mat3::from_cols_array_2d(&[[yc, -ys, 0.], [ys, yc, 0.], [0., 0., 1.]]) +} + +pub(crate) fn view(yaw: f32, pitch: f32, distance: f32) -> Affine3A { + Affine3A::from_mat3_translation(rotation(yaw, pitch), vec3(0., 0., distance)) +} + +pub(crate) fn projection(fovy: f32, aspect: f32) -> Mat4 { + Mat4::perspective_lh(fovy, aspect, 0.1, 10.0) +} + +#[cfg(test)] +mod tests { + use super::*; + + use glam::Vec3; + use std::f32::consts::PI; + + #[test] + fn test_rotation() { + let mut ok = true; + for (yaw, pitch, x, y, z) in [ + (0.0, 0.0, [0., 0., -1.], [-1., 0., 0.], [0., 1., 0.]), + (0.5, 0.0, [1., 0., 0.], [0., 0., -1.], [0., 1., 0.]), + (0.0, 0.5, [0., -1., 0.], [-1., 0., 0.], [0., 0., -1.]), + (0.5, 0.5, [1., 0., 0.], [0., -1., 0.], [0., 0., -1.]), + ] { + let m = rotation(yaw * PI, pitch * PI); + if !m.abs_diff_eq(Mat3::from_cols_array_2d(&[x, y, z]), 1e-3) { + ok = false; + println!("Wrong rotation for {}, {}", 180. * yaw, 180. * pitch); + println!("X: expected: {}, actual: {}", Vec3::from_array(x), m.x_axis); + println!("Y: expected: {}, actual: {}", Vec3::from_array(y), m.y_axis); + println!("Z: expected: {}, actual: {}", Vec3::from_array(z), m.z_axis); + } + } + if !ok { + panic!(); + } + } + + #[test] + fn test_view() { + let mut ok = true; + for (yaw, pitch, x1, y1, z1) in [ + (0.0, 0.0, [0., 0., 2.], [-1., 0., 3.], [0., 1., 3.]), + (0.5, 0.0, [1., 0., 3.], [0., 0., 2.], [0., 1., 3.]), + (0.0, 0.5, [0., -1., 3.], [-1., 0., 3.], [0., 0., 2.]), + (0.5, 0.5, [1., 0., 3.], [0., -1., 3.], [0., 0., 2.]), + ] { + let m = view(yaw * PI, pitch * PI, 3.); + for (label, world, expected) in + [('X', Vec3::X, x1), ('Y', Vec3::Y, y1), ('Z', Vec3::Z, z1)] + { + let expected = Vec3::from_array(expected); + let actual = m.transform_point3(world); + if !actual.abs_diff_eq(expected, 1e-3) { + ok = false; + println!( + "Wrong transform for {}, {}: for {label}, expected: {expected}, got: {actual}", + 180. * yaw, + 180. * pitch + ); + } + } + } + if !ok { + panic!(); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 294edfd..33f8d16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,8 @@ use std::error::Error; use glam::{UVec2, Vec4, vec4}; +mod camera; + const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth16Unorm; const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; -- 2.47.3 From 586d78b3b84a90dd54aeac953e999aefd1e11447 Mon Sep 17 00:00:00 2001 From: numzero Date: Wed, 5 Aug 2026 00:32:12 +0300 Subject: [PATCH 2/5] 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() -- 2.47.3 From 2bb28c240b42355eb6acc108fb4191bd5a3382f9 Mon Sep 17 00:00:00 2001 From: numzero Date: Wed, 5 Aug 2026 00:38:52 +0300 Subject: [PATCH 3/5] gray cube --- src/lib.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3d75395..1dc927b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,23 +39,27 @@ pub struct Core { surface: wgpu::Surface<'static>, mesh_pipeline: render::mesh::Pipeline, - mesh1: render::mesh::GpuMeshIndexed, + mesh_color: render::mesh::GpuMeshIndexed, + mesh_gray: render::mesh::GpuMeshIndexed, depth: wgpu::Texture, } -fn cube() -> render::mesh::CpuMeshIndexed { +fn cube(a: f32, b: f32) -> render::mesh::CpuMeshIndexed { + let gray_level = (b + a) / 2.; + let gray = Vec3::splat(gray_level); + let color_level = (b - a) / 2.; 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)] { + for m in [1., -1.] { 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, + color: gray + color_level * m * w, normal: m * w, }; mesh.vertices.push(vertex); @@ -77,8 +81,8 @@ impl Core { let depth = Self::create_depth_buffer(&device, pixel_size); let mesh_pipeline = render::mesh::Pipeline::new(&device); - let mesh1 = cube(); - let mesh1 = render::mesh::GpuMeshIndexed::new(&device, &mesh1); + let mesh_color = render::mesh::GpuMeshIndexed::new(&device, &cube(0.2, 0.8)); + let mesh_gray = render::mesh::GpuMeshIndexed::new(&device, &cube(0.7, 0.7)); Self::configure_surface(&surface, &device, pixel_size); @@ -87,7 +91,8 @@ impl Core { queue, surface, mesh_pipeline, - mesh1, + mesh_color, + mesh_gray, depth, } } @@ -109,7 +114,7 @@ impl Core { &self.queue, render::mesh::Params { mvp: proj * view, - light: vec3(1., 0.5, 0.5).normalize(), + light: vec3(0.7, 0.5, 1.0).normalize(), _zero: 0., }, ); @@ -147,7 +152,7 @@ impl Core { }), ..Default::default() }); - self.mesh1.render(&self.mesh_pipeline, &mut pass); + self.mesh_gray.render(&self.mesh_pipeline, &mut pass); drop(pass); self.queue.submit(std::iter::once(encoder.finish())); } -- 2.47.3 From 02707dd095d725f9703fed8e3af4d687cc48a0dd Mon Sep 17 00:00:00 2001 From: numzero Date: Wed, 5 Aug 2026 00:42:30 +0300 Subject: [PATCH 4/5] add a screenshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s not very fancy but shows how the thing looks like. And, it’s under 50 kB. --- README.md | 2 ++ init.sh | 1 + screenshot.png | Bin 0 -> 45753 bytes 3 files changed, 3 insertions(+) create mode 100644 screenshot.png diff --git a/README.md b/README.md index 5949dc3..c4821e5 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ This example combines Qt6 GUI (C++) with WGPU rendering (Rust). It doesn’t do Currently only X11 (XCB) is supported but it should be easy to extend to other platforms. +![Screenshot](screenshot.png) + ## Usage This program is intended as a starting point rather than a dependency, so just copy it. diff --git a/init.sh b/init.sh index 6fe9b04..a25cdcb 100755 --- a/init.sh +++ b/init.sh @@ -56,6 +56,7 @@ git rm -r skel-test # replace the README with a stub echo "# $name" >| README.md +git rm screenshot.png # escape HTML special characters (&, <, and >), but also sed special characters (& again, \, and /) htmlname=$(echo -n "$name" | sed 's/&/\&/g; s//\>/g; s:[\\/&]:\\&:g') diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..f47df3c1e41755e68ed8cc503f0362d6d874c529 GIT binary patch literal 45753 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 bk5-b>yE=ZnGwqm%8mb>Wtdh3h`11b(2G$B} literal 0 HcmV?d00001 -- 2.47.3 From 7b4ff1978429cf805495803a5bc5ec8dd42af20c Mon Sep 17 00:00:00 2001 From: numzero Date: Wed, 5 Aug 2026 01:13:38 +0300 Subject: [PATCH 5/5] fix linear-sRGB color mismatch --- src/lib.rs | 34 +++++++++++++++++++++++++++------- ui/src/main_window.ui | 4 ++-- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1dc927b..111ea41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ pub struct RedrawArgs { impl Default for RedrawArgs { fn default() -> Self { Self { - background: vec4(0.05, 0.20, 0.85, 1.00), + background: vec4(0.25, 0.49, 0.93, 1.00), cam_yaw: std::f32::consts::FRAC_PI_4, cam_pitch: std::f32::consts::FRAC_PI_6, cam_distance: 3., @@ -71,6 +71,31 @@ fn cube(a: f32, b: f32) -> render::mesh::CpuMeshIndexed { mesh } +fn srgb_to_linear(color: Vec3) -> Vec3 { + color.map(|x| { + if x > 0.04045 { + ((x + 0.055) / 1.055).powf(2.4) + } else { + x / 12.92 + } + }) +} + +fn srgba_to_linear(color: Vec4) -> Vec4 { + let alpha = color.w; + let color = srgb_to_linear(vec3(color.x, color.y, color.z)); + vec4(color.x, color.y, color.z, alpha) +} + +fn color_to_wgpu(color: Vec4) -> wgpu::Color { + wgpu::Color { + r: color.x.into(), + g: color.y.into(), + b: color.z.into(), + a: color.w.into(), + } +} + impl Core { pub fn new(gpu: Gpu, pixel_size: UVec2) -> Self { let Gpu { @@ -133,12 +158,7 @@ impl Core { depth_slice: None, resolve_target: None, ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: args.background.x.into(), - g: args.background.y.into(), - b: args.background.z.into(), - a: args.background.w.into(), - }), + load: wgpu::LoadOp::Clear(color_to_wgpu(srgba_to_linear(args.background))), store: wgpu::StoreOp::Store, }, })], diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index ee6bd40..71fced1 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -168,8 +168,8 @@ 0 - 32 - 60 + 99 + 133 -- 2.47.3