Compare commits

...

6 Commits

Author SHA1 Message Date
2995a830a2 show spheres 2025-11-24 17:29:36 +03:00
77cab94a6e add basic shape generation 2025-11-24 17:29:36 +03:00
4b138deb34 allow choosing which normals to use 2025-11-24 00:21:06 +03:00
a51f46a038 fix a bug
it's invisible with Lambertian model though
2025-11-24 00:19:28 +03:00
fb8ea92ce8 fix double-accounting for surface slope 2025-11-23 21:02:38 +03:00
707bb6f66e embed the simple shader 2025-11-23 13:29:24 +03:00
11 changed files with 431 additions and 11 deletions

View File

@ -9,12 +9,13 @@ use crate::{
camera::OrbitalCamera, camera::OrbitalCamera,
ray::Ray, ray::Ray,
render::lines::{LookParams, Mesh, Pipeline, Vertex}, render::lines::{LookParams, Mesh, Pipeline, Vertex},
trace::{Hit, Lambertian, Reflector, Scene, Source, Sphere}, trace::{Hit, Lambertian, Reflective, Reflector, Scene, Source, Sphere},
}; };
mod camera; mod camera;
mod ray; mod ray;
mod render; mod render;
mod shape;
mod trace; mod trace;
const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb; const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb;
@ -27,6 +28,13 @@ pub struct SphericalPosition {
pub distance: f32, pub distance: f32,
} }
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum UseNormal {
Light,
Camera,
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
#[repr(C)] #[repr(C)]
pub struct RedrawArgs { pub struct RedrawArgs {
@ -36,7 +44,8 @@ pub struct RedrawArgs {
pub light_spread: f32, pub light_spread: f32,
pub accum_sigma: f32, pub accum_sigma: f32,
pub accum_scale: f32, pub accum_scale: f32,
pub reflections: u32, pub reflections: u8,
pub use_normal: UseNormal,
pub show_axes: bool, pub show_axes: bool,
pub show_shapes: bool, pub show_shapes: bool,
pub show_hit_emission: bool, pub show_hit_emission: bool,
@ -58,6 +67,7 @@ pub struct Core {
surface: wgpu::Surface<'static>, surface: wgpu::Surface<'static>,
pipeline: Pipeline, pipeline: Pipeline,
mesh_pipe: render::faces::Pipeline,
tripod: Mesh, tripod: Mesh,
} }
@ -121,6 +131,7 @@ impl Core {
} = gpu; } = gpu;
let pipeline = Pipeline::new(&device, OUTPUT_FORMAT); let pipeline = Pipeline::new(&device, OUTPUT_FORMAT);
let mesh_pipe = render::faces::Pipeline::new(&device, OUTPUT_FORMAT);
let tripod = new_tripod(&device); let tripod = new_tripod(&device);
queue.submit([]); // flush buffer updates queue.submit([]); // flush buffer updates
@ -130,6 +141,7 @@ impl Core {
surface, surface,
pipeline, pipeline,
tripod, tripod,
mesh_pipe,
} }
} }
@ -152,6 +164,12 @@ impl Core {
m: perspective * camera.transform(), m: perspective * camera.transform(),
}, },
); );
self.mesh_pipe.set_look(
&self.queue,
render::faces::LookParams {
m: perspective * camera.transform(),
},
);
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());
@ -214,6 +232,26 @@ impl Core {
sphere(vec3(0.1, 0.3, 0.1)), sphere(vec3(0.1, 0.3, 0.1)),
], ],
}; };
if args.show_shapes {
let mut meshes = Vec::new();
for obj in &scene.objects {
let mesh = shape::sphere((obj.radius * 15.) as usize + 7);
let obj_mesh = render::faces::Mesh::new(
&self.device,
&mesh
.vertices
.iter()
.map(|v| render::faces::Vertex {
pos: obj.position + obj.radius * v,
color: 0.5 * (v + 1.),
})
.collect::<Vec<_>>(),
bytemuck::cast_slice(&mesh.indices),
);
meshes.push(obj_mesh);
}
self.mesh_pipe.render(&mut pass, &meshes);
}
let mut prng = rand_pcg::Pcg64::new(42, 0); let mut prng = rand_pcg::Pcg64::new(42, 0);
let source_rays: Vec<Ray> = (0..10240).map(|_| source.make_ray(&mut prng)).collect(); let source_rays: Vec<Ray> = (0..10240).map(|_| source.make_ray(&mut prng)).collect();
@ -305,13 +343,15 @@ impl Core {
if d2 > 9. * sigma2 { if d2 > 9. * sigma2 {
continue; continue;
} }
assert!(hit.normal.is_normalized()); let normal = match args.use_normal {
UseNormal::Light => light_hit.normal,
UseNormal::Camera => hit.normal,
};
assert!(normal.is_normalized());
assert!(hit.incident.dir.is_normalized()); assert!(hit.incident.dir.is_normalized());
let reflector = Lambertian; let reflector = Lambertian;
let in_lm = 1.0; let in_lm = 1.0;
let out_cd = in_lm let out_cd = in_lm * reflector.brdf(normal, light_hit.incident.dir, -ray.dir);
* hit.normal.dot(-hit.incident.dir)
* reflector.brdf(hit.normal, hit.incident.dir, -ray.dir);
let weight = accum_normalizator * (-0.5 * d2 / sigma2).exp(); let weight = accum_normalizator * (-0.5 * d2 / sigma2).exp();
total_cd += weight * out_cd; total_cd += weight * out_cd;
} }

