render the model

This commit is contained in:
numzero 2026-07-20 03:36:54 +03:00
parent 184f9550d9
commit db18de85ad
9 changed files with 688 additions and 17 deletions

8
Cargo.lock generated
View File

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

View File

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

47
src/camera.rs Normal file
View File

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

View File

@ -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<E: Error + 'static>(
surface,
})
}
fn faceted_mesh(mesh: &mesh::Mesh) -> Vec<render::Vertex> {
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
}

27
src/mesh.wgsl Normal file
View File

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

148
src/render.rs Normal file
View File

@ -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::<LookParams>() 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::<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, 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<Item = &'a Mesh>,
) {
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);
}
}
}

View File

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

View File

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

View File

@ -41,21 +41,241 @@
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Background color</string>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Camera</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Yaw</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="camYaw">
<property name="minimum">
<number>-180</number>
</property>
<property name="maximum">
<number>180</number>
</property>
<property name="pageStep">
<number>15</number>
</property>
<property name="value">
<number>45</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Pitch</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="camPitch">
<property name="minimum">
<number>-90</number>
</property>
<property name="maximum">
<number>90</number>
</property>
<property name="pageStep">
<number>15</number>
</property>
<property name="value">
<number>30</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="KColorCombo" name="inBackground">
<property name="color">
<color>
<red>25</red>
<green>220</green>
<blue>0</blue>
</color>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Light</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Pitch</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="lightPitch">
<property name="minimum">
<number>-90</number>
</property>
<property name="maximum">
<number>90</number>
</property>
<property name="pageStep">
<number>15</number>
</property>
<property name="value">
<number>60</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Shape</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Sides</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="sides">
<property name="minimum">
<number>3</number>
</property>
<property name="maximum">
<number>120</number>
</property>
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Layers up</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="layersUp">
<property name="maximum">
<number>11</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Layers down</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="layersDown">
<property name="maximum">
<number>11</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Size</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>Base diameter</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="dia">
<property name="suffix">
<string> mm</string>
</property>
<property name="minimum">
<double>4.000000000000000</double>
</property>
<property name="maximum">
<double>100.000000000000000</double>
</property>
<property name="stepType">
<enum>QAbstractSpinBox::AdaptiveDecimalStepType</enum>
</property>
<property name="value">
<double>36.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>Height</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="height">
<property name="suffix">
<string> mm</string>
</property>
<property name="minimum">
<double>4.000000000000000</double>
</property>
<property name="maximum">
<double>500.000000000000000</double>
</property>
<property name="value">
<double>120.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Appearance</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Background color</string>
</property>
</widget>
</item>
<item>
<widget class="KColorCombo" name="inBackground">
<property name="color">
<color>
<red>85</red>
<green>170</green>
<blue>255</blue>
</color>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
@ -97,8 +317,8 @@
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1531</x>
<y>115</y>
<x>1585</x>
<y>803</y>
</hint>
<hint type="destinationlabel">
<x>799</x>
@ -106,6 +326,134 @@
</hint>
</hints>
</connection>
<connection>
<sender>camYaw</sender>
<signal>valueChanged(int)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1520</x>
<y>142</y>
</hint>
<hint type="destinationlabel">
<x>1449</x>
<y>133</y>
</hint>
</hints>
</connection>
<connection>
<sender>camPitch</sender>
<signal>valueChanged(int)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1550</x>
<y>192</y>
</hint>
<hint type="destinationlabel">
<x>1447</x>
<y>198</y>
</hint>
</hints>
</connection>
<connection>
<sender>lightPitch</sender>
<signal>valueChanged(int)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1565</x>
<y>286</y>
</hint>
<hint type="destinationlabel">
<x>1445</x>
<y>265</y>
</hint>
</hints>
</connection>
<connection>
<sender>sides</sender>
<signal>valueChanged(int)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1521</x>
<y>370</y>
</hint>
<hint type="destinationlabel">
<x>1448</x>
<y>378</y>
</hint>
</hints>
</connection>
<connection>
<sender>layersUp</sender>
<signal>valueChanged(int)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1490</x>
<y>436</y>
</hint>
<hint type="destinationlabel">
<x>1446</x>
<y>441</y>
</hint>
</hints>
</connection>
<connection>
<sender>layersDown</sender>
<signal>valueChanged(int)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1478</x>
<y>513</y>
</hint>
<hint type="destinationlabel">
<x>1446</x>
<y>515</y>
</hint>
</hints>
</connection>
<connection>
<sender>dia</sender>
<signal>valueChanged(double)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1472</x>
<y>611</y>
</hint>
<hint type="destinationlabel">
<x>1446</x>
<y>629</y>
</hint>
</hints>
</connection>
<connection>
<sender>height</sender>
<signal>valueChanged(double)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1476</x>
<y>686</y>
</hint>
<hint type="destinationlabel">
<x>1446</x>
<y>686</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>updateView()</slot>