Published
Edited
May 29, 2021
2 forks
Importers
57 stars
Insert cell
Insert cell
Insert cell
Insert cell
{
let random = d3.randomInt(0,255)

let img = makeImage ()
let ctx = img.ctx
let d = img.data.data
let i = 0
for (let x = 0; x < w; x++){
for (let y = 0; y < h; y++) {
d [i++] = random()
d [i++] = random()
d [i++] = random()
d [i++] = 255
}
}
img.ctx.putImageData(img.data, 0, 0)
return img.canvas
}
Insert cell
Insert cell
// Computes the unit vector given a vector
unit_vector = function unit_vector(a) {
let len = Math.sqrt (dot(a,a))
return divVS(a, len)
}
Insert cell
// This takes as input a unit vector, and returns a 3-value vector representing the RGB color
background_color = function(v) {
let u = 0.5 * (1.0 + v[1])
return addVV (mulVS([0.7, 0.8, 0.9],u), mulVS([0.05, 0.05, 0.2], 1.0-u))
}
Insert cell
{
let img = makeImage ()
let ctx = img.ctx
let d = img.data.data
let ray_origin = [0,0,0]
let aspect = h/w
let window_depth = 2

let i = 0
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++){
let u = (x + 0.5) / w
let v = (y + 0.5) / h
let p = [2.0*u-1.0, aspect*(2.0*v-1.0), -window_depth]
let ray_direction = unit_vector(p)
let color = background_color (ray_direction)

putPixel (d, x, y, color)
}
}
img.ctx.putImageData(img.data, 0, 0)
return img.canvas
}
Insert cell
Insert cell
Insert cell
// This takes a function taht receives the buffer, x, y, ray_direction and ray_origin parameters
// the invoked method should call putPixel on it
renderImage = function (calculatePixel) {
let img = makeImage ()
let ctx = img.ctx
let d = img.data.data
let ray_origin = [0,0,0]
let aspect = h/w
let window_depth = 2

let i = 0
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++){
let u = (x + 0.5) / w
let v = (y + 0.5) / h
let p = [2.0*u-1.0, aspect*(2.0*v-1.0), -window_depth]
let ray_direction = unit_vector(p)

let color = calculatePixel (ray_origin, ray_direction)
putPixel (d, x, y, color)
}
}
img.ctx.putImageData(img.data, 0, 0)
return img.canvas
}
Insert cell
hit_sphere = function (ray_origin, ray_direction, center, radius) {
let oc = subVV (ray_origin, center)
let qb = dot(oc,ray_direction)
let qc = dot(oc,oc)-radius*radius
let discriminant = qb*qb-qc
if (discriminant > 0) {
let t = (-qb - Math.sqrt(discriminant))
if (t > 0.00001) {
return t
}
t = (-qb + Math.sqrt(discriminant))
if (t > 0.0001) {
return t
}
}
return 1.0e9
}
Insert cell
{
let center = [0, 0, -10]
let radius = 1.0

return renderImage (function (ray_origin, ray_direction) {
let t = hit_sphere(ray_origin, ray_direction, center, radius)
if (t < 1.0e8) {
return ([1, 0, 0])
} else {
return background_color (ray_direction)
}
})
}
Insert cell
Insert cell
pseudo_color = function (v) {
return mulVS (addVV ([1,1,1], v), 0.5)
}
Insert cell
{
let center = [0, 0, -10]
let radius = 1.0

return renderImage (function (ray_origin, ray_direction) {
let t = hit_sphere(ray_origin, ray_direction, center, radius)
if (t < 1.0e8) {
let hit_point = addVV (ray_origin, mulVS (ray_direction, t))
let surface_normal = mulVS (subVV (hit_point, center), 1/radius)
return pseudo_color (surface_normal)
} else {
return background_color (ray_direction)
}
})
}
Insert cell
Insert cell
hit_array = function (ray_origin, ray_direction, centers, radii) {
let t_min = 9.0e8
let i_min = -1
let hit_something = false
for (var i in centers) {
let t = hit_sphere(ray_origin, ray_direction, centers[i], radii[i])
if (t >= 0.0001 && t < t_min) {
t_min = t
i_min = i
hit_something = true
}
}
return [hit_something, t_min, i_min]
}
Insert cell
big_radius = 10000.0