View File

@ -1,7 +1,7 @@
use std::{f32::consts::PI, sync::Arc}; use std::{f32::consts::PI, sync::Arc};
use glam::uvec2; use glam::uvec2;
use photon_light::{Core, RedrawArgs, SphericalPosition, init_gpu_inner}; use photon_light::{Core, RedrawArgs, SphericalPosition, UseNormal, init_gpu_inner};
use winit::{ use winit::{
application::ApplicationHandler, application::ApplicationHandler,
event::WindowEvent, event::WindowEvent,
@ -53,6 +53,7 @@ impl MainWindow {
accum_sigma: 0.025, accum_sigma: 0.025,
accum_scale: 0.01, accum_scale: 0.01,
reflections: 2, reflections: 2,
use_normal: UseNormal::Light,
show_axes: true, show_axes: true,
show_shapes: true, show_shapes: true,
show_hit_emission: false, show_hit_emission: false,

161
src/render/faces.rs Normal file
View File

@ -0,0 +1,161 @@
use std::mem::offset_of;
use bytemuck::{Pod, Zeroable, bytes_of, cast_slice};
use glam::{Mat4, Vec3};
use wgpu::util::DeviceExt as _;
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct LookParams {
pub m: Mat4,
}
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct Vertex {
pub pos: Vec3,
pub color: Vec3,
}
impl Vertex {
pub fn new(pos: Vec3, color: Vec3) -> Self {
Self { pos, color }
}
}
pub struct Mesh {
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
index_count: u32,
}
impl Mesh {
pub fn new(device: &wgpu::Device, vertices: &[Vertex], indices: &[u16]) -> Self {
if vertices.len() >= 1 << 16 {
panic!("too many vertices");
}
for index in indices {
if *index as usize >= vertices.len() {
panic!(
"vertex index out of bounds: {index} out of {}",
vertices.len()
);
}
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
usage: wgpu::BufferUsages::VERTEX,
contents: cast_slice(vertices),
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
usage: wgpu::BufferUsages::INDEX,
contents: cast_slice(indices),
});
Self {
vertex_buffer,
index_buffer,
index_count: indices.len().try_into().expect("too many indices"),
}
}
}
pub struct Pipeline {
look_buf: wgpu::Buffer,
bindings: wgpu::BindGroup,
pipeline: wgpu::RenderPipeline,
}
impl Pipeline {
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> 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(super::SIMPLE_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, pos) as u64,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
shader_location: 1,
offset: offset_of!(Vertex, color) as u64,
format: wgpu::VertexFormat::Float32x3,
},
],
}],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
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,
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.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
pass.draw_indexed(0..mesh.index_count, 0, 0..1);
}
}
}

View File

@ -57,10 +57,9 @@ impl Pipeline {
mapped_at_creation: false, mapped_at_creation: false,
}); });
let shader = std::fs::read_to_string("shaders/line.wgsl").unwrap();
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None, label: None,
source: wgpu::ShaderSource::Wgsl(shader.into()), source: wgpu::ShaderSource::Wgsl(super::SIMPLE_SHADER.into()),
}); });
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None, label: None,

View File

