Compare commits

..

5 Commits

Author SHA1 Message Date
7b4ff19784 fix linear-sRGB color mismatch 2026-08-05 01:13:38 +03:00
02707dd095 add a screenshot
It’s not very fancy but shows how the thing looks like. And, it’s under 50 kB.
2026-08-05 00:46:46 +03:00
2bb28c240b gray cube 2026-08-05 00:46:46 +03:00
586d78b3b8 render a cube 2026-08-05 00:46:46 +03:00
759b6a4211 add camera calculations 2026-08-04 23:21:11 +03:00
13 changed files with 620 additions and 20 deletions

8
Cargo.lock generated
View File

@ -6,6 +6,7 @@ version = 4
name = "PROJECT-NAME" name = "PROJECT-NAME"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bytemuck",
"glam", "glam",
"pollster", "pollster",
"wgpu", "wgpu",
@ -177,9 +178,9 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.24.0" version = "1.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
dependencies = [ dependencies = [
"bytemuck_derive", "bytemuck_derive",
] ]
@ -526,6 +527,9 @@ name = "glam"
version = "0.30.10" version = "0.30.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "glow" name = "glow"

View File

@ -18,7 +18,8 @@ debug = false
opt-level = 3 opt-level = 3
[dependencies] [dependencies]
glam = { version = "0.30.9" } bytemuck = "1.25.2"
glam = { version = "0.30.9", features = ["bytemuck"] }
pollster = "0.4.0" pollster = "0.4.0"
wgpu = "27.0.1" wgpu = "27.0.1"
winit = "0.30.12" 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. Currently only X11 (XCB) is supported but it should be easy to extend to other platforms.
![Screenshot](screenshot.png)
## Usage ## Usage
This program is intended as a starting point rather than a dependency, so just copy it. 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 # replace the README with a stub
echo "# $name" >| README.md echo "# $name" >| README.md
git rm screenshot.png
# escape HTML special characters (&, <, and >), but also sed special characters (& again, \, and /) # 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') 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 std::error::Error;
use glam::{UVec2, Vec4, vec4}; use glam::{Mat4, UVec2, Vec3, Vec4, vec3, vec4};
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth16Unorm; mod camera;
const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; mod render;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
#[repr(C)] #[repr(C)]
pub struct RedrawArgs { pub struct RedrawArgs {
pub background: Vec4, pub background: Vec4,
pub cam_yaw: f32,
pub cam_pitch: f32,
pub cam_distance: f32,
pub cam_fovy: f32,
} }
impl Default for RedrawArgs { impl Default for RedrawArgs {
fn default() -> Self { fn default() -> Self {
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, queue: wgpu::Queue,
surface: wgpu::Surface<'static>, surface: wgpu::Surface<'static>,
mesh_pipeline: render::mesh::Pipeline,
mesh_color: render::mesh::GpuMeshIndexed,
mesh_gray: render::mesh::GpuMeshIndexed,
depth: wgpu::Texture, 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 { impl Core {
pub fn new(gpu: Gpu, pixel_size: UVec2) -> Self { pub fn new(gpu: Gpu, pixel_size: UVec2) -> Self {
let Gpu { let Gpu {
@ -42,7 +105,9 @@ impl Core {
} = gpu; } = gpu;
let depth = Self::create_depth_buffer(&device, pixel_size); 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); Self::configure_surface(&surface, &device, pixel_size);
@ -50,6 +115,9 @@ impl Core {
device, device,
queue, queue,
surface, surface,
mesh_pipeline,
mesh_color,
mesh_gray,
depth, depth,
} }
} }
@ -61,6 +129,20 @@ impl Core {
let h = size.height as f32; let h = size.height as f32;
w / h 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 self.queue.submit([]); // flush buffer updates
let view = output.create_view(&wgpu::TextureViewDescriptor::default()); let view = output.create_view(&wgpu::TextureViewDescriptor::default());
@ -76,12 +158,7 @@ impl Core {
depth_slice: None, depth_slice: None,
resolve_target: None, resolve_target: None,
ops: wgpu::Operations { ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { load: wgpu::LoadOp::Clear(color_to_wgpu(srgba_to_linear(args.background))),
r: args.background.x.into(),
g: args.background.y.into(),
b: args.background.z.into(),
a: args.background.w.into(),
}),
store: wgpu::StoreOp::Store, store: wgpu::StoreOp::Store,
}, },
})], })],
@ -95,7 +172,7 @@ impl Core {
}), }),
..Default::default() ..Default::default()
}); });
self.mesh_gray.render(&self.mesh_pipeline, &mut pass);
drop(pass); drop(pass);
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
} }
@ -111,7 +188,7 @@ impl Core {
mip_level_count: 1, mip_level_count: 1,
sample_count: 1, sample_count: 1,
dimension: wgpu::TextureDimension::D2, dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT, format: render::DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT, usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[], view_formats: &[],
}) })
@ -122,7 +199,7 @@ impl Core {
device, device,
&wgpu::SurfaceConfiguration { &wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
format: OUTPUT_FORMAT, format: render::OUTPUT_FORMAT,
width: pixel_size.x, width: pixel_size.x,
height: pixel_size.y, height: pixel_size.y,
present_mode: wgpu::PresentMode::Fifo, 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 { struct RedrawArgs {
Vec4 background; Vec4 background;
float cam_yaw;
float cam_pitch;
float cam_distance;
float cam_fovy;
}; };
} // namespace ffi } // namespace ffi

View File

@ -15,6 +15,10 @@ void PROJECTNAME::updateView() {
const auto color = m_ui->inBackground->color(); const auto color = m_ui->inBackground->color();
RedrawArgs args{ RedrawArgs args{
.background = { color.redF(), color.greenF(), color.blueF(), 1.00 }, .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); m_ui->viewport->setView(args);
} }

View File

@ -40,6 +40,122 @@
</attribute> </attribute>
<widget class="QWidget" name="dockWidgetContents"> <widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout_2"> <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> <item>
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
@ -51,9 +167,9 @@
<widget class="KColorCombo" name="inBackground"> <widget class="KColorCombo" name="inBackground">
<property name="color"> <property name="color">
<color> <color>
<red>25</red> <red>0</red>
<green>220</green> <green>99</green>
<blue>0</blue> <blue>133</blue>
</color> </color>
</property> </property>
</widget> </widget>
@ -106,6 +222,70 @@
</hint> </hint>
</hints> </hints>
</connection> </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> </connections>
<slots> <slots>
<slot>updateView()</slot> <slot>updateView()</slot>