Insert cell
radii = [ 1.0, big_radius, 1.0, 1.0, -0.95 ]


Insert cell
// And the 4 vectors for the centers of the spheres
centers = [ [0.0, 3.0, -10.0], [0.0, -big_radius - 1, 0.0], [1.0, 1.0, -7.0], [-1.0, 0.0, -6.0]]

Insert cell
Insert cell
centers2 = [ [0.0, 3.0, -10.0], [0.0, -big_radius - 1, 0.0], [1.0, 1.0, -7.0], [-1.0, 0.0, -6.0], [1, 1, -7]]
Insert cell
{

return renderImage (function (ray_origin, ray_direction) {
var hit_something, t, i
[hit_something, t, i] = hit_array(ray_origin, ray_direction, centers, radii)
if (hit_something){
let hit_point = addVV (ray_origin, mulVS (ray_direction, t))
let surface_normal = mulVS (subVV (hit_point, centers[i]), 1.0/radii[i])
return pseudo_color(surface_normal)
} else {
return background_color (ray_direction)
}
})
}

Insert cell
Insert cell
reflect = function(v, n) {
return subVV (v, mulVS (mulVS (n, dot(v,n)), 2.0))
}
Insert cell
{
return renderImage (function (ray_origin, ray_direction) {
var hit_something, t, i
while (true) {
[hit_something, t, i] = hit_array(ray_origin, ray_direction, centers, radii)
if (hit_something){
ray_origin = addVV (ray_origin, mulVS (ray_direction, t))
let surface_normal = mulVS (subVV (ray_origin, centers[i]), 1.0/radii[i])
ray_direction = reflect (ray_direction, surface_normal)
} else {
return background_color (ray_direction)
}
}
})
}

