Skip to content

Commit

Permalink
feat: implement camera-space hatching based on lighting
Browse files Browse the repository at this point in the history
Geometry can be associated with arbitrary metadata; however, if
associated with a Material, the scene can be rendered usng raydeon's
lighting model.

The lighting model implements diffuse and specular lighting, with hatch
line subsegments removed stochastically based on the brightess at the
associated point in world space.
  • Loading branch information
cbgbt committed Nov 26, 2024
1 parent cf7f663 commit 447906b
Show file tree
Hide file tree
Showing 31 changed files with 2,988 additions and 228 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ euclid = "0.22"
float-cmp = "0.5"
log = "0.4"
pyo3 = "0.22"
rand = "0.8"
rayon = "1.2"
numpy = "0.22"
svg = "0.18"
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ use raydeon::shapes::AxisAlignedCuboid;
use raydeon::{Camera, Scene, WPoint3, WVec3};
use std::sync::Arc;

env_logger::Builder::from_default_env()
.format_timestamp_nanos()
.init();

fn main() {
let scene = Scene::new(vec![Arc::new(AxisAlignedCuboid::new(
env_logger::Builder::from_default_env()
.format_timestamp_nanos()
.init();

let scene = Scene::new().with_geometry(vec![Arc::new(AxisAlignedCuboid::new(
WVec3::new(-1.0, -1.0, -1.0),
WVec3::new(1.0, 1.0, 1.0),
))]);
Expand All @@ -40,12 +40,14 @@ fn main() {
let up = WVec3::new(0.0, 0.0, 1.0);

let fovy = 50.0;
let width = 1024.0;
let height = 1024.0;
let width = 1024;
let height = 1024;
let znear = 0.1;
let zfar = 10.0;

let camera = Camera::new().look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar);
let camera = Camera::new()
.look_at(eye, focus, up)
.perspective(fovy, width, height, znear, zfar);

let paths = scene.attach_camera(camera).render();

Expand Down
140 changes: 140 additions & 0 deletions pyraydeon/examples/py_sphere.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import math
import svg
import numpy as np

from pyraydeon import (
Camera,
Point3,
Scene,
Sphere,
Vec3,
Geometry,
Material,
PointLight,
Plane,
LineSegment3D,
)


class PySphere(Geometry):
def __init__(self, point, radius, material=None):
if material is not None:
self._material = material

self.sphere = Sphere(point, radius)

@property
def material(self):
return self._material

def collision_geometry(self):
return [self.sphere]

def paths(self, cam):
hyp = np.linalg.norm(self.sphere.center - cam.eye)
opp = self.sphere.radius

theta = math.asin(opp / hyp)
adj = opp / math.tan(theta)
d = math.cos(theta) * adj
r = math.sin(theta) * adj

w = self.sphere.center - cam.eye
w = w / np.linalg.norm(w)

u = np.cross(w, cam.up)
u = u / np.linalg.norm(u)

v = np.cross(w, u)
v = v / np.linalg.norm(v)

points = []
c = cam.eye + d * w
for i in range(0, 180):
a = math.radians(float(i))
p = c
p = p + u * (math.cos(a) * r)
p = p + v * (math.sin(a) * r)

push = p - self.sphere.center
push = push / np.linalg.norm(push)

p += push * 0.00015
points.append(p)

paths = []
for i in range(0, len(points) - 1):
paths.append(LineSegment3D(points[i], points[(i + 1)]))

return paths


class PyPlane(Geometry):
def __init__(self, point, normal, material=None):
if material is not None:
self._material = material

self.plane = Plane(point, normal)

@property
def material(self):
return self._material

def collision_geometry(self):
return [self.plane]

def paths(self, cam):
return []


scene = Scene(
geometry=[
PySphere(Point3(0, 0, 0), 1.0, Material(3.0, 3.0, 3)),
PyPlane(Point3(0, -2, 0), Vec3(0, 1, 0), Material(9000.0, 3.0, 3)),
],
lights=[PointLight((4, 3, 10), 4.0, 2.0, 0.15, 0.4, 0.11)],
)


eye = Point3(0, 0, 5)
focus = Vec3(0, 0, 0)
up = Vec3(0, 1, 0)

fovy = 50.0
width = 1024
height = 1024
znear = 0.1
zfar = 100.0

cam = Camera().look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar)

paths = scene.render_with_lighting(cam, seed=5)

canvas = svg.SVG(
width="8in",
height="8in",
viewBox="0 0 1024 1024",
)
backing_rect = svg.Rect(
x=0,
y=0,
width="100%",
height="100%",
fill="white",
)
svg_lines = [
svg.Line(
x1=f"{path.p1[0]}",
y1=f"{path.p1[1]}",
x2=f"{path.p2[0]}",
y2=f"{path.p2[1]}",
stroke_width="0.7mm",
stroke="black",
)
for path in paths
]
line_group = svg.G(transform=f"translate(0, {height}) scale(1, -1)", elements=svg_lines)
canvas.elements = [backing_rect, line_group]


print(canvas)
1 change: 1 addition & 0 deletions pyraydeon/examples/py_sphere_expected.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions pyraydeon/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use pyo3::prelude::*;

#[derive(Copy, Clone, Debug, Default)]
struct Material;

macro_rules! pywrap {
($name:ident, $wraps:ty) => {
#[derive(Debug, Clone, Copy)]
Expand All @@ -25,7 +22,9 @@ macro_rules! pywrap {
};
}

mod light;
mod linear;
mod material;
mod ray;
mod scene;
mod shapes;
Expand All @@ -37,5 +36,7 @@ fn pyraydeon(m: &Bound<'_, PyModule>) -> PyResult<()> {
crate::shapes::register(m)?;
crate::scene::register(m)?;
crate::ray::register(m)?;
crate::material::register(m)?;
crate::light::register(m)?;
Ok(())
}
82 changes: 82 additions & 0 deletions pyraydeon/src/light.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use crate::linear::Point3;
use pyo3::prelude::*;

pywrap!(PointLight, raydeon::lights::PointLight);

#[pymethods]
impl PointLight {
#[new]
#[pyo3(signature = (
position,
intensity=0.0,
specular_intensity=0.0,
constant_attenuation=0.0,
linear_attenuation=0.0,
quadratic_attenuation=0.0
))]
fn new(
position: &Bound<'_, PyAny>,
intensity: f64,
specular_intensity: f64,
constant_attenuation: f64,
linear_attenuation: f64,
quadratic_attenuation: f64,
) -> PyResult<Self> {
let position = Point3::try_from(position)?;
Ok(raydeon::lights::PointLight::new(
intensity,
specular_intensity,
position.0.cast_unit(),
constant_attenuation,
linear_attenuation,
quadratic_attenuation,
)
.into())
}

#[getter]
fn intensity(&self) -> f64 {
self.0.intensity()
}

#[getter]
fn specular(&self) -> f64 {
self.0.specular()
}

#[getter]
fn position(&self) -> Point3 {
self.0.position().cast_unit().into()
}

#[getter]
fn constant_attenuation(&self) -> f64 {
self.0.constant_attenuation()
}

#[getter]
fn linear_attenuation(&self) -> f64 {
self.0.linear_attenuation()
}

#[getter]
fn quadratic_attenuation(&self) -> f64 {
self.0.quadratic_attenuation()
}

fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
let class_name = slf.get_type().qualname()?;
Ok(format!("{}<{:#?}>", class_name, slf.borrow().0))
}
}

impl Default for PointLight {
fn default() -> Self {
raydeon::lights::PointLight::default().into()
}
}

pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PointLight>()?;
Ok(())
}
48 changes: 48 additions & 0 deletions pyraydeon/src/material.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use pyo3::prelude::*;

