From 0e919c339cb8a4179bec0cdcd0df42632afc2cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20R=C3=B6ger?= Date: Thu, 2 Apr 2026 14:41:01 +0200 Subject: [PATCH] feat(texture): add plane textures --- scenes/demo-3.lisp | 76 ++++++++++++++++++++++ src/raytracer/lisp.rs | 64 ++++++++++++++++++- src/raytracer/mod.rs | 1 + src/raytracer/plane.rs | 135 +++++++++++++++++++++++++++++++++++---- src/raytracer/sphere.rs | 2 - src/raytracer/texture.rs | 121 +++++++++++++++++++++++++++++++++++ src/raytracer/types.rs | 2 + 7 files changed, 386 insertions(+), 15 deletions(-) create mode 100644 scenes/demo-3.lisp create mode 100644 src/raytracer/texture.rs diff --git a/scenes/demo-3.lisp b/scenes/demo-3.lisp new file mode 100644 index 0000000..3f9b06a --- /dev/null +++ b/scenes/demo-3.lisp @@ -0,0 +1,76 @@ +(set 'red + (material + (color 1 0 0) + (color 1 0 0) + (color 0.5 0 0) + 50 0.25)) +(set 'blue + (material + (color 0 0 1) + (color 0 0 1) + (color 0 0 0.6) + 50 0.25)) +(set 'green + (material + (color 0 1 0) + (color 0 1 0) + (color 0 0.6 0) + 50 0.25)) + +(set 's1 + (sphere + (point 0 1 0) 1 blue)) +(set 's2 + (sphere + (point 2 0.5 2) 0.5 green)) + +(defun spiral-sphere (i n) + (sphere + (progn + (print "Spiral Sphere at: ") + (println (point + (* 2 (cos (/ (* i 6.2) n))) + 0.5 + (* 2 (sin (/ (* i 6.2) n))))) + ) + 0.5 red)) + +(defun spiral (scn i n) + (if (< i n) + (scene-add + (spiral scn (+ i 1) n) + (spiral-sphere i n)) + scn)) + +(set 'mandelbrot + (mandelbrot-texture + 1800.0 + (point2 -0.7489967346191402 -0.06952285766601607) + 1000 + (color 0.3 0 0) + (color 0.3 0 0) + (color 0.3 0 0) + )) + +(set 'p1 + (texture-plane + mandelbrot + (point 0 0 0) + (vector 0 1 0) + 1.0 + (vector 1 0 0))) + +(set 'l1 (light (point 3 10 5) (color 1 1 1))) +(set 'l2 (light (point 2 10 5) (color 1 1 1))) + + +(set 'scn (scene + (color 0.1 0.1 0.1) + '(s1 s2 p1) + '(l1 l2))) + +(println (cons "Final Scene:" scn)) + +(set 'cam (camera (point 0 3 6) (point 0 0 0) (vector 0 1 0) 40 1920 1080)) + +(render cam scn 5 4 "demo-3.png") diff --git a/src/raytracer/lisp.rs b/src/raytracer/lisp.rs index 27f50a3..a14bbae 100644 --- a/src/raytracer/lisp.rs +++ b/src/raytracer/lisp.rs @@ -1,6 +1,10 @@ use std::path::PathBuf; -use crate::raytracer::{scene::Scene, types::Light}; +use crate::raytracer::{ + scene::Scene, + texture::TextureWrapper, + types::{Light, Point2}, +}; use lispers_macro::{native_lisp_function, native_lisp_function_proxy}; @@ -13,8 +17,9 @@ use lispers_core::lisp::{ use super::{ camera::Camera, - plane::{Checkerboard, Plane}, + plane::{Checkerboard, Plane, TexturePlane}, sphere::Sphere, + texture::MandelbrotTexture, types::{Color, Material, Point3, RTObjectWrapper, Vector3}, RTError, }; @@ -24,6 +29,11 @@ pub fn point(x: f64, y: f64, z: f64) -> Result, EvalE Ok(ForeignDataWrapper::new(Point3::new(x, y, z))) } +#[native_lisp_function(eval)] +pub fn point2(x: f64, y: f64) -> Result, EvalError> { + Ok(ForeignDataWrapper::new(Point2::new(x, y))) +} + #[native_lisp_function(eval)] pub fn vector(x: f64, y: f64, z: f64) -> Result, EvalError> { Ok(ForeignDataWrapper::new(Vector3::new(x, y, z))) @@ -90,6 +100,47 @@ pub fn checkerboard( ) } +#[native_lisp_function(eval)] +pub fn texture_plane( + texture: ForeignDataWrapper, + pos: ForeignDataWrapper, + norm: ForeignDataWrapper, + sca: f64, + up: ForeignDataWrapper, +) -> Result, EvalError> { + Ok( + ForeignDataWrapper::new(RTObjectWrapper::from(TexturePlane::new( + *pos, + *norm, + texture.clone(), + sca, + *up, + ))) + .into(), + ) +} + +#[native_lisp_function(eval)] +pub fn mandelbrot_texture( + scale: f64, + at: ForeignDataWrapper, + max_iter: i64, + ambient_color: ForeignDataWrapper, + diffuse_color: ForeignDataWrapper, + specular_color: ForeignDataWrapper, +) -> Result, EvalError> { + Ok(ForeignDataWrapper::new(TextureWrapper::new( + MandelbrotTexture::new( + scale, + *at, + max_iter as u32, + *ambient_color, + *diffuse_color, + *specular_color, + ), + ))) +} + pub fn scene(env: &Environment, expr: Expression) -> Result { let [amb, objs, lgts]: [Expression; 3] = expr.try_into()?; @@ -469,6 +520,7 @@ native_lisp_function_proxy!( /// Adds the raytracing functions to the given environment layer. pub fn mk_raytrace(layer: &mut EnvironmentLayer) { layer.set("point".to_string(), Expression::Function(point)); + layer.set("point2".to_string(), Expression::Function(point2)); layer.set("vector".to_string(), Expression::Function(vector)); layer.set("color".to_string(), Expression::Function(color)); layer.set("light".to_string(), Expression::Function(light)); @@ -478,6 +530,14 @@ pub fn mk_raytrace(layer: &mut EnvironmentLayer) { "checkerboard".to_string(), Expression::Function(checkerboard), ); + layer.set( + "texture-plane".to_string(), + Expression::Function(texture_plane), + ); + layer.set( + "mandelbrot-texture".to_string(), + Expression::Function(mandelbrot_texture), + ); layer.set("sphere".to_string(), Expression::Function(sphere)); layer.set("scene".to_string(), Expression::Function(scene)); layer.set("scene-add".to_string(), Expression::Function(scene_add)); diff --git a/src/raytracer/mod.rs b/src/raytracer/mod.rs index 868f138..e0f5437 100644 --- a/src/raytracer/mod.rs +++ b/src/raytracer/mod.rs @@ -3,6 +3,7 @@ pub mod lisp; pub mod plane; pub mod scene; pub mod sphere; +mod texture; pub mod types; mod vec; diff --git a/src/raytracer/plane.rs b/src/raytracer/plane.rs index 984f3da..6b02b1c 100644 --- a/src/raytracer/plane.rs +++ b/src/raytracer/plane.rs @@ -1,4 +1,7 @@ -use super::types::{Intersect, Material, Point3, Scalar, Vector3}; +use super::{ + texture::TextureWrapper, + types::{Intersect, Material, Point3, Scalar, Vector3}, +}; extern crate nalgebra as na; @@ -26,6 +29,21 @@ pub struct Checkerboard { projection_matrix: na::Matrix2x3, } +/// Define a plane using a 2D texture function +#[derive(Clone)] +pub struct TexturePlane { + /// The position of the plane. + position: Point3, + /// The normal of the plane. + normal: Vector3, + /// The scale of the plane (factor for x,y passed to material function) + scale: f64, + /// A projection matrix to map 3D points to the 2D plane space. + projection_matrix: na::Matrix2x3, + /// The texture to use. + texture: TextureWrapper, +} + impl Plane { /// Create a new plane. /// - `position` is the position of the plane. @@ -66,6 +84,49 @@ impl Checkerboard { } } +impl TexturePlane { + /// Create a new Function Plane. + /// - `position` is the position of the plane. + /// - `normal` is the normal of the plane. + /// - `texture` the texture to use + /// - `scale` is the side-length of each square. + /// - `up` is "y" direction on the plane in 3D-Space. + pub fn new( + position: Point3, + normal: Vector3, + texture: TextureWrapper, + scale: f64, + up: Vector3, + ) -> TexturePlane { + let right = up.cross(&normal).normalize(); + TexturePlane { + position, + normal, + scale, + projection_matrix: na::Matrix3x2::from_columns(&[right, up]).transpose(), + texture, + } + } +} + +fn plane_intersect( + position: Point3, + normal: Vector3, + ray: &super::types::Ray, +) -> Option<(Point3, Vector3, super::types::Scalar)> { + let denom = normal.dot(&ray.direction); + if denom != 0.0 { + let d = normal.dot(&position.coords); + let t = (d - normal.dot(&ray.origin.coords)) / denom; + + if t > 1e-5 { + let point = ray.origin + ray.direction * t; + return Some((point, normal, t)); + } + } + None +} + impl Intersect for Plane { fn intersect( &self, @@ -76,17 +137,11 @@ impl Intersect for Plane { super::types::Scalar, super::types::Material, )> { - let denom = self.normal.dot(&ray.direction); - if denom != 0.0 { - let d = self.normal.dot(&self.position.coords); - let t = (d - self.normal.dot(&ray.origin.coords)) / denom; - - if t > 1e-5 { - let point = ray.origin + ray.direction * t; - return Some((point, self.normal, t, self.material.clone())); - } + if let Some((point, normal, t)) = plane_intersect(self.position, self.normal, ray) { + Some((point, normal, t, self.material.clone())) + } else { + None } - None } } @@ -117,6 +172,29 @@ impl Intersect for Checkerboard { } } +impl Intersect for TexturePlane { + fn intersect( + &self, + ray: &super::types::Ray, + ) -> Option<( + Point3, + Vector3, + super::types::Scalar, + super::types::Material, + )> { + if let Some((point, normal, t)) = plane_intersect(self.position, self.normal, ray) { + let v3 = point - self.position; + let v2 = self.projection_matrix * v3; + let material = self + .texture + .material_at(na::Point2::new(v2.x / self.scale, v2.y / self.scale)); + Some((point, normal, t, material)) + } else { + None + } + } +} + impl std::fmt::Display for Plane { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -137,6 +215,26 @@ impl std::fmt::Display for Checkerboard { } } +impl std::fmt::Debug for TexturePlane { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "(function-plane position: {:?}, normal: {:?}, scale: {:?})", + self.position, self.normal, self.scale, + ) + } +} + +impl std::fmt::Display for TexturePlane { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "(function-plane position: {}, normal: {}, scale: {})", + self.position, self.normal, self.scale, + ) + } +} + impl PartialOrd for Plane { fn partial_cmp(&self, _other: &Self) -> Option { None @@ -148,3 +246,18 @@ impl PartialOrd for Checkerboard { None } } + +impl PartialEq for TexturePlane { + fn eq(&self, other: &Self) -> bool { + self.normal == other.normal + && self.position == other.position + && self.projection_matrix == other.projection_matrix + && self.scale == other.scale + } +} + +impl PartialOrd for TexturePlane { + fn partial_cmp(&self, _other: &Self) -> Option { + None + } +} diff --git a/src/raytracer/sphere.rs b/src/raytracer/sphere.rs index ade2c21..fadb9f7 100644 --- a/src/raytracer/sphere.rs +++ b/src/raytracer/sphere.rs @@ -1,5 +1,3 @@ -use nalgebra::distance; - use super::types::{Intersect, Material, Point3, Ray, Scalar, Vector3}; extern crate nalgebra as na; diff --git a/src/raytracer/texture.rs b/src/raytracer/texture.rs new file mode 100644 index 0000000..3d90710 --- /dev/null +++ b/src/raytracer/texture.rs @@ -0,0 +1,121 @@ +use std::fmt::Debug; +use std::fmt::Display; +use std::sync::Arc; + +use as_any::AsAny; +use nalgebra as na; + +use super::types::Color; +use super::types::Material; +use super::types::Point2; +use super::types::Scalar; + +pub trait Texture: Display + Debug + AsAny + Sync + Send { + fn material_at(&self, pt: Point2) -> Material; +} + +#[derive(Clone, Debug)] +pub struct TextureWrapper(Arc); + +impl TextureWrapper { + pub fn new(texture: T) -> Self { + Self(Arc::new(texture)) + } +} + +impl Display for TextureWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self, f) + } +} + +impl TextureWrapper { + pub fn material_at(&self, pt: Point2) -> Material { + self.0.material_at(pt) + } +} + +impl PartialEq for TextureWrapper { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl PartialOrd for TextureWrapper { + fn partial_cmp(&self, other: &Self) -> Option { + PartialOrd::partial_cmp(&Arc::as_ptr(&self.0).addr(), &Arc::as_ptr(&other.0).addr()) + } +} + +pub struct MandelbrotTexture { + scale: Scalar, + at: Point2, + max_iter: u32, + ambient_color: Color, + diffuse_color: Color, + specular_color: Color, +} + +impl MandelbrotTexture { + pub fn new( + scale: Scalar, + at: Point2, + max_iter: u32, + ambient_color: Color, + diffuse_color: Color, + specular_color: Color, + ) -> Self { + Self { + scale, + at, + max_iter, + ambient_color, + diffuse_color, + specular_color, + } + } +} + +impl Texture for MandelbrotTexture { + fn material_at(&self, pt: Point2) -> Material { + let x = (pt.x / self.scale) + self.at.x; + let y = (pt.y / self.scale) + self.at.y; + let mut z = na::Vector2::new(0.0, 0.0); + let mut n = 0; + while z.norm() < 2.0 && n < self.max_iter { + let xtemp = z.x * z.x - z.y * z.y + x; + z.y = 2.0 * z.x * z.y + y; + z.x = xtemp; + n += 1; + } + let c = n as f64 / self.max_iter as f64; + + Material { + ambient_color: self.ambient_color * c, + diffuse_color: self.diffuse_color * c, + specular_color: self.specular_color * c, + shininess: (1.0 - c) * 10.0, + mirror: 1.0 - c, + } + } +} + +impl Display for MandelbrotTexture { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "MandelbrotTexture{{at={}, max_iter={}}}", + self.at, self.max_iter + ) + } +} + +impl Debug for MandelbrotTexture { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "MandelbrotTexture{{at={}, max_iter={}}}", + self.at, self.max_iter + ) + } +} diff --git a/src/raytracer/types.rs b/src/raytracer/types.rs index 3e72906..09af6ec 100644 --- a/src/raytracer/types.rs +++ b/src/raytracer/types.rs @@ -10,6 +10,8 @@ pub type Scalar = f64; pub type Vector3 = na::Vector3; /// The Point3 type to use for raytracing pub type Point3 = na::Point3; +/// The Point2 type to use for texture lookups +pub type Point2 = na::Point2; /// The Color type to use for raytracing pub type Color = Vector3;