Insert cell
md`**PROGRAM 6:** Now we add two things: 1) allow objects to emit light of a certain color, and 2) the specularly (mirror) reflecting spheres attenuate the light so that only the fraction "specular_reflectivity" (a potentially different number of each sphere) is passed on.`
Insert cell
emitted_colors = [ [5.0, 5.0, 5.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0, 0, 0] ]
Insert cell
specular_reflectivity = [ [0.0, 0.0, 0.0], [0.7, 0.7, 0.7], [0.7, 0.6, 0.5], [1.0, 1.0, 1.0], [0,0,0] ]
Insert cell
not_zero = function (c) {
return dot(c,c) > 1.0e-6
}
Insert cell
{
return renderImage (function (ray_origin, ray_direction) {
var hit_something, t, i
let multiplier = [1.0,1.0,1.0]
while (true) {
[hit_something, t, i] = hit_array(ray_origin, ray_direction, centers, radii)
if (hit_something){
if (not_zero (emitted_colors [i])) {
return mulVV (multiplier, emitted_colors [i])
}
ray_origin = addVV (ray_origin, mulVS (ray_direction, t))
let surface_normal = mulVS (subVV (ray_origin, centers[i]), 1.0/radii[i])
ray_direction = reflect (ray_direction, surface_normal)
multiplier = mulVV (multiplier, specular_reflectivity[i])
} else {
return mulVV (multiplier, background_color (ray_direction))
}
}
})
}
Insert cell
Insert cell
// v and normal are vectors, ni and nt are scalars
refract = function (v, normal, ni, nt) {
let eta = ni/nt
let c1 = -dot(v, normal)
let w = eta*c1
let c2m = (w-eta)*(w+eta)
if (c2m < -1.0) {
return [false, v]
} else {
return [true, addVV (mulVS (v, eta), mulVS (normal, (w-Math.sqrt(1.0+c2m))))]
}
}
Insert cell
refractive_indices = [ 1.0, 1.0, 1.5, 1.0, 1.5 ]
Insert cell
renderImage (function (ray_origin, ray_direction) {
var hit_something, t, i
let multiplier = [1.0,1.0,1.0]
while (true) {
[hit_something, t, i] = hit_array(ray_origin, ray_direction, centers, radii)
if (hit_something){
if (not_zero (emitted_colors [i])) {
return mulVV (multiplier, emitted_colors [i])
break
}
ray_origin = addVV (ray_origin, mulVS (ray_direction, t))
let surface_normal = mulVS (subVV (ray_origin, centers[i]), 1.0/radii[i])
if (refractive_indices [i] > 1.0) {
let cos_incident = dot(ray_direction,surface_normal)
var ni, nt
if (cos_incident < 0){
ni = 1.0
nt = refractive_indices[i]
} else {
ni = refractive_indices[i]
nt = 1.0
surface_normal = subVV ([0, 0, 0], surface_normal)
}
var can_refract, refracted_direction
[can_refract, refracted_direction] = refract(ray_direction, surface_normal, ni, nt)

if (can_refract) {
ray_direction = refracted_direction
} else {
ray_direction = reflect(ray_direction, surface_normal)
}
} else {
ray_direction = reflect (ray_direction, surface_normal)
multiplier = mulVV (multiplier, specular_reflectivity[i])
}
} else {
return mulVV (multiplier, background_color (ray_direction))
break
}
}
})
Insert cell
Insert cell
//
// The only difference here is that we are starting to use `centers2` instead of centers
renderImage (function (ray_origin, ray_direction) {
var hit_something, t, i
let multiplier = [1.0,1.0,1.0]
while (true) {
[hit_something, t, i] = hit_array(ray_origin, ray_direction, centers2, radii)
if (hit_something){
if (not_zero (emitted_colors [i])) {
return mulVV (multiplier, emitted_colors [i])
break
}
ray_origin = addVV (ray_origin, mulVS (ray_direction, t))
let surface_normal = mulVS (subVV (ray_origin, centers2[i]), 1.0/radii[i])
if (refractive_indices [i] > 1.0) {
let cos_incident = dot(ray_direction,surface_normal)
var ni, nt
if (cos_incident < 0){
ni = 1.0
nt = refractive_indices[i]
} else {
ni = refractive_indices[i]
nt = 1.0
surface_normal = subVV ([0, 0, 0], surface_normal)
}
var can_refract, refracted_direction
[can_refract, refracted_direction] = refract(ray_direction, surface_normal, ni, nt)

if (can_refract) {
ray_direction = refracted_direction
} else {
ray_direction = reflect(ray_direction, surface_normal)
}
} else {
ray_direction = reflect (ray_direction, surface_normal)
multiplier = mulVV (multiplier, specular_reflectivity[i])
}
} else {
return mulVV (multiplier, background_color (ray_direction))
break
}
}
})
Insert cell
md`**PROGRAM 9:** Here we add that light splits between reflective and refractive components, and the amount going in each component adds to 100% but the proporions varies with the angle of incidence givened by the "Fresnal Equations". This is commonly (pretty much always) approximated by the "Schlick Approximation" a polynomial of the cosine of the angle of incidence *on the air side*. To avoid two recursive calls to track both rays we do this in a Monte Carlo choice that chooses either a reflection or refraction but not both. This is noisy, but on average correct.`
Insert cell
glass_fresnel = function(c) {
let c5 = Math.pow (1-c, 5)
return 0.05 * (1-c5)+ 0.95 * c5
}
Insert cell
// The change here is in the introduction of cosine1/cosine2/cosine, the class to glass_fresnel, and the randomdetermination for refract or reflect
renderImage (function (ray_origin, ray_direction) {
var hit_something, t, i
let multiplier = [1.0,1.0,1.0]
while (true) {
[hit_something, t, i] = hit_array(ray_origin, ray_direction, centers2, radii)
if (hit_something){
if (not_zero (emitted_colors [i])) {
return mulVV (multiplier, emitted_colors [i])
break
}
ray_origin = addVV (ray_origin, mulVS (ray_direction, t))
let surface_normal = mulVS (subVV (ray_origin, centers2[i]), 1.0/radii[i])
if (refractive_indices [i] > 1.0) {
let cosine1 = dot(ray_direction,surface_normal)
var ni, nt
if (cosine1 < 0){
ni = 1.0
nt = refractive_indices[i]
} else {
ni = refractive_indices[i]
nt = 1.0
surface_normal = subVV ([0, 0, 0], surface_normal)
}
var can_refract, refracted_direction
[can_refract, refracted_direction] = refract(ray_direction, surface_normal, ni, nt)

if (can_refract) {
cosine1 = Math.abs(cosine1)
let cosine2 = Math.abs(dot(refracted_direction, surface_normal))
let cosine = Math.min (cosine1, cosine2)
let prob_reflect = glass_fresnel(cosine)
if (randomUniform() < prob_reflect) {
ray_direction = reflect(ray_direction, surface_normal)
} else {
ray_direction = refracted_direction
}
} else {
ray_direction = reflect(ray_direction, surface_normal)
}
} else {
ray_direction = reflect (ray_direction, surface_normal)
multiplier = mulVV (multiplier, specular_reflectivity[i])
}
} else {
return mulVV (multiplier, background_color (ray_direction))
break
}
}
})
Insert cell
Insert cell
md`For this, we are going to change the render function that we have been using, for one that can do multiple samples`