pywrap!(Material, raydeon::material::Material);

#[pymethods]
impl Material {
#[new]
#[pyo3(signature = (diffuse=0.0, specular=0.0, shininess=0.0, tag=0))]
fn new(diffuse: f64, specular: f64, shininess: f64, tag: usize) -> PyResult<Self> {
Ok(raydeon::material::Material::new(diffuse, specular, shininess, tag).into())
}

#[getter]
fn diffuse(&self) -> f64 {
self.diffuse
}

#[getter]
fn specular(&self) -> f64 {
self.specular
}

#[getter]
fn shininess(&self) -> f64 {
self.shininess
}

#[getter]
fn tag(&self) -> usize {
self.tag
}

fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
let class_name = slf.get_type().qualname()?;
Ok(format!("{}<{:#?}>", class_name, slf.borrow().0))
}
}

impl Default for Material {
fn default() -> Self {
raydeon::material::Material::default().into()
}
}

pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Material>()?;
Ok(())
}
9 changes: 7 additions & 2 deletions pyraydeon/src/ray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ pywrap!(HitData, raydeon::HitData);
#[pymethods]
impl HitData {
#[new]
fn new(hit_point: PyReadonlyArray1<f64>, dist_to: f64) -> PyResult<Self> {
fn new(
hit_point: PyReadonlyArray1<f64>,
dist_to: f64,
normal: PyReadonlyArray1<f64>,
) -> PyResult<Self> {
let hit_point = Point3::try_from(hit_point)?;
Ok(raydeon::HitData::new(hit_point.0.cast_unit(), dist_to).into())
let normal = Vec3::try_from(normal)?;
Ok(raydeon::HitData::new(hit_point.0.cast_unit(), dist_to, normal.0.cast_unit()).into())
}

#[getter]
Expand Down
Loading

0 comments on commit 447906b

Please sign in to comment.