Difficulty |
3 |
Prerequisites |
def scene_at(now)
{
var camera = Cameras.perspective( [ "eye": pos(0,0,5),
"look_at": pos(0,0,0) ] )
var floor_material = Materials.uniform( [ "ambient": Colors.white() * 0.1,
"diffuse": Colors.white() * 0.8,
"reflectivity": 0.5 ] )
var left_wall_material = Materials.uniform( [ "ambient": Colors.red() * 0.1,
"diffuse": Colors.red() * 0.8 ] )
var right_wall_material = Materials.uniform( [ "ambient": Colors.green() * 0.1,
"diffuse": Colors.green() * 0.8 ] )
var back_wall_material = Materials.uniform( [ "ambient": Colors.blue() * 0.1,
"diffuse": Colors.blue() * 0.8 ] )
var ceiling_material = floor_material
var sphere_material = Materials.uniform( [ "ambient": RGB(0.1,0.1,0),
"diffuse": RGB(0.8,0.8,0),
"specular": Colors.white() * 0.8,
"specular_exponent": 10,
"light_filtering": RGB(0.8,0.8,0),
"transparency": 0.7,
"refractive_index": 2.5 ] )
var sphere_material2 = Materials.uniform( [ "ambient": RGB(0.1,0,0.1),
"diffuse": RGB(0.8,0,0.8) * 0.8,
"specular": Colors.white() * 0.8,
"specular_exponent": 10,
"light_filtering": RGB(0.8,0,0.8),
"transparency": 0.7,
"refractive_index": 2.5 ] )
var small_sphere_material = Materials.uniform( [ "ambient": Colors.white() * 0.1,
"diffuse": Colors.white() * 0.8,
"reflectivity": 0.8 ] )
var primitives = []
primitives.push_back( translate(vec(0,-2,0), decorate(floor_material, xz_plane())) )
primitives.push_back( translate(vec(0,2,0), decorate(ceiling_material, xz_plane())) )
primitives.push_back( translate(vec(-2,0,0), decorate(left_wall_material, yz_plane())) )
primitives.push_back( translate(vec(2,0,0), decorate(right_wall_material, yz_plane())) )
primitives.push_back( translate(vec(0,0,-2), decorate(back_wall_material, xy_plane())) )
var sphere_position = Animations.circular( [ "position": pos(0,0,1),
"around": pos(0,0,0),
"duration": seconds(5) ] )
var sphere_position2 = Animations.circular( [ "position": pos(0,1.1,0.5),
"around": pos(0,0,0),
"duration": seconds(5),
"axis": vec(0,-1,0) ] )
primitives.push_back( decorate( sphere_material, translate(sphere_position[now] - pos(0,0,0), scale(0.5, 0.5, 0.5, sphere())) ) )
primitives.push_back( decorate( sphere_material2, translate(sphere_position2[now] - pos(0,0,0), scale(0.25, 0.25, 0.25, sphere())) ) )
primitives.push_back( decorate( small_sphere_material, scale(0.25, 0.25, 0.25, sphere()) ) )
var root = union(primitives)
var lights = [ Lights.omnidirectional( pos(0,1.9,0), Colors.white() ) ]
create_scene(camera, root, lights)
}
var raytracer = Raytracers.v5()
var renderer = Renderers.standard( [ "width": 500,
"height": 500,
"sampler": Samplers.multijittered(2),
"ray_tracer": raytracer ] )
pipeline( scene_animation(scene_at, seconds(5)),
[ Pipeline.animation(30),
Pipeline.renderer(renderer),
Pipeline.studio() ] )
1. How It Works
The previous version of the ray tracer, objects were considered 100% opaque: they would block 100% of the light. However, we would like objects to be able to be translucent, i.e., that they can only block part of the light.
Below we have three disks that each let through certain colors. The shadows shows what light "survives".
White light (FFF
) travels from the top to the plane below.
-
If it only encounters the cyan disk (
0FF
), then red (F00
) gets blocked. -
If the light beam needs to get through both the cyan disk (
0FF
) and the magenta disk (F0F
), only blue (00F
) survives. -
If light encounters all three disks, all photons are blocked, resulting in a full black shadow.
Note
|
Admittedly, the disks themselves don’t look very translucent. This extension focuses solely on the shadows. Actually translucent looking disks will be possible in ray tracer v7. |
2. Material Properties
We need a way to specify how much light gets through an object.
For this, an extra MaterialProperties
field will be necessary.
In
|
Update the |
Update |
3. RayTracerV5
The previous ray tracer version operated in a very simple way: to render some position \(P\), process_light_ray
checks if there is any object between the light source’s position \(L\) and \(P\).
This was achieved by taking the light ray, which travels from \(L\) through \(P\) and find an intersection with the scene.
If there is an intersection where \(0 \leq t < 1\), there is something obstructing the light from getting from \(L\) to \(P\), causing us to disregard all photons originating from \(L\), creating a shadow.
We will add some nuance to this approach.
3.1. process_light_ray
An improved form of the process_light_ray
algorithm goes as follows:
def process_light_rays(scene, material_props, hit, eye_ray, light_ray):
hits = scene.find_all_hits(light_ray)
color_filter = white
for hit in [h for h in hits if 0 < h.t < 1]:
# Determine material properties at hit
matprops = ...
# Use color multiplication
color_filter *= matprops.light_filtering
# Make new light ray
surviving_light_ray = LightRay(original_light_ray.ray,
original_light_ray.color * color_filter)
# Delegate further work to v3 implementation
return RayTracerV3.process_light_ray(self, scene, matprops, hit, eye_ray, surviving_light_ray)
Notice how we rely on MaterialProperties::light_filtering
to determine how much light a primitive lets through.
-
If
light_filtering
equals black, no light gets through. This is the default. -
If
light_filtering
equals white, all light gets through. Such an object will never cast shadows. -
If
light_filtering
equals red, only red light will get through. Such an object will cast a red shadow.
|
3.2. Finishing Touches
Create a factory function |
Add bindings so that |
4. Evaluation
Reproduce the scene below. |