Skip to content

Commit b58dc7c

Browse files
committed
feat: implement camera-space hatching based on lighting
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.
1 parent cf7f663 commit b58dc7c

31 files changed

+2858
-225
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ euclid = "0.22"
1717
float-cmp = "0.5"
1818
log = "0.4"
1919
pyo3 = "0.22"
20+
rand = "0.8"
2021
rayon = "1.2"
2122
numpy = "0.22"
2223
svg = "0.18"

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ use raydeon::shapes::AxisAlignedCuboid;
2525
use raydeon::{Camera, Scene, WPoint3, WVec3};
2626
use std::sync::Arc;
2727

28-
env_logger::Builder::from_default_env()
29-
.format_timestamp_nanos()
30-
.init();
31-
3228
fn main() {
33-
let scene = Scene::new(vec![Arc::new(AxisAlignedCuboid::new(
29+
env_logger::Builder::from_default_env()
30+
.format_timestamp_nanos()
31+
.init();
32+
33+
let scene = Scene::new().with_geometry(vec![Arc::new(AxisAlignedCuboid::new(
3434
WVec3::new(-1.0, -1.0, -1.0),
3535
WVec3::new(1.0, 1.0, 1.0),
3636
))]);
@@ -40,12 +40,14 @@ fn main() {
4040
let up = WVec3::new(0.0, 0.0, 1.0);
4141

4242
let fovy = 50.0;
43-
let width = 1024.0;
44-
let height = 1024.0;
43+
let width = 1024;
44+
let height = 1024;
4545
let znear = 0.1;
4646
let zfar = 10.0;
4747

48-
let camera = Camera::new().look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar);
48+
let camera = Camera::new()
49+
.look_at(eye, focus, up)
50+
.perspective(fovy, width, height, znear, zfar);
4951

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

pyraydeon/examples/py_sphere.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import svg
2+
3+
from pyraydeon import (
4+
Camera,
5+
Point3,
6+
Scene,
7+
Sphere,
8+
Vec3,
9+
Geometry,
10+
Material,
11+
PointLight,
12+
)
13+
14+
15+
class PySphere(Geometry):
16+
def __init__(self, point, radius, material=None):
17+
if material is not None:
18+
self._material = material
19+
20+
self.sphere = Sphere(point, radius)
21+
22+
@property
23+
def material(self):
24+
return self._material
25+
26+
def collision_geometry(self):
27+
return [self.sphere]
28+
29+
def paths(self, cam):
30+
return []
31+
32+
33+
scene = Scene(
34+
geometry=[PySphere(Point3(0, 0, 0), 1.0, Material(3.0, 3.0, 3))],
35+
lights=[PointLight((2, 0, 5), 1.0, 2.0, 0.05, 0.2, 0.09)],
36+
)
37+
38+
39+
eye = Point3(0, 0, 5)
40+
focus = Vec3(0, 0, 0)
41+
up = Vec3(0, 1, 0)
42+
43+
fovy = 50.0
44+
width = 1024
45+
height = 1024
46+
znear = 0.1
47+
zfar = 10.0
48+
49+
cam = Camera().look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar)
50+
51+
paths = scene.render_with_lighting(cam, seed=5)
52+
53+
canvas = svg.SVG(
54+
width="8in",
55+
height="8in",
56+
viewBox="0 0 1024 1024",
57+
)
58+
backing_rect = svg.Rect(
59+
x=0,
60+
y=0,
61+
width="100%",
62+
height="100%",
63+
fill="white",
64+
)
65+
svg_lines = [
66+
svg.Line(
67+
x1=f"{path.p1[0]}",
68+
y1=f"{path.p1[1]}",
69+
x2=f"{path.p2[0]}",
70+
y2=f"{path.p2[1]}",
71+
stroke_width="0.7mm",
72+
stroke="black",
73+
)
74+
for path in paths
75+
]
76+
line_group = svg.G(transform=f"translate(0, {height}) scale(1, -1)", elements=svg_lines)
77+
canvas.elements = [backing_rect, line_group]
78+
79+
80+
print(canvas)

pyraydeon/examples/py_sphere_expected.svg

Lines changed: 1 addition & 0 deletions
Loading

pyraydeon/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
use pyo3::prelude::*;
22

3-
#[derive(Copy, Clone, Debug, Default)]
4-
struct Material;
5-
63
macro_rules! pywrap {
74
($name:ident, $wraps:ty) => {
85
#[derive(Debug, Clone, Copy)]
@@ -25,7 +22,9 @@ macro_rules! pywrap {
2522
};
2623
}
2724

25+
mod light;
2826
mod linear;
27+
mod material;
2928
mod ray;
3029
mod scene;
3130
mod shapes;
@@ -37,5 +36,7 @@ fn pyraydeon(m: &Bound<'_, PyModule>) -> PyResult<()> {
3736
crate::shapes::register(m)?;
3837
crate::scene::register(m)?;
3938
crate::ray::register(m)?;
39+
crate::material::register(m)?;
40+
crate::light::register(m)?;
4041
Ok(())
4142
}

pyraydeon/src/light.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use crate::linear::Point3;
2+
use pyo3::prelude::*;
3+
4+
pywrap!(PointLight, raydeon::lights::PointLight);
5+
6+
#[pymethods]
7+
impl PointLight {
8+
#[new]
9+
#[pyo3(signature = (
10+
position,
11+
intensity=0.0,
12+
specular_intensity=0.0,
13+
constant_attenuation=0.0,
14+
linear_attenuation=0.0,
15+
quadratic_attenuation=0.0
16+
))]
17+
fn new(
18+
position: &Bound<'_, PyAny>,
19+
intensity: f64,
20+
specular_intensity: f64,
21+
constant_attenuation: f64,
22+
linear_attenuation: f64,
23+
quadratic_attenuation: f64,
24+
) -> PyResult<Self> {
25+
let position = Point3::try_from(position)?;
26+
Ok(raydeon::lights::PointLight::new(
27+
intensity,
28+
specular_intensity,
29+
position.0.cast_unit(),
30+
constant_attenuation,
31+
linear_attenuation,
32+
quadratic_attenuation,
33+
)
34+
.into())
35+
}
36+
37+
#[getter]
38+
fn intensity(&self) -> f64 {
39+
self.0.intensity()
40+
}
41+
42+
#[getter]
43+
fn specular(&self) -> f64 {
44+
self.0.specular()
45+
}
46+
47+
#[getter]
48+
fn position(&self) -> Point3 {
49+
self.0.position().cast_unit().into()
50+
}
51+
52+
#[getter]
53+
fn constant_attenuation(&self) -> f64 {
54+
self.0.constant_attenuation()
55+
}
56+
57+
#[getter]
58+
fn linear_attenuation(&self) -> f64 {
59+
self.0.linear_attenuation()
60+
}
61+
62+
#[getter]
63+
fn quadratic_attenuation(&self) -> f64 {
64+
self.0.quadratic_attenuation()
65+
}
66+
67+
fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
68+
let class_name = slf.get_type().qualname()?;
69+
Ok(format!("{}<{:#?}>", class_name, slf.borrow().0))
70+
}
71+
}
72+
73+
impl Default for PointLight {
74+
fn default() -> Self {
75+
raydeon::lights::PointLight::default().into()
76+
}
77+
}
78+
79+
pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
80+
m.add_class::<PointLight>()?;
81+
Ok(())
82+
}

