Add sample rendering #2

Open
noteuclid wants to merge 5 commits from sample into master
13 changed files with 620 additions and 20 deletions

8
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -4,6 +4,8 @@ This example combines Qt6 GUI (C++) with WGPU rendering (Rust). It doesnt 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.

View File

@ -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/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s:[\\/&]:\\&:g')

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

78
src/camera.rs Normal file
View File

@ -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!();
}
}
}

View File

@ -1,20 +1,28 @@
use std::error::Error;
use glam::{UVec2, Vec4, vec4};
use glam::{Mat4, UVec2, Vec3, Vec4, vec3, vec4};
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth16Unorm;
const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb;
mod camera;
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),
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.,
cam_fovy: std::f32::consts::FRAC_PI_2,
}
}
}
@ -30,9 +38,64 @@ pub struct Core {
queue: wgpu::Queue,
surface: wgpu::Surface<'static>,
mesh_pipeline: render::mesh::Pipeline,
mesh_color: render::mesh::GpuMeshIndexed,
mesh_gray: render::mesh::GpuMeshIndexed,
depth: wgpu::Texture,
}
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 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: gray + color_level * m * w,
normal: m * w,
};
mesh.vertices.push(vertex);
}
mesh.faces.push([base, base + 1, base + 2]);
mesh.faces.push([base, base + 2, base + 3]);
}
}
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 {
@ -42,7 +105,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 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);
@ -50,6 +115,9 @@ impl Core {
device,
queue,
surface,
mesh_pipeline,
mesh_color,
mesh_gray,
depth,
}
}
@ -61,6 +129,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(0.7, 0.5, 1.0).normalize(),
_zero: 0.,
},
);
self.queue.submit([]); // flush buffer updates
let view = output.create_view(&wgpu::TextureViewDescriptor::default());
@ -76,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,
},
})],
@ -95,7 +172,7 @@ impl Core {
}),
..Default::default()
});
self.mesh_gray.render(&self.mesh_pipeline, &mut pass);
drop(pass);
self.queue.submit(std::iter::once(encoder.finish()));
}
@ -111,7 +188,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: &[],
})
@ -122,7 +199,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,

209
src/render/mesh.rs Normal file
View File

@ -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<Vertex>,
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::<Params>() 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::<Vertex>() 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(&params));
}
}
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);
}
}

36
src/render/mesh.wgsl Normal file
View File

@ -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<uniform> 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;
}

4
src/render/mod.rs Normal file
View File

@ -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;

View File

@ -13,6 +13,10 @@ struct Core;
struct RedrawArgs {
Vec4 background;
float cam_yaw;
float cam_pitch;
float cam_distance;
float cam_fovy;
};
} // namespace ffi

View File

@ -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);
}

View File

@ -40,6 +40,122 @@
</attribute>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Camera</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Yaw</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="camYaw">
<property name="suffix">
<string> °</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>-180.000000000000000</double>
</property>
<property name="maximum">
<double>180.000000000000000</double>
</property>
<property name="singleStep">
<double>5.000000000000000</double>
</property>
<property name="value">
<double>45.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Pitch</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="camPitch">
<property name="suffix">
<string> °</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>-90.000000000000000</double>
</property>
<property name="maximum">
<double>90.000000000000000</double>
</property>
<property name="singleStep">
<double>5.000000000000000</double>
</property>
<property name="value">
<double>30.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Distance</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="camDistance">
<property name="maximum">
<double>10.000000000000000</double>
</property>
<property name="stepType">
<enum>QAbstractSpinBox::StepType::AdaptiveDecimalStepType</enum>
</property>
<property name="value">
<double>4.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Field of view</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="camFoV">
<property name="suffix">
<string> °</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>15.000000000000000</double>
</property>
<property name="maximum">
<double>150.000000000000000</double>
</property>
<property name="singleStep">
<double>5.000000000000000</double>
</property>
<property name="value">
<double>90.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
@ -51,9 +167,9 @@
<widget class="KColorCombo" name="inBackground">
<property name="color">
<color>
<red>25</red>
<green>220</green>
<blue>0</blue>
<red>0</red>
<green>99</green>
<blue>133</blue>
</color>
</property>
</widget>
@ -106,6 +222,70 @@
</hint>
</hints>
</connection>
<connection>
<sender>camYaw</sender>
<signal>valueChanged(double)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1507</x>
<y>136</y>
</hint>
<hint type="destinationlabel">
<x>1475</x>
<y>183</y>
</hint>
</hints>
</connection>
<connection>
<sender>camPitch</sender>
<signal>valueChanged(double)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1507</x>
<y>214</y>
</hint>
<hint type="destinationlabel">
<x>1479</x>
<y>280</y>
</hint>
</hints>
</connection>
<connection>
<sender>camDistance</sender>
<signal>valueChanged(double)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1536</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>1478</x>
<y>345</y>
</hint>
</hints>
</connection>
<connection>
<sender>camFoV</sender>
<signal>valueChanged(double)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1541</x>
<y>325</y>
</hint>
<hint type="destinationlabel">
<x>1476</x>
<y>432</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>updateView()</slot>