Insert cell
// This version takes a number of samples, and calls the callback with the accumulator vector
// Thanks to Paul Robello for providing a fix to this method, which I had ported incorrectly.
renderSamplesImage = function (num_samples, calculatePixel) {
let img = makeImage ()
let ctx = img.ctx
let d = img.data.data
let ray_origin = [0,0,0]
let aspect = h/w
let window_depth = 2

let i = 0
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++){
let accum = [0, 0, 0]
for (let s = 0; s < num_samples; s++) {
let u = (x + randomUniform()) / w
let v = (y + randomUniform()) / h
let p = [2.0*u-1.0, aspect*(2.0*v-1.0), -window_depth]
let ray_direction = unit_vector(p)
let color = background_color (ray_direction)
let res = calculatePixel (ray_origin, ray_direction, [0,0,0])
accum = addVV(res,accum);
}
putPixel (d, x, y, divVS (accum, num_samples))
}
}
img.ctx.putImageData(img.data, 0, 0)
return img.canvas
}
Insert cell
// The change here is in the introduction of cosine1/cosine2/cosine, the class to glass_fresnel, and the randomdetermination for refract or reflect
renderSamplesImage (2, function (ray_origin, ray_direction, accum) {
var hit_something, t, i
let multiplier = [1.0,1.0,1.0]
while (true) {
[hit_something, t, i] = hit_array(ray_origin, ray_direction, centers2, radii)
if (hit_something){
if (not_zero (emitted_colors [i])) {
accum = addVV (accum, emitted_colors [i])
break
}
ray_origin = addVV (ray_origin, mulVS (ray_direction, t))
let surface_normal = mulVS (subVV (ray_origin, centers2[i]), 1.0/radii[i])
if (refractive_indices [i] > 1.0) {
let cosine1 = dot(ray_direction,surface_normal)
var ni, nt
if (cosine1 < 0){
ni = 1.0
nt = refractive_indices[i]
} else {
ni = refractive_indices[i]
nt = 1.0
surface_normal = subVV ([0, 0, 0], surface_normal)
}
var can_refract, refracted_direction
[can_refract, refracted_direction] = refract(ray_direction, surface_normal, ni, nt)

if (can_refract) {
cosine1 = Math.abs(cosine1)
let cosine2 = Math.abs(dot(refracted_direction, surface_normal))
let cosine = Math.min (cosine1, cosine2)
let prob_reflect = glass_fresnel(cosine)
if (randomUniform() < prob_reflect) {
ray_direction = reflect(ray_direction, surface_normal)
} else {
ray_direction = refracted_direction
}
} else {
ray_direction = reflect(ray_direction, surface_normal)
}
} else {
ray_direction = reflect (ray_direction, surface_normal)
multiplier = mulVV (multiplier, specular_reflectivity[i])
}
} else {
accum = addVV (accum, mulVV (multiplier, background_color (ray_direction)))
break
}
}
return accum
})
Insert cell
Insert cell
random_in_sphere = function(c, rad) {
let a = randomUniform2()
let b = Math.sqrt(1.0-a*a)
let phi = randomUniform2pi ()
let R = rad*Math.pow(randomUniform(), 0.33333)
let x = R*b*Math.cos(phi)
let y = R*b*Math.sin(phi)
let z = R*a
return addVV (c, [x, y, z])
}
Insert cell
{
let refractive_indices = [1.0, 1.5]
let emitted_colors = [[3.0,3.0,3.0], [0.0,0.0,0.0]]
let centers = [[-1.0,0.0,-5.0], [1.0,0.0,-5.0]]
let radii = [1.0, 1.0]
let specular_reflectivity = [[0.0,0.0,0.0], [0.0,0.0,0.0]]

let num_small_spheres = 10
let small_radius = 0.3*Math.pow(1.0/num_small_spheres, 0.333)
for (var i = 0; i < num_small_spheres; i++) {
refractive_indices.push(1.5)
emitted_colors.push([0.0,0.0,0.0])
centers.push(random_in_sphere([1.0, 0.0, -5.0], 1.0-1.1*small_radius))
radii.push(-small_radius)
specular_reflectivity.push([0.0,0.0,0.0])
}
return renderSamplesImage (2, function (ray_origin, ray_direction, accum) {
var hit_something, t, i
let multiplier = [1.0,1.0,1.0]
for (var bounces = 0; bounces < 30; bounces++){
[hit_something, t, i] = hit_array(ray_origin, ray_direction, centers, radii)
if (hit_something){
if (not_zero (emitted_colors [i])) {
accum = addVV (accum, emitted_colors [i])
break
}
ray_origin = addVV (ray_origin, mulVS (ray_direction, t))
let surface_normal = mulVS (subVV (ray_origin, centers[i]), 1.0/radii[i])
if (refractive_indices [i] > 1.0) {
let cosine1 = dot(ray_direction,surface_normal)
var ni, nt
if (cosine1 < 0){
ni = 1.0
nt = refractive_indices[i]
} else {
ni = refractive_indices[i]
nt = 1.0
surface_normal = subVV ([0, 0, 0], surface_normal)
}
var can_refract, refracted_direction
[can_refract, refracted_direction] = refract(ray_direction, surface_normal, ni, nt)

if (can_refract) {
cosine1 = Math.abs(cosine1)
let cosine2 = Math.abs(dot(refracted_direction, surface_normal))
let cosine = Math.min (cosine1, cosine2)
let prob_reflect = glass_fresnel(cosine)
if (randomUniform() < prob_reflect) {
ray_direction = reflect(ray_direction, surface_normal)
} else {
ray_direction = refracted_direction
}
} else {
ray_direction = reflect(ray_direction, surface_normal)
}
} else {
ray_direction = reflect (ray_direction, surface_normal)
multiplier = mulVV (multiplier, specular_reflectivity[i])
}
} else {
accum = addVV (accum, mulVV (multiplier, background_color (ray_direction)))
break
}
}
return accum
})
}