pyraydeon/src/material.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use pyo3::prelude::*;
2+
3+
pywrap!(Material, raydeon::material::Material);
4+
5+
#[pymethods]
6+
impl Material {
7+
#[new]
8+
#[pyo3(signature = (diffuse=0.0, specular=0.0, shininess=0.0, tag=0))]
9+
fn new(diffuse: f64, specular: f64, shininess: f64, tag: usize) -> PyResult<Self> {
10+
Ok(raydeon::material::Material::new(diffuse, specular, shininess, tag).into())
11+
}
12+
13+
#[getter]
14+
fn diffuse(&self) -> f64 {
15+
self.diffuse
16+
}
17+
18+
#[getter]
19+
fn specular(&self) -> f64 {
20+
self.specular
21+
}
22+
23+
#[getter]
24+
fn shininess(&self) -> f64 {
25+
self.shininess
26+
}
27+
28+
#[getter]
29+
fn tag(&self) -> usize {
30+
self.tag
31+
}
32+
33+
fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
34+
let class_name = slf.get_type().qualname()?;
35+
Ok(format!("{}<{:#?}>", class_name, slf.borrow().0))
36+
}
37+
}
38+
39+
impl Default for Material {
40+
fn default() -> Self {
41+
raydeon::material::Material::default().into()
42+
}
43+
}
44+
45+
pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
46+
m.add_class::<Material>()?;
47+
Ok(())
48+
}

pyraydeon/src/ray.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@ pywrap!(HitData, raydeon::HitData);
3636
#[pymethods]
3737
impl HitData {
3838
#[new]
39-
fn new(hit_point: PyReadonlyArray1<f64>, dist_to: f64) -> PyResult<Self> {
39+
fn new(
40+
hit_point: PyReadonlyArray1<f64>,
41+
dist_to: f64,
42+
normal: PyReadonlyArray1<f64>,
43+
) -> PyResult<Self> {
4044
let hit_point = Point3::try_from(hit_point)?;
41-
Ok(raydeon::HitData::new(hit_point.0.cast_unit(), dist_to).into())
45+
let normal = Vec3::try_from(normal)?;
46+
Ok(raydeon::HitData::new(hit_point.0.cast_unit(), dist_to, normal.0.cast_unit()).into())
4247
}
4348

4449
#[getter]

0 commit comments

Comments
 (0)