@ -1 +1,4 @@
pub mod faces;
pub mod lines; pub mod lines;
static SIMPLE_SHADER: &str = include_str!("simple.wgsl");

140
src/shape.rs Normal file
View File

@ -0,0 +1,140 @@
use std::f32::consts::PI;
use glam::{Vec3, vec3};
pub type Face = [u16; 3];
#[derive(Debug, Clone)]
pub struct Mesh {
pub vertices: Vec<Vec3>,
pub indices: Vec<Face>,
}
pub fn sphere(subdiv: usize) -> Mesh {
assert!(subdiv >= 2);
assert!(2 * subdiv * (subdiv - 1) + 2 <= 1 << 16);
let mut vertices = Vec::new();
vertices.push(Vec3::Z);
for j in 1..subdiv {
let v = j as f32 / subdiv as f32;
let theta = PI * v;
let (xy, z) = theta.sin_cos();
for i in 0..2 * subdiv {
let u = i as f32 / (2 * subdiv) as f32;
let phi = 2. * PI * u;
let (y, x) = phi.sin_cos();
vertices.push(vec3(xy * x, xy * y, z));
}
}
vertices.push(-Vec3::Z);
assert_eq!(vertices.len(), 2 * subdiv * (subdiv - 1) + 2);
let mut indices = Vec::new();
let subdiv = subdiv as u16;
for i in 0..2 * subdiv - 1 {
indices.push([0, i + 1, i + 2]);
}
indices.push([0, 2 * subdiv, 1]);
for j in 0..subdiv - 2 {
let k1 = j * 2 * subdiv + 1;
let k2 = k1 + 2 * subdiv;
for i in 0..2 * subdiv - 1 {
let a = k1 + i;
let b = k2 + i;
indices.push([a, b, b + 1]);
indices.push([a, b + 1, a + 1]);
}
let a = k1 + 2 * subdiv - 1;
let b = k2 + 2 * subdiv - 1;
indices.push([a, b, k2]);
indices.push([a, k2, k1]);
}
let k1 = 2 * subdiv * (subdiv - 2) + 1;
let k2 = 2 * subdiv * (subdiv - 1) + 1;
for i in 0..2 * subdiv - 1 {
indices.push([k1 + i, k2, k1 + i + 1]);
}
indices.push([k1 + 2 * subdiv - 1, k2, k1]);
Mesh { vertices, indices }
}
#[cfg(test)]
mod tests {
use approx::abs_diff_eq;
use super::*;
#[test]
fn test_sphere_2_topology() {
assert_eq!(
sphere(2).indices,
vec![
[0, 1, 2],
[0, 2, 3],
[0, 3, 4],
[0, 4, 1],
[1, 5, 2],
[2, 5, 3],
[3, 5, 4],
[4, 5, 1],
]
);
}
fn are_equal_eps(left: &[Vec3], right: &[Vec3], eps: f32) -> bool {
if left.len() != right.len() {
return false;
}
left.iter()
.zip(right.iter())
.all(|(a, b)| abs_diff_eq!(a, b, epsilon = eps))
}
#[test]
fn test_sphere_2_geometry() {
let left = sphere(2).vertices;
let right = [Vec3::Z, Vec3::X, Vec3::Y, -Vec3::X, -Vec3::Y, -Vec3::Z];
assert!(
are_equal_eps(&left, &right, 1e-6),
"assertion `left == right` failed\n left: {left:?}\n right: {right:?}"
);
}
#[test]
fn test_sphere_3_topology() {
assert_eq!(
sphere(3).indices,
vec![
// top
[0, 1, 2],
[0, 2, 3],
[0, 3, 4],
[0, 4, 5],
[0, 5, 6],
[0, 6, 1],
// belt
[1, 7, 8],
[1, 8, 2],
[2, 8, 9],
[2, 9, 3],
[3, 9, 10],
[3, 10, 4],
[4, 10, 11],
[4, 11, 5],
[5, 11, 12],
[5, 12, 6],
[6, 12, 7],
[6, 7, 1],
// bottom
[7, 13, 8],
[8, 13, 9],
[9, 13, 10],
[10, 13, 11],
[11, 13, 12],
[12, 13, 7],
]
);
}
}

View File

