feat: add real-time view

This commit is contained in:
Elise Amber Katze 2024-06-30 15:54:25 +02:00
parent 500314755c
commit c0ed10b2e5
Signed by: Elise Amber Katze
GPG key ID: FA8F56FFFE6E8B3E
12 changed files with 2173 additions and 97 deletions

1572
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,6 @@
[workspace]
members = [".", "rtrt"]
[package]
name = "rsrt"
authors = ["Elise Amber Katze <git@katze.sh>"]
@ -7,6 +10,7 @@ edition = "2021"
[dependencies]
fastrand = "2.1.0"
image = { version = "0.25.1", default-features = false, features = ["png"] }
paste = "1.0.15"
rayon = "1.10.0"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

13
rtrt/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "rtrt"
version = "0.1.0"
edition = "2021"
[dependencies]
pixels = "0.13.0"
rayon = "1.10.0"
rsrt = { path = ".." }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
winit = "0.28"
winit_input_helper = "0.14"

12
rtrt/src/main.rs Normal file
View file

@ -0,0 +1,12 @@
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod window;
fn main() {
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(tracing_subscriber::EnvFilter::from_default_env())
.init();
window::run().unwrap();
}

331
rtrt/src/window.rs Normal file
View file

@ -0,0 +1,331 @@
use std::time::{Duration, Instant};
use pixels::{Error, Pixels, SurfaceTexture};
use rsrt::builder::RaytracerBuilder;
use rsrt::interval::Interval;
use rsrt::raytracer::util::RenderContext;
use rsrt::raytracer::{random_f64, random_f64_range, Raytracer};
use rsrt::vec3::{Color, Point3, Vec3};
use winit::dpi::LogicalSize;
use winit::event::{Event, VirtualKeyCode};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::WindowBuilder;
use winit_input_helper::WinitInputHelper;
const WIDTH: u32 = 1920 / 1;
const HEIGHT: u32 = 1080 / 1;
const SPP_HQ: usize = 100;
const SPP_LQ: usize = 15;
const SPP_SLQ: usize = 1;
struct State {
pub look_from: Point3,
pub look_at: Point3,
pub vfov: f64,
pub focus: f64,
pub width: usize,
pub height: usize,
builder: RaytracerBuilder,
render_ctx: RenderContext,
pub last_render: Instant,
pub last_slq: bool,
}
pub fn run() -> Result<(), Error> {
let event_loop = EventLoop::new();
let mut input = WinitInputHelper::new();
let window = {
let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64);
WindowBuilder::new()
.with_title("rtrt")
.with_inner_size(size)
.with_min_inner_size(size)
.build(&event_loop)
.unwrap()
};
let mut pixels = {
let window_size = window.inner_size();
let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window);
Pixels::new(WIDTH, HEIGHT, surface_texture)?
};
let mut world = State::new();
event_loop.run(move |event, _, control_flow| {
if world.last_slq && world.last_render.elapsed() >= Duration::from_secs(2) {
world.upgrade_from_slq();
}
// Draw the current frame
if let Event::RedrawRequested(_) = event {
world.draw(pixels.frame_mut());
if let Err(err) = pixels.render() {
tracing::error!("pixels.render: {err}");
*control_flow = ControlFlow::Exit;
return;
}
}
// Handle input events
if input.update(&event) {
// Close events
if input.key_pressed(VirtualKeyCode::Escape) || input.close_requested() {
*control_flow = ControlFlow::Exit;
return;
}
const MOVE_ADJ: f64 = 0.25;
const LOOK_ADJ: f64 = 0.25;
const FOV_ADJ: f64 = 0.10;
const FOCUS_ADJ: f64 = 1.00;
let mut move_backwards_amount = 0.0f64;
let mut move_left_amount = 0.0f64;
let mut move_up_amount = 0.0f64;
let mut look_up_amount = 0.0f64;
let mut look_left_amount = 0.0f64;
let mut fov_amount = 0.0f64;
let mut hq = false;
let mut focus_amount = 0.0f64;
// Move events
if input.key_pressed(VirtualKeyCode::W) {
move_backwards_amount -= MOVE_ADJ;
}
if input.key_pressed(VirtualKeyCode::S) {
move_backwards_amount += MOVE_ADJ;
}
if input.key_pressed(VirtualKeyCode::A) {
move_left_amount += MOVE_ADJ;
}
if input.key_pressed(VirtualKeyCode::D) {
move_left_amount -= MOVE_ADJ;
}
if input.key_pressed(VirtualKeyCode::Up) {
look_up_amount += LOOK_ADJ;
}
if input.key_pressed(VirtualKeyCode::Down) {
look_up_amount -= LOOK_ADJ;
}
if input.key_pressed(VirtualKeyCode::Right) {
look_left_amount -= LOOK_ADJ;
}
if input.key_pressed(VirtualKeyCode::Left) {
look_left_amount += LOOK_ADJ;
}
if input.key_pressed(VirtualKeyCode::Space) {
move_up_amount += LOOK_ADJ;
}
if input.key_pressed(VirtualKeyCode::LShift) || input.key_pressed(VirtualKeyCode::RShift) {
move_up_amount -= LOOK_ADJ;
}
if input.key_pressed(VirtualKeyCode::Equals) {
fov_amount += FOV_ADJ;
}
if input.key_pressed(VirtualKeyCode::Minus) {
fov_amount -= FOV_ADJ;
}
if input.key_pressed(VirtualKeyCode::LBracket) {
focus_amount += FOCUS_ADJ;
}
if input.key_pressed(VirtualKeyCode::RBracket) {
focus_amount -= FOCUS_ADJ;
}
if input.key_pressed(VirtualKeyCode::Return) {
hq = true;
}
let resized: Option<(usize, usize)> = None;
// Resize the window
if let Some(size) = input.window_resized() {
if let Err(err) = pixels.resize_surface(size.width, size.height) {
tracing::error!("pixels.resize_surface: {err}");
*control_flow = ControlFlow::Exit;
return;
}
// if let Err(err) = pixels.resize_buffer(size.width, size.height) {
// tracing::error!("pixels.resize_buffer: {err}");
// *control_flow = ControlFlow::Exit;
// return;
// }
// resized = Some((size.width, size.height))
}
// Update internal state and request a redraw
if hq
|| look_left_amount != 0.0
|| look_up_amount != 0.0
|| move_backwards_amount != 0.0
|| move_left_amount != 0.0
|| move_up_amount != 0.0
|| resized.is_some()
|| fov_amount != 0.0
|| focus_amount != 0.0
{
if let Some((width, height)) = resized {
world.width = width as usize;
world.height = height as usize;
}
world.focus += focus_amount;
world.look_at += Vec3::new(0.0, look_up_amount, look_left_amount); //move_backwards_amount, look_up_amount + move_up_amount, look_left_amount + move_left_amount);
world.look_from += Vec3::new(move_backwards_amount, move_up_amount, move_left_amount);
world.vfov += fov_amount;
world.update(hq, resized.is_some());
}
window.request_redraw();
}
});
}
impl State {
/// Create a new `World` instance that can draw a moving box.
fn new() -> Self {
let look_from = Point3::new(13.0, 2.0, 3.0);
let look_at = Point3::new(0.0, 0.0, 0.0);
let vfov = 20.0;
let focus = 10.0;
let mut builder = Raytracer::builder()
.image_size(WIDTH as usize, HEIGHT as usize)
.samples_per_pixel(1)
.max_depth(50)
.vertical_fov(vfov)
.look_from(13.0, 2.0, 3.0)
.look_at(0.0, 0.0, 0.0)
.vup(0.0, 1.0, 0.0)
.defocus_angle(0.6)
.focus_dist(focus)
.with_sphere(0.0, -1000.0, 0.0, 1000.0)
.lambertian(1.0, 0.6, 0.7);
for a in -11..11 {
for b in -11..11 {
let choose_mat = random_f64();
let center =
Point3::new(a as f64 + 0.9 * random_f64(), 0.2, b as f64 + 0.9 * random_f64());
if (center - Point3::new(4.0, 0.2, 0.0)).len() > 0.9 {
let shape_builder =
builder.with_sphere(center.x(), center.y(), center.z(), 0.2);
builder = if choose_mat < 0.8 {
let albedo = Color::random() * Color::random();
shape_builder.lambertian(albedo.r(), albedo.g(), albedo.b())
} else if choose_mat < 0.95 {
let albedo = Color::random_range(0.5, 1.0);
let fuzz = random_f64_range(0.0, 0.5);
shape_builder.metal(albedo.r(), albedo.g(), albedo.b(), fuzz)
} else {
shape_builder.dielectric(1.5)
};
}
}
}
let builder = builder
.with_sphere(0.0, 1.0, 0.0, 1.0)
.dielectric(1.5)
.with_sphere(-4.0, 1.0, 0.0, 1.0)
.lambertian(1.0, 0.5, 0.6)
.with_sphere(4.0, 1.0, 0.0, 1.0)
.metal(0.7, 0.6, 0.5, 0.0);
let render_ctx = RenderContext::new(WIDTH as usize, HEIGHT as usize, 0);
builder.clone().finish().render_multithread_rayon_detached(render_ctx.clone());
Self {
builder: builder.clone(),
look_from,
look_at,
render_ctx,
width: WIDTH as usize,
height: HEIGHT as usize,
last_render: Instant::now(),
last_slq: true,
vfov,
focus
}
}
fn upgrade_from_slq(&mut self) {
assert!(self.last_slq);
self.last_slq = false;
self.builder
.samples_per_pixel_ref(SPP_LQ);
self.builder.clone().finish().render_multithread_rayon_detached(self.render_ctx.clone());
}
fn update(&mut self, hq: bool, changed_buffer: bool) {
self.render_ctx.next();
if changed_buffer {
self.render_ctx = RenderContext::new(self.width, self.height, self.render_ctx.render_id());
self.builder.image_size_ref(self.width, self.height);
}
let spp = match hq {
true => SPP_HQ,
false => SPP_SLQ,
};
self.last_render = Instant::now();
self.last_slq = spp == SPP_SLQ;
self.builder
.focus_dist_ref(self.focus)
.vertical_fov_ref(self.vfov)
.samples_per_pixel_ref(match hq {
true => SPP_HQ,
false => SPP_SLQ,
})
.look_from_ref(self.look_from.x(), self.look_from.y(), self.look_from.z())
.look_at_ref(self.look_at.x(), self.look_at.y(), self.look_at.z());
self.builder.clone().finish().render_multithread_rayon_detached(self.render_ctx.clone());
}
fn draw(&mut self, frame: &mut [u8]) {
for (index, pixel) in frame.chunks_exact_mut(4).enumerate() {
fn linear_to_gamma(linear_component: f64) -> f64 {
if linear_component > 0.0 {
return linear_component.sqrt();
}
0.0
}
let color = self.render_ctx.get_color_raw_idx(index);
let r = linear_to_gamma(color.r());
let g = linear_to_gamma(color.g());
let b = linear_to_gamma(color.b());
const INTENSITY: Interval = Interval::new(0.000, 0.999);
pixel.copy_from_slice(&[
(INTENSITY.clamp(r) * 256.0) as u8,
(INTENSITY.clamp(g) * 256.0) as u8,
(INTENSITY.clamp(b) * 256.0) as u8,
255 as u8,
]);
}
}
}

