Skip to content

0tt049/ExperimentsIn

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Experiments In

Experiments In

Experiments in Ray Tracing

A port of the book Ray Tracing in One Weekend by Peter Shirley to the Ruby programming language.

Introduction

As far as I know there are two ways one could programmatically generate an image. By having the program, while it is running, produce a window and, in it, the image. Or, by having the program produce the image and save it in a image file (JPEG, PNG, TIFF, etc…), which can then be opened by an image viewer. This experiment deals with the latter.

I’ll be following the book Ray Tracing in One Weekend, it describes the processes of writing a Ray Tracer using C++. I am not a C++ programmer though, and do not think that simply rewriting code shown in a book as being a very effective learning methodology. Therefore I’ll try to re-implement this Ray Tracer using Ruby.

The PPM image format

There are many image formats available, most of them are very complex, PPM will be the format used throughout this experiment. Maybe I’ll change it later.

The code bellow (PPM image file) would produce a 3x2 pixel image of red, green, blue, yellow, white, and black colors. I commented a few lines in the snippet bellow to describe the PPM image format.

P3  # Set colors to ASCII

3 2 # Set file to 3 columns and 2 rows
255 # Set max color to 255

# Image content

255   0   0       0 255   0       0   0 255
255 255   0     255 255 255       0   0   0

First gradient in Ruby

Let’s begin with a simple image gradient. First we set the image size, then we store both dimensions as two arrays that will be iterated on later.

image_width = 256
image_height = 256
hor = (0..(image_width - 1)).to_a
ver = hor.reverse

$stdout is our door to the outside world, and whatever gets out of this file I’ll then pipe to a new file. And again, the snippet bellow sets up our image file as per PPM requirements.

$stdout << "P3\n" << image_width << " " << image_height << "\n255\n"

And now for the fun part. Think of this as we were creating a 2D array of 3-tuple. Line by line, and pixel by pixel. Each pixel is described by 3 integers, each of those is a RGB value. Notice that we first get a floating number between 0.0 and 1.0, that has to be normalized back to an integer between 0 and 255. And lastly $stdout spits out our 3 values, go down a line, and iterate again.

ver.each do |ypx|
  hor.each do |xpx|
    r = xpx.to_f / (image_width - 1)
    g = ypx.to_f / (image_height - 1)
    b = 0.25

    ir = (r * 255.999).to_i
    ig = (g * 255.999).to_i
    ib = (b * 255.999).to_i

    $stdout << ir << " " << ig << " " << ib << "\n"
  end
end

The creating-image-file part gets dealt by the command line. At least for now.

$ ruby raytracer.rb > gradient.ppm

And since most places don’t know what to do with an PPM image file, we can must also convert it to whatever file format you prefer. I’m converting it to JPEG using ImageMagick.

$ convert gradient.ppm gradient.jpg

./Raytracing/gradient.jpg

A bit of refactoring

This section of the book shows the creation of a vec3 class that could represent a coordinate or a RGB value. I’m sticking with Ruby’s Vector class for now. I’m using this class to implement a write_color helper function.

require 'matrix'

def color(red, green, blue)
  Vector.elements([red, green, blue])
end

def write_color(color, out = $stdout)
  out << (color[0] * 255.999).to_i << " " <<
    (color[1] * 255.999).to_i << " " <<
    (color[2] * 255.999).to_i << "\n"
end

And now we can use write_color inside our Ray Tracer.

require_relative 'color'

image_width = 256
image_height = 256
hor = (0..(image_width - 1)).to_a
ver = hor.reverse

$stdout << "P3\n" << image_width << " " << image_height << "\n255\n"

ver.each do |ypx|
  hor.each do |xpx|
    color_arr = [
      (xpx.to_f / (image_width - 1)),
      (ypx.to_f / (image_height - 1)),
      0.25
    ]
    pixel_color = Vector.elements(color_arr)
    write_color(pixel_color)
  end
end

Rays, Camera and Background