@ -13,6 +13,11 @@ struct SphericalPosition {
float distance = 1.; float distance = 1.;
}; };
enum class UseNormal: std::uint8_t {
Light,
Camera,
};
struct RedrawArgs { struct RedrawArgs {
SphericalPosition camera_position; SphericalPosition camera_position;
SphericalPosition light_position; SphericalPosition light_position;
@ -20,7 +25,8 @@ struct RedrawArgs {
float light_spread = 0.; float light_spread = 0.;
float accum_sigma = 1.; float accum_sigma = 1.;
float accum_scale = 1.; float accum_scale = 1.;
std::uint32_t reflections = 0; std::uint8_t reflections = 0;
UseNormal use_normal = UseNormal::Light;
bool show_axes = true; bool show_axes = true;
bool show_shapes = true; bool show_shapes = true;
bool show_hit_emission = true; bool show_hit_emission = true;
@ -31,6 +37,7 @@ struct RedrawArgs {
}; };
} // namespace ffi } // namespace ffi
using ffi::UseNormal;
using ffi::RedrawArgs; using ffi::RedrawArgs;
using ffi::SphericalPosition; using ffi::SphericalPosition;

View File

@ -16,6 +16,11 @@ float deg_to_rad(float val) {
} }
void PhotonLight::updateView() { void PhotonLight::updateView() {
UseNormal use_normal = {};
if (m_ui->normalFromLight->isChecked())
use_normal = UseNormal::Light;
else if (m_ui->normalFromCamera->isChecked())
use_normal = UseNormal::Camera;
RedrawArgs args{ RedrawArgs args{
.camera_position = SphericalPosition{ .camera_position = SphericalPosition{
.yaw = deg_to_rad(m_ui->cameraYaw->value()), .yaw = deg_to_rad(m_ui->cameraYaw->value()),
@ -31,7 +36,8 @@ void PhotonLight::updateView() {
.light_spread = 0.125, .light_spread = 0.125,
.accum_sigma = exp10f(m_ui->accumSigma->value() / 25.0), .accum_sigma = exp10f(m_ui->accumSigma->value() / 25.0),
.accum_scale = exp10f(m_ui->accumScale->value() / 25.0), .accum_scale = exp10f(m_ui->accumScale->value() / 25.0),
.reflections = std::uint32_t(m_ui->reflections->value()), .reflections = std::uint8_t(m_ui->reflections->value()),
.use_normal = use_normal,
.show_axes = m_ui->displayAxes->isChecked(), .show_axes = m_ui->displayAxes->isChecked(),
.show_shapes = m_ui->displayShapes->isChecked(), .show_shapes = m_ui->displayShapes->isChecked(),
.show_hit_emission = m_ui->displayEmitted->isChecked(), .show_hit_emission = m_ui->displayEmitted->isChecked(),
@ -51,4 +57,9 @@ void PhotonLight::updateView() {
m_ui->viewport->setView(args); m_ui->viewport->setView(args);
} }
void PhotonLight::updateViewIf(bool update) {
if (update)
updateView();
}
#include "moc_main_window.cpp" #include "moc_main_window.cpp"

View File

@ -16,6 +16,7 @@ public:
public slots: public slots:
void updateView(); void updateView();
void updateViewIf(bool update); // for radio buttons
private: private:
const std::unique_ptr<Ui::MainWindow> m_ui; const std::unique_ptr<Ui::MainWindow> m_ui;

View File

@ -298,6 +298,30 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Use normal at</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="normalFromLight">
<property name="text">
<string>Hit position</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="normalFromCamera">
<property name="text">
<string>Show position</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -637,8 +661,41 @@
</hint> </hint>
</hints> </hints>
</connection> </connection>
<connection>
<sender>normalFromLight</sender>
<signal>toggled(bool)</signal>
<receiver>MainWindow</receiver>
<slot>updateViewIf(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>1429</x>
<y>787</y>
</hint>
<hint type="destinationlabel">
<x>1381</x>
<y>783</y>
</hint>
</hints>
</connection>
<connection>
<sender>normalFromCamera</sender>
<signal>toggled(bool)</signal>
<receiver>MainWindow</receiver>
<slot>updateViewIf(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>1932</x>
<y>816</y>
</hint>
<hint type="destinationlabel">
<x>1600</x>
<y>874</y>
</hint>
</hints>
</connection>
</connections> </connections>
<slots> <slots>
<slot>updateView()</slot> <slot>updateView()</slot>
<slot>updateViewIf(bool)</slot>
</slots> </slots>
</ui> </ui>