Insert cell
Insert cell
random_on_sphere = function(c, rad) {
let a = randomUniform2()
let b = Math.sqrt(1.0-a*a)
let phi = randomUniform2pi ()
let x = rad*b*Math.cos(phi)
let y = rad*b*Math.sin(phi)
let z = rad*a
return addVV (c, [x, y, z])
}
Insert cell
{
let refractive_indices = [1.0, 1.0]
let emitted_colors = [[3.0,3.0,3.0], [0.0,0.0,0.0]]
let centers = [[-1.0,0.0,-5.0], [1.0,0.0,-5.0]]
let radii = [1.0, 1.0]
let specular_reflectivity = [[0.0,0.0,0.0], [0.0,0.0,0.0]]
let diffuse_reflectivity = [[0,0,0], [0.9, 0.8, 0.5]]
return renderSamplesImage (2, function (ray_origin, ray_direction, accum) {
var hit_something, t, i
let multiplier = [1.0,1.0,1.0]
for (var bounces = 0; bounces < 30; bounces++){
[hit_something, t, i] = hit_array(ray_origin, ray_direction, centers, radii)
if (hit_something){
if (not_zero (emitted_colors [i])) {
accum = addVV (accum, emitted_colors [i])
break
}
ray_origin = addVV (ray_origin, mulVS (ray_direction, t))
let surface_normal = mulVS (subVV (ray_origin, centers[i]), 1.0/radii[i])
if (not_zero(diffuse_reflectivity[i])) {
let target = random_on_sphere(addVV (ray_origin, surface_normal), 0.99)
ray_direction = unit_vector(subVV (target, ray_origin))
multiplier = mulVV (multiplier, diffuse_reflectivity[i])
} else if (refractive_indices [i] > 1.0) {
let cosine1 = dot(ray_direction,surface_normal)
var ni, nt
if (cosine1 < 0){
ni = 1.0
nt = refractive_indices[i]
} else {
ni = refractive_indices[i]
nt = 1.0
surface_normal = subVV ([0, 0, 0], surface_normal)
}
var can_refract, refracted_direction
[can_refract, refracted_direction] = refract(ray_direction, surface_normal, ni, nt)

if (can_refract) {
cosine1 = Math.abs(cosine1)
let cosine2 = Math.abs(dot(refracted_direction, surface_normal))
let cosine = Math.min (cosine1, cosine2)
let prob_reflect = glass_fresnel(cosine)
if (randomUniform() < prob_reflect) {
ray_direction = reflect(ray_direction, surface_normal)
} else {
ray_direction = refracted_direction
}
} else {
ray_direction = reflect(ray_direction, surface_normal)
}
} else {
ray_direction = reflect (ray_direction, surface_normal)
multiplier = mulVV (multiplier, specular_reflectivity[i])
}
} else {
accum = addVV (accum, mulVV (multiplier, background_color (ray_direction)))
break
}
}
return accum
})
}