Ray

A ray is a vector, it has an origin and a direction. It can be described by the mathematical function: $P(t) = A + tb$ Raytracing/drawing.png

require 'matrix'
require_relative 'vector'

class Ray
  attr_reader :origin, :direction

  def initialize(origin, direction)
    @origin = origin
    @direction = direction
  end

  def at(time)
    @origin + (time * @direction)
  end
end

Sending Rays

  1. Calculate the ray from the eye to the pixel_color.
  2. Determine which objects the ray intersects.
  3. Compute a color for that intersection point.
require_relative 'color'
require_relative 'ray'
require 'matrix'

def ray_color(ray)
  unit_direction = unit_vector(ray.direction)
  t = (0.5 * unit_direction[1]) + 1.0
  ((1.0 - t) * color(1.0, 1.0, 1.0)) + (t * color(0.5, 0.7, 1.0))
end

# Image
aspect_ratio = 16.0 / 9.0
image_width = 400
image_height = (image_width / aspect_ratio).to_i
vert = (0..image_height).to_a
vert.reverse!

# Camera
viewport_height = 2.0.to_i
viewport_width = (aspect_ratio * viewport_height).to_i
focal_length = 1.0

origin = Vector.elements([0, 0, 0])
vec3 = Vector.elements([0, 0, focal_length])
horizontal = Vector.elements([viewport_width, 0, 0])
vertical = Vector.elements([0, viewport_height, 0])
lower_left_corner = origin - (horizontal / 2) - (vertical / 2) - vec3

$stdout << "P3\n" << image_width << " " << image_height << "\n255\n"

vert.each do |ypx|
  image_width.times do |xpx|
    u = xpx.to_f / (image_width - 1)
    v = ypx.to_f / (image_height - 1)
    r = Ray.new(origin, lower_left_corner + (u * horizontal) + (v * vertical) - origin)
    pixel_color = ray_color(r)
    write_color(pixel_color)
  end
end

First we define a color for a ray that hits nothing, which is the same as defining a background. Raytracing/background.jpg

require_relative 'color'
require_relative 'ray'
require 'matrix'

def sphere_hit?(center, radius, ray)
  oc = ray.origin - center
  a = ray.direction.dot(ray.direction)
  b = 2.0 * oc.dot(ray.direction)
  c = oc.dot(oc) - (radius * radius)
  discriminant = (b * b) - (4 * a * c)
  discriminant.positive?
end

def ray_color(ray)
  center = Vector.elements([0, 0, -1])
  color(1, 0, 0) if sphere_hit?(center, 0.5, ray)
  unit_direction = unit_vector(ray.direction)
  t = (0.5 * unit_direction[1]) + 1.0
  ((1.0 - t) * color(1.0, 1.0, 1.0)) + (t * color(0.5, 0.7, 1.0))
end

# Image
aspect_ratio = 16.0 / 9.0
image_width = 400
image_height = (image_width / aspect_ratio).to_i
vert = (0..image_height).to_a
vert.reverse!

# Camera
viewport_height = 2.0.to_i
viewport_width = (aspect_ratio * viewport_height).to_i
focal_length = 1.0

origin = Vector.elements([0, 0, 0])
vec3 = Vector.elements([0, 0, focal_length])
horizontal = Vector.elements([viewport_width, 0, 0])
vertical = Vector.elements([0, viewport_height, 0])
lower_left_corner = origin - (horizontal / 2) - (vertical / 2) - vec3

$stdout << "P3\n" << image_width << " " << image_height << "\n255\n"

vert.each do |ypx|
  image_width.times do |xpx|
    u = xpx.to_f / (image_width - 1)
    v = ypx.to_f / (image_height - 1)
    r = Ray.new(origin, lower_left_corner +
                        (u * horizontal) + (v * vertical) -
                        origin)
    pixel_color = ray_color(r)
    write_color(pixel_color)
  end
end

About

My ongoing record of experiments I'm interested in

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published