Compare commits

...

2 Commits

Author SHA1 Message Date
11ae302290 render a cube 2026-08-05 00:32:12 +03:00
759b6a4211 add camera calculations 2026-08-04 23:21:11 +03:00
10 changed files with 587 additions and 15 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"

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.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,
} }
} }
} }
@ -30,9 +38,35 @@ pub struct Core {
queue: wgpu::Queue, queue: wgpu::Queue,
surface: wgpu::Surface<'static>, surface: wgpu::Surface<'static>,
mesh_pipeline: render::mesh::Pipeline,
mesh1: render::mesh::GpuMeshIndexed,
depth: wgpu::Texture, 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 { 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 +76,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 mesh1 = cube();
let mesh1 = render::mesh::GpuMeshIndexed::new(&device, &mesh1);
Self::configure_surface(&surface, &device, pixel_size); Self::configure_surface(&surface, &device, pixel_size);
@ -50,6 +86,8 @@ impl Core {
device, device,
queue, queue,
surface, surface,
mesh_pipeline,
mesh1,
depth, depth,
} }
} }
@ -61,6 +99,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(1., 0.5, 0.5).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());
@ -95,7 +147,7 @@ impl Core {
}), }),
..Default::default() ..Default::default()
}); });
self.mesh1.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 +163,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 +174,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

@ -26,20 +26,136 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1600</width> <width>1600</width>
<height>34</height> <height>33</height>
</rect> </rect>
</property> </property>
</widget> </widget>
<widget class="QStatusBar" name="statusbar"/> <widget class="QStatusBar" name="statusbar"/>
<widget class="QDockWidget" name="dockWidget"> <widget class="QDockWidget" name="dockWidget">
<property name="features"> <property name="features">
<set>QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable</set> <set>QDockWidget::DockWidgetFeature::DockWidgetFloatable|QDockWidget::DockWidgetFeature::DockWidgetMovable</set>
</property> </property>
<attribute name="dockWidgetArea"> <attribute name="dockWidgetArea">
<number>2</number> <number>2</number>
</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>32</green>
<blue>0</blue> <blue>60</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>