Added DistantDiskLight (a.k.a. sun light) parsing and data structures.
Also created a proper World struct in the process, to store all infinite-extent type stuff. Note that I goofed and did a new rustfmt pass but forgot to commit before making these changes, so there's a lot of formatting changes in this too. *sigh*
This commit is contained in:
parent
746b3b0c1f
commit
3cbb816d4b
|
@ -191,7 +191,8 @@ pub fn merge_slices_to<T: Lerp + Copy, F>(slice1: &[T],
|
|||
if slice1.len() == 0 || slice2.len() == 0 {
|
||||
return;
|
||||
} else if slice1.len() == slice2.len() {
|
||||
for (xfo, (xf1, xf2)) in Iterator::zip(slice_out.iter_mut(),
|
||||
for (xfo, (xf1, xf2)) in
|
||||
Iterator::zip(slice_out.iter_mut(),
|
||||
Iterator::zip(slice1.iter(), slice2.iter())) {
|
||||
*xfo = merge(xf1, xf2);
|
||||
}
|
||||
|
@ -216,14 +217,12 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
fn quick_select_ints(list: &mut [i32], i: usize) {
|
||||
quick_select(list, i, |a, b| {
|
||||
if a < b {
|
||||
quick_select(list, i, |a, b| if a < b {
|
||||
Ordering::Less
|
||||
} else if a == b {
|
||||
Ordering::Equal
|
||||
} else {
|
||||
Ordering::Greater
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,8 @@ impl Assembly {
|
|||
} else {
|
||||
Matrix4x4::new()
|
||||
};
|
||||
if let Some((light_i, sel_pdf, whittled_n)) = self.light_accel
|
||||
if let Some((light_i, sel_pdf, whittled_n)) =
|
||||
self.light_accel
|
||||
.select(incoming * sel_xform,
|
||||
pos * sel_xform,
|
||||
nor * sel_xform,
|
||||
|
@ -239,8 +240,7 @@ impl AssemblyBuilder {
|
|||
// sources.
|
||||
let mut light_instances: Vec<_> = self.instances
|
||||
.iter()
|
||||
.filter(|inst| {
|
||||
match inst.instance_type {
|
||||
.filter(|inst| match inst.instance_type {
|
||||
InstanceType::Object => {
|
||||
if let Object::Light(_) = self.objects[inst.data_index] {
|
||||
true
|
||||
|
@ -252,7 +252,6 @@ impl AssemblyBuilder {
|
|||
InstanceType::Assembly => {
|
||||
self.assemblies[inst.data_index].light_accel.approximate_energy() > 0.0
|
||||
}
|
||||
}
|
||||
})
|
||||
.map(|&a| a)
|
||||
.collect();
|
||||
|
|
82
src/light/distant_disk_light.rs
Normal file
82
src/light/distant_disk_light.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use std::f64::consts::PI as PI_64;
|
||||
|
||||
use color::{XYZ, SpectralSample, Color};
|
||||
use lerp::lerp_slice;
|
||||
use math::{Vector, coordinate_system_from_vector};
|
||||
use sampling::{uniform_sample_cone, uniform_sample_cone_pdf};
|
||||
|
||||
use super::WorldLightSource;
|
||||
|
||||
// TODO: handle case where radius = 0.0.
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DistantDiskLight {
|
||||
radii: Vec<f32>,
|
||||
directions: Vec<Vector>,
|
||||
colors: Vec<XYZ>,
|
||||
}
|
||||
|
||||
impl DistantDiskLight {
|
||||
pub fn new(radii: Vec<f32>, directions: Vec<Vector>, colors: Vec<XYZ>) -> DistantDiskLight {
|
||||
DistantDiskLight {
|
||||
radii: radii,
|
||||
directions: directions,
|
||||
colors: colors,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WorldLightSource for DistantDiskLight {
|
||||
fn sample(&self, u: f32, v: f32, wavelength: f32, time: f32) -> (SpectralSample, Vector, f32) {
|
||||
// Calculate time interpolated values
|
||||
let radius: f64 = lerp_slice(&self.radii, time) as f64;
|
||||
let direction = lerp_slice(&self.directions, time);
|
||||
let col = lerp_slice(&self.colors, time);
|
||||
let solid_angle_inv = 1.0 / (2.0 * PI_64 * (1.0 - radius.cos()));
|
||||
|
||||
// Create a coordinate system from the vector pointing at the center of
|
||||
// of the light.
|
||||
let (z, x, y) = coordinate_system_from_vector(-direction);
|
||||
let (x, y, z) = (x.normalized(), y.normalized(), z.normalized());
|
||||
|
||||
// Calculate the radius in terms of cosine
|
||||
let cos_theta_max: f64 = radius.cos();
|
||||
|
||||
// Sample the cone subtended by the light.
|
||||
let sample = uniform_sample_cone(u, v, cos_theta_max).normalized();
|
||||
|
||||
// Calculate the final values and return everything.
|
||||
let shadow_vec = (x * sample.x()) + (y * sample.y()) + (z * sample.z());
|
||||
let pdf = uniform_sample_cone_pdf(cos_theta_max);
|
||||
let spectral_sample = (col * solid_angle_inv as f32).to_spectral_sample(wavelength);
|
||||
return (spectral_sample, shadow_vec, pdf as f32);
|
||||
}
|
||||
|
||||
fn sample_pdf(&self, sample_dir: Vector, wavelength: f32, time: f32) -> f32 {
|
||||
// We're not using these, silence warnings
|
||||
let _ = (sample_dir, wavelength);
|
||||
|
||||
let radius: f64 = lerp_slice(&self.radii, time) as f64;
|
||||
return uniform_sample_cone_pdf(radius.cos()) as f32;
|
||||
}
|
||||
|
||||
fn outgoing(&self, dir: Vector, wavelength: f32, time: f32) -> SpectralSample {
|
||||
// We're not using this, silence warning
|
||||
let _ = dir;
|
||||
|
||||
let radius = lerp_slice(&self.radii, time) as f64;
|
||||
let col = lerp_slice(&self.colors, time);
|
||||
let solid_angle = 2.0 * PI_64 * (1.0 - radius.cos());
|
||||
(col / solid_angle as f32).to_spectral_sample(wavelength)
|
||||
}
|
||||
|
||||
fn is_delta(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn approximate_energy(&self) -> f32 {
|
||||
let color: XYZ = self.colors.iter().fold(XYZ::new(0.0, 0.0, 0.0), |a, &b| a + b) /
|
||||
self.colors.len() as f32;
|
||||
color.y
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
mod distant_disk_light;
|
||||
mod rectangle_light;
|
||||
mod sphere_light;
|
||||
|
||||
|
@ -7,14 +8,17 @@ use boundable::Boundable;
|
|||
use color::SpectralSample;
|
||||
use math::{Vector, Point, Matrix4x4};
|
||||
|
||||
pub use self::distant_disk_light::DistantDiskLight;
|
||||
pub use self::rectangle_light::RectangleLight;
|
||||
pub use self::sphere_light::SphereLight;
|
||||
|
||||
|
||||
/// A finite light source that can be bounded in space.
|
||||
pub trait LightSource: Boundable + Debug + Sync {
|
||||
/// Samples the light source for a given point to be illuminated.
|
||||
///
|
||||
/// - arr: The point to be illuminated.
|
||||
/// - space: The world-to-object space transform of the light.
|
||||
/// - arr: The point to be illuminated (in world space).
|
||||
/// - u: Random parameter U.
|
||||
/// - v: Random parameter V.
|
||||
/// - wavelength: The wavelength of light to sample at.
|
||||
|
@ -77,6 +81,54 @@ pub trait LightSource: Boundable + Debug + Sync {
|
|||
fn is_delta(&self) -> bool;
|
||||
|
||||
|
||||
/// Returns an approximation of the total energy emitted by the light
|
||||
/// source. Note that this does not need to be exact: it is used for
|
||||
/// importance sampling.
|
||||
fn approximate_energy(&self) -> f32;
|
||||
}
|
||||
|
||||
|
||||
/// An infinite light source that cannot be bounded in space. E.g.
|
||||
/// a sun light source.
|
||||
pub trait WorldLightSource: Debug + Sync {
|
||||
/// Samples the light source for a given point to be illuminated.
|
||||
///
|
||||
/// - u: Random parameter U.
|
||||
/// - v: Random parameter V.
|
||||
/// - wavelength: The wavelength of light to sample at.
|
||||
/// - time: The time to sample at.
|
||||
///
|
||||
/// Returns: The light arriving from the shadow-testing direction, the
|
||||
/// vector to use for shadow testing, and the pdf of the sample.
|
||||
fn sample(&self, u: f32, v: f32, wavelength: f32, time: f32) -> (SpectralSample, Vector, f32);
|
||||
|
||||
|
||||
/// Calculates the pdf of sampling the given sample_dir. This is used
|
||||
/// primarily to calculate probabilities for multiple importance sampling.
|
||||
///
|
||||
/// NOTE: this function CAN assume that sample_dir is a valid sample for
|
||||
/// the light source (i.e. hits/lies on the light source). No guarantees
|
||||
/// are made about the correctness of the return value if it isn't valid.
|
||||
fn sample_pdf(&self, sample_dir: Vector, wavelength: f32, time: f32) -> f32;
|
||||
|
||||
|
||||
/// Returns the color emitted in the given direction from the
|
||||
/// given parameters on the light.
|
||||
///
|
||||
/// - dir: The direction of the outgoing light.
|
||||
/// - wavelength: The hero wavelength of light to sample at.
|
||||
/// - time: The time to sample at.
|
||||
fn outgoing(&self, dir: Vector, wavelength: f32, time: f32) -> SpectralSample;
|
||||
|
||||
|
||||
/// Returns whether the light has a delta distribution.
|
||||
///
|
||||
/// If a light has no chance of a ray hitting it through random process
|
||||
/// then it is a delta light source. For example, point light sources,
|
||||
/// lights that only emit in a single direction, etc.
|
||||
fn is_delta(&self) -> bool;
|
||||
|
||||
|
||||
/// Returns an approximation of the total energy emitted by the light
|
||||
/// source. Note that this does not need to be exact: it is used for
|
||||
/// importance sampling.
|
||||
|
|
|
@ -9,6 +9,7 @@ use sampling::{uniform_sample_cone, uniform_sample_cone_pdf, uniform_sample_sphe
|
|||
|
||||
use super::LightSource;
|
||||
|
||||
// TODO: handle case where radius = 0.0.
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SphereLight {
|
||||
|
|
|
@ -41,6 +41,7 @@ mod timer;
|
|||
mod tracer;
|
||||
mod transform_stack;
|
||||
mod triangle;
|
||||
mod world;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
|
|
|
@ -48,8 +48,8 @@ pub fn fast_pow2(p: f32) -> f32 {
|
|||
let z: f32 = clipp - w as f32 + offset;
|
||||
|
||||
let i: u32 = ((1 << 23) as f32 *
|
||||
(clipp + 121.2740575 + 27.7280233 / (4.84252568 - z) -
|
||||
1.49012907 * z)) as u32;
|
||||
(clipp + 121.2740575 + 27.7280233 / (4.84252568 - z) - 1.49012907 * z)) as
|
||||
u32;
|
||||
|
||||
unsafe { transmute_copy::<u32, f32>(&i) }
|
||||
}
|
||||
|
|
|
@ -64,8 +64,8 @@ pub fn free_sah_split<'a, T, F>(seed: u32, objects: &mut [T], bounder: &F) -> (u
|
|||
|
||||
// Build SAH bins
|
||||
let sah_bins = {
|
||||
let mut sah_bins =
|
||||
[[(BBox::new(), BBox::new(), 0, 0); SAH_BIN_COUNT - 1]; SPLIT_PLANE_COUNT];
|
||||
let mut sah_bins = [[(BBox::new(), BBox::new(), 0, 0); SAH_BIN_COUNT - 1];
|
||||
SPLIT_PLANE_COUNT];
|
||||
for obj in objects.iter() {
|
||||
let tb = lerp_slice(bounder(obj), 0.5);
|
||||
let centroid = tb.center().into_vector();
|
||||
|
|
|
@ -9,11 +9,14 @@ use camera::Camera;
|
|||
use color::{XYZ, rec709e_to_xyz};
|
||||
use math::Matrix4x4;
|
||||
use renderer::Renderer;
|
||||
use world::World;
|
||||
use scene::Scene;
|
||||
use light::WorldLightSource;
|
||||
|
||||
use super::basics::{ws_u32, ws_f32};
|
||||
use super::DataTree;
|
||||
use super::psy_assembly::parse_assembly;
|
||||
use super::psy_light::parse_distant_disk_light;
|
||||
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
@ -24,7 +27,6 @@ pub enum PsyParseError {
|
|||
|
||||
|
||||
/// Takes in a DataTree representing a Scene node and returns
|
||||
/// a renderer.
|
||||
pub fn parse_scene(tree: &DataTree) -> Result<Renderer, PsyParseError> {
|
||||
// Verify we have the right number of each section
|
||||
if tree.iter_children_with_type("Output").count() != 1 {
|
||||
|
@ -79,8 +81,8 @@ pub fn parse_scene(tree: &DataTree) -> Result<Renderer, PsyParseError> {
|
|||
};
|
||||
let scene = Scene {
|
||||
name: scene_name,
|
||||
background_color: world,
|
||||
camera: camera,
|
||||
world: world,
|
||||
root: assembly,
|
||||
};
|
||||
|
||||
|
@ -266,9 +268,10 @@ fn parse_camera(tree: &DataTree) -> Result<Camera, PsyParseError> {
|
|||
|
||||
|
||||
|
||||
fn parse_world(tree: &DataTree) -> Result<XYZ, PsyParseError> {
|
||||
fn parse_world(tree: &DataTree) -> Result<World, PsyParseError> {
|
||||
if tree.is_internal() {
|
||||
let background_color;
|
||||
let mut lights: Vec<Box<WorldLightSource>> = Vec::new();
|
||||
|
||||
// Parse background shader
|
||||
let bgs = {
|
||||
|
@ -281,7 +284,8 @@ fn parse_world(tree: &DataTree) -> Result<XYZ, PsyParseError> {
|
|||
if bgs.iter_children_with_type("Type").count() != 1 {
|
||||
return Err(PsyParseError::UnknownError);
|
||||
}
|
||||
if let &DataTree::Leaf { contents, .. } = bgs.iter_children_with_type("Type")
|
||||
if let &DataTree::Leaf { contents, .. } =
|
||||
bgs.iter_children_with_type("Type")
|
||||
.nth(0)
|
||||
.unwrap() {
|
||||
contents.trim()
|
||||
|
@ -291,11 +295,11 @@ fn parse_world(tree: &DataTree) -> Result<XYZ, PsyParseError> {
|
|||
};
|
||||
match bgs_type {
|
||||
"Color" => {
|
||||
if let Some(&DataTree::Leaf { contents, .. }) = bgs.iter_children_with_type("Color")
|
||||
if let Some(&DataTree::Leaf { contents, .. }) =
|
||||
bgs.iter_children_with_type("Color")
|
||||
.nth(0) {
|
||||
if let IResult::Done(_, color) = closure!(tuple!(ws_f32,
|
||||
ws_f32,
|
||||
ws_f32))(contents.trim()
|
||||
if let IResult::Done(_, color) =
|
||||
closure!(tuple!(ws_f32, ws_f32, ws_f32))(contents.trim()
|
||||
.as_bytes()) {
|
||||
// TODO: proper color space management, not just assuming
|
||||
// rec.709.
|
||||
|
@ -311,7 +315,22 @@ fn parse_world(tree: &DataTree) -> Result<XYZ, PsyParseError> {
|
|||
_ => return Err(PsyParseError::UnknownError),
|
||||
}
|
||||
|
||||
return Ok(background_color);
|
||||
// Parse light sources
|
||||
for child in tree.iter_children() {
|
||||
match child {
|
||||
&DataTree::Internal { type_name, .. } if type_name == "DistantDiskLight" => {
|
||||
lights.push(Box::new(parse_distant_disk_light(&child)?));
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Build and return the world
|
||||
return Ok(World {
|
||||
background_color: background_color,
|
||||
lights: lights,
|
||||
});
|
||||
} else {
|
||||
return Err(PsyParseError::UnknownError);
|
||||
}
|
||||
|
@ -321,7 +340,8 @@ fn parse_world(tree: &DataTree) -> Result<XYZ, PsyParseError> {
|
|||
|
||||
|
||||
pub fn parse_matrix(contents: &str) -> Result<Matrix4x4, PsyParseError> {
|
||||
if let IResult::Done(_, ns) = closure!(terminated!(tuple!(ws_f32,
|
||||
if let IResult::Done(_, ns) =
|
||||
closure!(terminated!(tuple!(ws_f32,
|
||||
ws_f32,
|
||||
ws_f32,
|
||||
ws_f32,
|
||||
|
|
|
@ -4,14 +4,70 @@ use std::result::Result;
|
|||
|
||||
use nom::IResult;
|
||||
|
||||
use math::Vector;
|
||||
use color::{XYZ, rec709e_to_xyz};
|
||||
use light::{SphereLight, RectangleLight};
|
||||
use light::{DistantDiskLight, SphereLight, RectangleLight};
|
||||
|
||||
use super::basics::ws_f32;
|
||||
use super::DataTree;
|
||||
use super::psy::PsyParseError;
|
||||
|
||||
|
||||
pub fn parse_distant_disk_light(tree: &DataTree) -> Result<DistantDiskLight, PsyParseError> {
|
||||
if let &DataTree::Internal { ref children, .. } = tree {
|
||||
let mut radii = Vec::new();
|
||||
let mut directions = Vec::new();
|
||||
let mut colors = Vec::new();
|
||||
|
||||
// Parse
|
||||
for child in children.iter() {
|
||||
match child {
|
||||
// Radius
|
||||
&DataTree::Leaf { type_name, contents } if type_name == "Radius" => {
|
||||
if let IResult::Done(_, radius) = ws_f32(contents.as_bytes()) {
|
||||
radii.push(radius);
|
||||
} else {
|
||||
// Found radius, but its contents is not in the right format
|
||||
return Err(PsyParseError::UnknownError);
|
||||
}
|
||||
}
|
||||
|
||||
// Direction
|
||||
&DataTree::Leaf { type_name, contents } if type_name == "Direction" => {
|
||||
if let IResult::Done(_, direction) =
|
||||
closure!(tuple!(ws_f32, ws_f32, ws_f32))(contents.as_bytes()) {
|
||||
directions.push(Vector::new(direction.0, direction.1, direction.2));
|
||||
} else {
|
||||
// Found color, but its contents is not in the right format
|
||||
return Err(PsyParseError::UnknownError);
|
||||
}
|
||||
}
|
||||
|
||||
// Color
|
||||
&DataTree::Leaf { type_name, contents } if type_name == "Color" => {
|
||||
if let IResult::Done(_, color) =
|
||||
closure!(tuple!(ws_f32, ws_f32, ws_f32))(contents.as_bytes()) {
|
||||
// TODO: handle color space conversions properly.
|
||||
// Probably will need a special color type with its
|
||||
// own parser...?
|
||||
colors.push(XYZ::from_tuple(rec709e_to_xyz(color)));
|
||||
} else {
|
||||
// Found color, but its contents is not in the right format
|
||||
return Err(PsyParseError::UnknownError);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(DistantDiskLight::new(radii, directions, colors));
|
||||
} else {
|
||||
return Err(PsyParseError::UnknownError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn parse_sphere_light(tree: &DataTree) -> Result<SphereLight, PsyParseError> {
|
||||
if let &DataTree::Internal { ref children, .. } = tree {
|
||||
let mut radii = Vec::new();
|
||||
|
@ -32,9 +88,8 @@ pub fn parse_sphere_light(tree: &DataTree) -> Result<SphereLight, PsyParseError>
|
|||
|
||||
// Color
|
||||
&DataTree::Leaf { type_name, contents } if type_name == "Color" => {
|
||||
if let IResult::Done(_, color) = closure!(tuple!(ws_f32,
|
||||
ws_f32,
|
||||
ws_f32))(contents.as_bytes()) {
|
||||
if let IResult::Done(_, color) =
|
||||
closure!(tuple!(ws_f32, ws_f32, ws_f32))(contents.as_bytes()) {
|
||||
// TODO: handle color space conversions properly.
|
||||
// Probably will need a special color type with its
|
||||
// own parser...?
|
||||
|
@ -76,9 +131,8 @@ pub fn parse_rectangle_light(tree: &DataTree) -> Result<RectangleLight, PsyParse
|
|||
|
||||
// Color
|
||||
&DataTree::Leaf { type_name, contents } if type_name == "Color" => {
|
||||
if let IResult::Done(_, color) = closure!(tuple!(ws_f32,
|
||||
ws_f32,
|
||||
ws_f32))(contents.as_bytes()) {
|
||||
if let IResult::Done(_, color) =
|
||||
closure!(tuple!(ws_f32, ws_f32, ws_f32))(contents.as_bytes()) {
|
||||
// TODO: handle color space conversions properly.
|
||||
// Probably will need a special color type with its
|
||||
// own parser...?
|
||||
|
|
|
@ -35,9 +35,8 @@ pub fn parse_mesh_surface(tree: &DataTree) -> Result<TriangleMesh, PsyParseError
|
|||
|
||||
// Collect verts for this time sample
|
||||
let mut vert_count = 0;
|
||||
while let IResult::Done(remaining, vert) = closure!(tuple!(ws_f32,
|
||||
ws_f32,
|
||||
ws_f32))(raw_text) {
|
||||
while let IResult::Done(remaining, vert) =
|
||||
closure!(tuple!(ws_f32, ws_f32, ws_f32))(raw_text) {
|
||||
raw_text = remaining;
|
||||
|
||||
verts.push(Point::new(vert.0, vert.1, vert.2));
|
||||
|
|
|
@ -113,9 +113,10 @@ impl Renderer {
|
|||
(halton::sample(0, offset + si as u32),
|
||||
halton::sample(1, offset + si as u32)),
|
||||
halton::sample(2, offset + si as u32),
|
||||
map_0_1_to_wavelength(
|
||||
halton::sample(3, offset + si as u32)
|
||||
),
|
||||
map_0_1_to_wavelength(halton::sample(3,
|
||||
offset +
|
||||
si as
|
||||
u32)),
|
||||
offset + si as u32);
|
||||
paths.push(path);
|
||||
rays.push(ray);
|
||||
|
@ -297,7 +298,8 @@ impl LightPath {
|
|||
let light_n = self.next_lds_samp();
|
||||
let light_uvw = (self.next_lds_samp(), self.next_lds_samp(), self.next_lds_samp());
|
||||
xform_stack.clear();
|
||||
if let Some((light_color, shadow_vec, light_pdf, light_sel_pdf)) = scene.root
|
||||
if let Some((light_color, shadow_vec, light_pdf, light_sel_pdf)) =
|
||||
scene.root
|
||||
.sample_lights(xform_stack,
|
||||
light_n,
|
||||
light_uvw,
|
||||
|
@ -327,7 +329,7 @@ impl LightPath {
|
|||
}
|
||||
} else {
|
||||
// Didn't hit anything, so background color
|
||||
self.color += scene.background_color.to_spectral_sample(self.wavelength) *
|
||||
self.color += scene.world.background_color.to_spectral_sample(self.wavelength) *
|
||||
self.light_attenuation;
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use assembly::Assembly;
|
||||
use camera::Camera;
|
||||
use color::XYZ;
|
||||
use world::World;
|
||||
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Scene {
|
||||
pub name: Option<String>,
|
||||
pub background_color: XYZ,
|
||||
pub camera: Camera,
|
||||
pub world: World,
|
||||
pub root: Assembly,
|
||||
}
|
||||
|
|
8
src/world.rs
Normal file
8
src/world.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use color::XYZ;
|
||||
use light::WorldLightSource;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct World {
|
||||
pub background_color: XYZ,
|
||||
pub lights: Vec<Box<WorldLightSource>>,
|
||||
}
|
Loading…
Reference in New Issue
Block a user