Insert cell
md`**PROGRAM 13**. Now we add a bunch of random spheres for our final program`
Insert cell
Insert cell
makeImage = function makeImage(){
// Create a Canvas element
let canvas = document.createElement('canvas');

// Size the canvas to the element
canvas.width = w;
canvas.height = h;

// Draw image onto the canvas
let ctx = canvas.getContext('2d');
// Finally, get the image data as a Uint8ClampedArray
// ('data' is an array of RGBA pixel values for each pixel)
const raw = ctx.getImageData(0, 0, w, h);
// Let us get a normal array back to maniputate
const flatArray = Array.from(raw.data);
return { canvas: canvas, ctx: ctx, data: raw }

}
Insert cell
// Computes the dot product of two 3D vectors
dot = function dot(a,b) {
return a [0]*b[0]+a[1]*b[1]+a[2]*b[2]
}

Insert cell
// Adds two 3d vectors
addVV = function (a,b) {
return [a[0]+b[0],a[1]+b[1],a[2]+b[2]]
}
Insert cell
// Subtracts the second 3d vector from the first
subVV = function (a,b){
return [a[0]-b[0],a[1]-b[1],a[2]-b[2]]
}
Insert cell
// Divides a 3D vector by a scalar value
divVS = function divVS(vec,scalar) {
return [vec[0]/scalar,vec[1]/scalar, vec[2]/scalar]
}
Insert cell
// Multiplies a 3D vector by a scalar
mulVS = function mulVS (vec, scalar) {
return [vec[0]*scalar, vec[1]*scalar, vec[2]*scalar]
}
Insert cell
// Multiplies two 3D vectors
mulVV = function (a, b) {
return [a[0]*b[0],a[1]*b[1],a[2]*b[2]]
}
Insert cell
// Draws a pixel on the buffer `bitmap` that is of size `w`x`h` at position `x`, `y`
// with the color specified as RGB values in the range 0..1
putPixel = function(bitmap,x,y,color) {
let p = ((h-y)*w+x)*4
bitmap [p++] = color [0]*255
bitmap [p++] = color [1]*255
bitmap [p++] = color [2]*255
bitmap [p] = 255
}
Insert cell
w = 512

Insert cell
h = 384
Insert cell
d3 = require("d3-random@>=2.2")
Insert cell
randomUniform = d3.randomUniform(0.0,1.0)
Insert cell
randomUniform2 = d3.randomUniform(-1.0,1.0)
Insert cell
randomUniform2pi = d3.randomUniform(0, 2*3.141592)
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more