View file

@ -36,6 +36,7 @@ impl SphereBuilder {
}
}
#[derive(Debug, Clone)]
pub struct RaytracerBuilder {
pub world: HittableList,
@ -71,51 +72,62 @@ impl Default for RaytracerBuilder {
}
}
macro_rules! fn_ref {
($(pub fn $fident:ident(&mut $selfident:ident, $($aident:ident: $aty:ty),*) $b:block)*) => {
paste::paste! {
$(
pub fn $fident(mut $selfident, $($aident: $aty),*) -> Self {
$b
$selfident
}
pub fn [<$fident _ref>](&mut $selfident, $($aident: $aty),*) -> &mut Self {
$b
$selfident
}
)*
}
};
}
impl RaytracerBuilder {
pub fn image_size(mut self, width: usize, height: usize) -> Self {
fn_ref! {
pub fn image_size(&mut self, width: usize, height: usize) {
self.image_width = width;
self.image_height = height;
self
}
pub fn look_from(mut self, x: f64, y: f64, z: f64) -> Self {
pub fn look_from(&mut self, x: f64, y: f64, z: f64) {
self.look_from = Point3::new(x, y, z);
self
}
pub fn look_at(mut self, x: f64, y: f64, z: f64) -> Self {
pub fn look_at(&mut self, x: f64, y: f64, z: f64) {
self.look_at = Point3::new(x, y, z);
self
}
pub fn vup(mut self, x: f64, y: f64, z: f64) -> Self {
pub fn vup(&mut self, x: f64, y: f64, z: f64) {
self.vup = Point3::new(x, y, z);
self
}
pub fn max_depth(mut self, depth: usize) -> Self {
pub fn max_depth(&mut self, depth: usize) {
self.max_depth = depth;
self
}
pub fn samples_per_pixel(mut self, samples_per_pixel: usize) -> Self {
pub fn samples_per_pixel(&mut self, samples_per_pixel: usize) {
self.samples_per_pixel = samples_per_pixel;
self
}
pub fn vertical_fov(mut self, vertical_fov: f64) -> Self {
pub fn vertical_fov(&mut self, vertical_fov: f64) {
self.vertical_fov = vertical_fov;
self
}
pub fn defocus_angle(mut self, defocus_angle: f64) -> Self {
pub fn defocus_angle(&mut self, defocus_angle: f64) {
self.defocus_angle = defocus_angle;
self
}
pub fn focus_dist(mut self, focus_dist: f64) -> Self {
pub fn focus_dist(&mut self, focus_dist: f64) {
self.focus_dist = focus_dist;
self
}
}
pub fn with_sphere(self, x: f64, y: f64, z: f64, radius: f64) -> SphereBuilder {

View file

@ -36,6 +36,7 @@ pub trait Hittable {
fn hit(&self, ray: &Ray, ray_t: &Interval) -> Option<HitRecord>;
}
#[derive(Debug, Clone, Copy)]
pub enum Object {
Sphere(sphere::Sphere),
}
@ -48,6 +49,7 @@ impl Hittable for Object {
}
}
#[derive(Debug, Clone)]
pub struct HittableList {
objects: Vec<Object>,
}

View file

@ -2,6 +2,7 @@ use crate::{interval::Interval, material::Materials, vec3::Point3};
use super::{HitRecord, Hittable};
#[derive(Debug, Clone, Copy)]
pub struct Sphere {
center: Point3,
radius: f64,

View file

@ -10,7 +10,7 @@ fn main() {
.with(tracing_subscriber::EnvFilter::from_default_env())
.init();
let image = cover(WIDTH, HEIGHT, 500).render();
let image = cover(WIDTH, HEIGHT, 500).render_multithread_rayon();
out::write_image("./image.png", &image[..], WIDTH as u32, HEIGHT as u32).unwrap();
}

View file

@ -1,4 +1,5 @@
use rayon::iter::{IntoParallelIterator, ParallelIterator};
pub mod util;
pub mod render;
use crate::{
builder::RaytracerBuilder,
@ -124,43 +125,6 @@ impl Raytracer {
let a = 0.5 * (unit_direction.y() + 1.0);
((1.0 - a) * Color::new(1.0, 1.0, 1.0)) + (a * Color::new(0.5, 0.7, 1.0))
}
pub fn render_multithread_rayon(&self) -> Vec<Color> {
(0..self.image_height)
.into_par_iter()
.flat_map(|y| (0..self.image_width).into_par_iter().map(move |x| (x, y)))
.map(|(j, i)| {
let mut pixel_color = Vec3::default();
for _ in 0..self.samples_per_pixel {
let ray = self.get_ray(i, j);
pixel_color += self.ray_color(self.max_depth, &ray);
}
pixel_color * self.pixel_samples_scale
})
.collect()
}
pub fn render(&self) -> Vec<Color> {
let mut out = Vec::with_capacity(self.image_height * self.image_width);
for j in 0..self.image_height {
tracing::debug!("Scanlines remaining: {}", self.image_height - j);
for i in 0..self.image_width {
let mut pixel_color = Vec3::default();
for _ in 0..self.samples_per_pixel {
let ray = self.get_ray(i, j);
pixel_color += self.ray_color(self.max_depth, &ray);
}
out.push(pixel_color * self.pixel_samples_scale);
}
}
out
}
}
#[inline(always)]

77
src/raytracer/render.rs Normal file
View file

@ -0,0 +1,77 @@
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use crate::vec3::{Color, Vec3};
use super::util::RenderContext;
impl super::Raytracer {
pub fn render_multithread_rayon_detached(self, ctx: RenderContext) {
let render_id = ctx.render_id();
std::thread::spawn(move || {
let start = std::time::Instant::now();
tracing::info!("Starting Render ID {render_id:#06X}");
let terminated = (0..self.image_height)
.into_par_iter()
.flat_map(|y| (0..self.image_width).into_par_iter().map(move |x| (x, y)))
.any(|(i, j)| {
if ctx.render_id() != render_id {
return true;
}
let mut pixel_color = Vec3::default();
for _ in 0..self.samples_per_pixel {
let ray = self.get_ray(i, j);
pixel_color += self.ray_color(self.max_depth, &ray);
}
ctx.set_color(i, j, pixel_color * self.pixel_samples_scale);
false
});
tracing::info!(
"Finished Render ID {render_id:#06X} in {}s (terminated={terminated})",
start.elapsed().as_secs_f64()
);
});
}
pub fn render_multithread_rayon(&self) -> Vec<Color> {
(0..self.image_height)
.into_par_iter()
.flat_map(|y| (0..self.image_width).into_par_iter().map(move |x| (x, y)))
.map(|(i, j)| {
tracing::debug!("Scanlines remaining: {}", self.image_height - j);
let mut pixel_color = Vec3::default();
for _ in 0..self.samples_per_pixel {
let ray = self.get_ray(i, j);
pixel_color += self.ray_color(self.max_depth, &ray);
}
pixel_color * self.pixel_samples_scale
})
.collect()
}
pub fn render(&self) -> Vec<Color> {
let mut out = Vec::with_capacity(self.image_height * self.image_width);
for j in 0..self.image_height {
tracing::debug!("Scanlines remaining: {}", self.image_height - j);
for i in 0..self.image_width {
let mut pixel_color = Color::default();
for _ in 0..self.samples_per_pixel {
let ray = self.get_ray(i, j);
pixel_color += self.ray_color(self.max_depth, &ray);
}
out.push(pixel_color * self.pixel_samples_scale);
}
}
out
}
}

118
src/raytracer/util.rs Normal file
View file

@ -0,0 +1,118 @@
use std::{alloc::Layout, cell::UnsafeCell, ops::Deref, ptr::NonNull, sync::atomic::AtomicUsize};
use crate::vec3::Color;
pub struct RenderContextInner {
width: usize,
render_id: UnsafeCell<usize>,
max: usize,
layout: Layout,
buffer: UnsafeCell<*mut Color>,
refs: AtomicUsize,
}
impl RenderContextInner {
pub fn new(width: usize, height: usize, id: usize) -> Self {
let layout = Layout::array::<Color>(width * height).unwrap();
let buffer = UnsafeCell::new(unsafe { std::alloc::alloc_zeroed(layout) } as *mut Color);
Self {
width,
max: width * height,
layout,
buffer,
refs: AtomicUsize::new(1),
render_id: UnsafeCell::new(id),
}
}
pub fn render_id(&self) -> usize {
let ptr = self.render_id.get();
unsafe { std::ptr::read_volatile(ptr) }
}
pub fn next(&self) {
let ptr = self.render_id.get();
unsafe {
std::ptr::write_volatile(ptr, std::ptr::read_volatile(ptr) + 1);
}
}
pub fn set_color(&self, x: usize, y: usize, color: Color) {
let offset = (y * self.width) + x;
if offset >= self.max {
panic!("Bad index: {offset} >= {}", self.max);
}
unsafe {
let ptr = *self.buffer.get();
std::ptr::write(ptr.add(offset), color);
}
}
pub fn get_color_raw_idx(&self, offset: usize) -> Color {
if offset >= self.max {
panic!("Bad index: {offset} >= {}", self.max);
}
unsafe {
let ptr = *self.buffer.get();
std::ptr::read(ptr.add(offset))
}
}
pub fn get_color(&self, x: usize, y: usize) -> Color {
let offset = (y * self.width) + x;
self.get_color_raw_idx(offset)
}
}
#[derive(Debug)]
pub struct RenderContext {
inner: NonNull<RenderContextInner>,
}
impl Deref for RenderContext {
type Target = RenderContextInner;
fn deref(&self) -> &Self::Target {
unsafe { self.inner.as_ref() }
}
}
impl RenderContext {
pub fn new(width: usize, height: usize, id: usize) -> Self {
const LAYOUT: Layout = Layout::new::<RenderContextInner>();
let inner = NonNull::new(unsafe { std::alloc::alloc_zeroed(LAYOUT) as *mut RenderContextInner }).unwrap();
unsafe { inner.write_volatile(RenderContextInner::new(width, height, id)); }
Self { inner }
}
}
impl Clone for RenderContext {
fn clone(&self) -> Self {
if unsafe { self.inner.as_ref() }.refs.fetch_add(1, std::sync::atomic::Ordering::SeqCst) == usize::MAX {
panic!("RenderContextInner refcount overflowed??");
}
unsafe { std::ptr::read_volatile(self as *const Self) }
}
}
impl Drop for RenderContext {
fn drop(&mut self) {
let r = unsafe { self.inner.as_ref() }.refs.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
if r <= 1 {
let n = unsafe { self.inner.as_ref() }.refs.load(std::sync::atomic::Ordering::SeqCst);
println!("Freeing... old={r} new={n}");
unsafe {
let ptr_ptr: *mut *mut Color = self.inner.as_ref().buffer.get();
let ptr = *ptr_ptr;
std::alloc::dealloc(ptr as *mut u8, self.inner.as_ref().layout);
}
}
}
}
unsafe impl Send for RenderContext {}
unsafe impl Sync for RenderContext {}