Skip to content

Instantly share code, notes, and snippets.

@phrozen
Last active February 6, 2017 20:15
Show Gist options
  • Save phrozen/58c572d29846d3373e23590b66d35cc3 to your computer and use it in GitHub Desktop.
Save phrozen/58c572d29846d3373e23590b66d35cc3 to your computer and use it in GitHub Desktop.
# Proyecto 1
# Ray tracer básico
# Guillermo Estrada
# Rene Garcia
# Edgar López
# Luis Ledezma
# Daniel Castillo
#
# Constantes utilizadas dentro del programa
MAX_DISTANCE = 9999999999
PI_OVER_180 = 0.017453292
#SMALL = 0.000000001
# Práctica 1
# Definimos nuestra clase Vector3 y sus métodos
class Vector3
# Se definen x, y, z como de solo lectura publicamente en la clase
attr_accessor :x, :y, :z
# Constructor de la clase, se inicializa Vector3.new(x, y, z)
# los valores predeterminados son 0.0 -> Vector3.new()
def initialize(x = 0.0, y = 0.0, z = 0.0)
@x, @y, @z = x, y, z
end
# Producto punto
def dot(v)
return @x*v.x + @y*v.y + @z*v.z #escalar
end
# Producto cruz
def cross(v)
r = Vector3.new()
r.x = v.y*@z - v.z*@y
r.y = v.z*@x - v.x*@z
r.z = v.x*@y - v.y*@z
return r
end
# Módulo de un vector
def module()
# La raiz cuadrada 'sqrt' esta en la libreria 'Math'
return Math.sqrt(@x*@x + @y*@y + @z*@z)
end
# Normalizar el vector
# En Ruby es común que los metodos que modifican el objeto
# terminen con ! en su nombre
def normalize!()
m = self.module()
# Dividimos cada componente del vector entre el modulo
# si este es diferente de 0. (x /= m) --> (@x = @x/m)
if m != 0.0
@x /= m; @y /= m; @z /= m;
end
return self
end
# Operator overloading, para poder sumar, restar o multiplicar
# vectores utilizando +, -, *
# Suma de Vectores
def +(v)
return Vector3.new(@x+v.x, @y+v.y, @z+v.z)
end
# Resta de vectores
def -(v)
return Vector3.new(@x-v.x, @y-v.y, @z-v.z)
end
# Multiplicación por escalar
def *(s)
return Vector3.new(@x*s, @y*s, @z*s)
end
# to_s opcional para debugging imprime vector en formato texto
#def to_s
# return "[#{@x}, #{@y}, #{@z}]"
#end
end
# Práctica 2
# Definimos nuestra clase Color que incluye las componentes RGB
# que forman un pixel. En nuestro caso de uso, las componentes
# estan en el rango [0.0, 1.0] para poder hacer operaciones compatibles
# con vectores. Para generar la imagen deberemos transformarlas
# al rango [0, 255] que es 8 bits por componente.
class Color
# Definimos acceso publico de solo lectura para las componentes
# RGB que son los canales de color Red Green Blue
attr_reader :r, :g, :b
# Constructor de la clase, se inicializa Color.new(r, g, b)
# los valores predeterminados son 0.0 -> Color.new() color negro
def initialize(r = 0.0, g = 0.0, b = 0.0)
@r, @g, @b = r, g, b
end
# Operator overloading, para poder sumar o multiplicar
# colores utilizando +, *
# Suma de colores, se suman cada componente por separado
# devolvemos un nuevo color sin modificar el original
def +(c)
return Color.new(@r + c.r, @g + c.g, @b + c.b)
end
# Multiplicación de color por escalar (factor), se multiplica cada
# componente por el valor del escalar y devolvemos un nuevo color.
def *(f)
return Color.new(@r *f, @g * f, @b * f)
end
# Sobre escribimos el método to_s (to string) de Ruby, que se usa
# para especificar como convertir el objeto a texto. Aquí es donde
# convertiremos cada color del rango [0.0, 1.0] al rango [0, 255]
# y lo devolveremos en cadena de texto para su escritura en el
# formato PPM.
def to_s()
# Multiplicaremos cada componente por 255 y acotaremos el valor final
# para que se este dentro del rango, sólo regresaremos la parte entera
# del valor final (%d) en cadena de texto.
return "%d %d %d" % [ [[255.0, @r*255.0].min, 0.0].max.to_i,
[[255.0, @g*255.0].min, 0.0].max.to_i,
[[255.0, @b*255.0].min, 0.0].max.to_i]
end
end
# Definimos nuestra clase Image que contendra los datos de todos los pixeles
# (colores) de nuestra imagen y tiene un método para guardarse en formato PPM.
class Image
# Acceso de lectura y escritura a todas las propiedades.
attr_accessor :data, :width, :height
# El constructor recibe ancho y alto de la imagen e inicializa un arreglo de
# tamaño ancho*alto para almacenar la totalidad de los pixeles de la imagen.
# Cada campo de nuestro arreglo será un objeto de la clase Color.
# Guardamos los valores de ancho y alto pues nos serviran para escribir la
# imagen final en PPM.
def initialize(width, height)
@width, @height = width, height
@data = Array.new()
end
# Este método recibe una ruta/nombre de archivo y crea una imágen en formato
# PPM en la ruta indicada.
def to_PPM(filename)
# Basado en la especificación del formato PPM
File.open(filename+'.ppm', 'w') do |f|
f.puts("P3") #componentes por pixel
f.puts("#{@width} #{@height}") #ancho y alto
f.puts("255") #valores posibles por componente
# Escribimos cada renglón de valores consecutivamente separados por espacios.
# R G B R G B R G B ... y al final un salto de línea
(0...@height).each do |y|
# Aqui iteramos sobre cada renglón tomamos el pedazo del arreglo con todos
# Los pixeles, mappeamos la funcion to_s a cada uno y los juntamos (join)
# con un espacio entre ellos.
f.puts(@data[y*@width...y*@width+@width].map{|c| c.to_s}.join(" "))
end
end
end
end
# La clase Material es donde almacenaremos todas las propiedades del objeto
class Material
# Acceso de sólo lectura a las propiedades
attr_reader :color, :diffuse, :specular, :shininess, :reflect, :trasmit, :ior
# Constructor de la clase, por el momento sólo usaremos color, posteriormente
# se usaran (shaders) las demás propiedades de la clase como son los componentes
# difuso, especular, brillo, reflexion, transmitividad, IOR (índice de refraccion).
# https://en.wikipedia.org/wiki/Phong_reflection_model
def initialize(color, diffuse=0.0, specular=0.0, shininess=0.0, reflect=0.0, transmit=0.0, ior=0.0)
@color = color
@diffuse = diffuse
@specular = specular
@shininess = shininess
@reflect = reflect
@transmit = transmit
@ior = ior
end
end
# De la Práctica 5
# Clase de un rayo, este tiene un origen y una dirección
# Además debe de poder almacenar la distancia de intersección
# cuando encuentra una colisión, así como la referencia del objeto
# con el que colisiono.
class Ray
attr_accessor :origin, :direction, :distance, :object
# Constructor de la clase, sólo definimos el origen y la
# dirección del rayo, la distancia inicial será la distancia
# máxima (horizonte) de nuestra escena, y el objeto de colisión
# es nulo cuando se crea el rayo.
# La constante MAX_DISTANCE se define previamente a nivel global
def initialize(origin, direction)
@origin = origin
@direction = direction
@distance = MAX_DISTANCE
@object = nil
end
end
# Práctica 3
# Definimos una clase genérica de objeto de la cual heredaran
# nuestras primitivas y todos los objetos virtuales.
# Todos los objetos deberan implementar el método 'intersect(ray)'
# con sus propias funciones de colisión, así como 'get_normal(point)'
# para calcular la normal en el punto de colisión.
# Nota: La función intersect es de la practica 5
class Object3D
attr_reader :type, :material
# Todos los objetos tienen un tipo ("plano", "esfera", etc...)
# Y un material, que correspondera a una referencia en la lista
# de materiales.
def initialize(type, material)
@type = type
@material = material
end
end
# Clase para la esfera nuestra primera primitiva.}
# La esfera como primitiva tiene una resolución infinita lo que significa
# que tiene infinitas caras, para definirla sólo requerimos de la posición
# o centro de la esfera y de su radio.
class Sphere < Object3D
attr_reader :center, :radius
# Constructor de la esfera, se requiere su material como en cualquier
# objeto, así como su centro (vector) y su radio (escalar)
def initialize(material, center, radius)
super('sphere', material)
@center = center
@radius = radius
end
# Sirve para calcular la normal en un punto de la esfera.
# Es el vector que va desde el centro hasta el punto, normalizado.
def get_normal(point)
return (point - @center).normalize!
end
# De la práctica 5
# La función de intersección del plano, revisa si hay colisiones entre
# 'ray' (el rayo que recibe) y el plano devuelve true o false dependiendo
# y en caso de ser true asigna valores al rayo.
def intersect(ray)
# Calculamos un vector que vaya desde el centro de nuestra esfera hasta
# el origen del rayo.
sph_ray = @center - ray.origin
# Con el producto punto podemos calcular la distancia que hay entre el
# rayo y la esfera. Si le restamos el radio de la esfera y esa distancia
# es mayor a la que ya tenia el rayo, la esfera no es el objeto más cercano
# así que podemos ignorarla.
v = sph_ray.dot(ray.direction)
return false if v - @radius > ray.distance
# Si es menor, resolvemos la ecuación quadrática para revisar colisión
# entre el rayo y el plano de corte de la esfera.
inter = @radius**2 + v**2 - sph_ray.x**2 - sph_ray.y**2 - sph_ray.z**2
# Si es negativa estamos viendo una cara interior de la esfera, ya sea
# la parte trasera de la esfera (segunda colisión) o bien la camara se
# encuentra dentro de la esfera.
return false if inter < 0.0
# Calculamos la distancia del rayo al plano en que el rayo colisiona
# con la esfera pues sabemos que hay intersección.
inter = v - Math.sqrt(inter)
# Si la distancia de intersección es negativa, la colisión se encuentra
# detrás de la cámara, así que la ignoramos. Del mismo modo si la
# distancia de colisión es mayor a una que el rayo haya encontrado antes
# la descartamos pues no es el objeto más cercano.
return false if inter < 0.0
return false if inter > ray.distance
# Si nada de lo anterior se cumple, la colisión es la más cercana hasta
# ahora, así que guardamos en el rayo la distancia y la referencia del
# objeto con el que choco.
ray.distance = inter
ray.object = self
# Devolvemos true pues encontramos una colisión válida.
return true
end
end
# Clase para el plano, segunda primitiva.
# En el caso de un Plano como primitiva se considera que el plano es
# infinito como se define matemáticamente. Lo único que se requiere
# para definirlo es su normal, y la distancia a la que el plano está
# del origen.
class Plane < Object3D
attr_reader :normal, :distance
# Inicializamos el plano con su material, su normal y su distancia.
def initialize(material, normal, distance)
super('plane', material)
@normal = normal
@distance = distance
end
# La normal de un plano en cualquier punto de este siempre es la normal
# del plano, así que simplemente devolvemos el vector almacenado.
def get_normal(point)
return @normal
end
# De la práctica 5
# La función de intersección del plano, revisa si hay colisiones entre
# 'ray' (el rayo que recibe) y el plano devuelve true o false dependiendo
# y en caso de ser true asigna valores al rayo.
def intersect(ray)
# Calculamos el producto punto entre la normal del plano y la dirección
# del rayo, un rayo siempre colisionará con un plano a menos de que sean
# paralelos.
v = @normal.dot(ray.direction)
# Si el producto punto es 0 significa que el ángulo entre el rayo y la
# normal del plano es de 90°. Lo cual implica que el rayo y el plano
# son paralelos y nunca colisionarán así que regresamos false.
return false if v == 0.0
# Calculamos la distancia de intersección entre el origen del rayo y la
# superficie del plano.
inter = -(@normal.dot(ray.origin) + @distance) / v
# Si la distancia de intersección es negativa, la colisión se encuentra
# detrás de la cámara, así que la ignoramos. Del mismo modo si la
# distancia de colisión es mayor a una que el rayo haya encontrado antes
# la descartamos pues no es el objeto más cercano.
return false if inter < 0.0
return false if inter > ray.distance
# Si nada de lo anterior se cumple, la colisión es la más cercana hasta
# ahora, así que guardamos en el rayo la distancia y la referencia del
# objeto con el que choco.
ray.distance = inter
ray.object = self
# Devolvemos true pues encontramos una colisión válida.
return true
end
end
# Práctica 4
# Clase para cargar la escena y guardar todos los objetos,
# luces, materiales, imagen, etc...
# Todo se carga desde un archivo de texto con la definición
# de la escena, es importante que esta clase vaya implementando
# los conceptos adicionales que se vayan utilizando.
class Scene
# Acceso de sólo lectura a las propiedades necesarias
attr_reader :image, :vhor, :vver, :vp, :cam_pos, :objects, :materials
# El constructor de la clase recibe el nombre del archivo de
# la escena y carga todo su contenido en las variables que
# serán utilizadas por el ray tracer
def initialize(filename)
@objects = Array.new()
@materials = Array.new()
#@lights = Array.new() # no implementado
# Valores predeterminados
@image = Image.new(320, 240) # Tamaño 320x240
# FoV (Field of view) o campo de visión es el ángulo de
# apertura de la cámara.
@fov = 60
# No implementado todavía
#@depth = 3 # profundidad de trazado
#@oversampling = 1 # Sobre muestreo (1 = no oversampling)
# Abrimos y leemos el archivo linea por linea
File.open(filename).each do |data|
# Separamos cada linea (split) por espacios (" ") y
# quitamos los espacios adicionales que pudieran
# haber en el archivo (strip)
line = data.split(" ").map{|i| i.strip}
# Seleccionamos el primer elemento line[0] y revisamos los
# posibles casos para cargarlo a memoria
case line[0]
when "#" # comentario en el archivo, lo ignoramos.
next
when "field_of_view" # campo de vision
@fov = line[1].to_f
#when "oversampling"
# @oversampling = line[1].to_i
#when "depth"
# @depth = line[1].to_i
when "image_size" # tamaño de la imagen
@image.width = line[1].to_i
@image.height = line[2].to_i
when "camera_position" # vector de posición de la camara
@cam_pos = parse_vector(line[1..3])
when "camera_look" # vector de hacia donde mira la camara
@cam_look = parse_vector(line[1..3])
when "camera_up" # vector de hacia donde es arriba para la camara
@cam_up = parse_vector(line[1..3])
when "material" # color(r, g, b), diffuse, specular, shininess, reflect, transmit, ior
@materials << parse_material(line)
when "plane" # material, normal(x, y, z), distance
@objects << Plane.new(line[1].to_i, parse_vector(line[2..4]), line[5].to_f)
when "sphere" # material, center(x, y, z), radius
@objects << Sphere.new(line[1].to_i, parse_vector(line[2..4]), line[5].to_f)
else # desconocido
next
end #end case
end # end File
# Continuamos calculando las variables una vez parseado el archivo
# Grid es la cuadricula a través de la cual trazaremos los rayos.
# La cuadricula se multiplica por el oversampling cuando este implementado
@grid_width = @image.width # * oversampling
@grid_height = @image.height # * oversampling
# Hacemos cálculos sobre los vectores de la cámara para trazar la piramide
# formada por la cuadrícula y el origen de la cámara.
@look = @cam_look - @cam_pos
# vhor y vver son los vectores que definen nuestra vista horizontal y vertical
# y los normalizamos, ambos se calculan usando producto cruz para obtener
# los vectores perpendiculares.
@vhor = @look.cross(@cam_up)
@vhor.normalize!
@vver = @look.cross(@vhor)
@vver.normalize!
# La constante PI_OVER_180 se define previamente 3.1415/180
fl = @grid_width / (2 * Math.tan((0.5 * @fov) * PI_OVER_180))
# Copiamos look para normalizarlo
vp = @look
vp.normalize!
vp.x = vp.x * fl - 0.5 * (@grid_width * @vhor.x + @grid_height * @vver.x)
vp.y = vp.y * fl - 0.5 * (@grid_width * @vhor.y + @grid_height * @vver.y)
vp.z = vp.z * fl - 0.5 * (@grid_width * @vhor.x + @grid_height * @vver.z)
@vp = vp
# stats
#puts "Objects #{@objects.length}"
#puts "Materials #{@materials.length}"
#puts "Camera #{@cam_pos}, #{@cam_look}, #{@cam_up}"
end # end initialize
# Funciones auxiliares para parsear el archivo devuelven los objetos convirtiendo
# los valores a flotantes para su contrucción
def parse_vector(line)
return Vector3.new(line[0].to_f, line[1].to_f, line[2].to_f)
end
def parse_color(line)
return Color.new(line[0].to_f, line[1].to_f, line[2].to_f)
end
def parse_material(line)
f = line[4..-1].map{|l| l.to_f}
return Material.new(parse_color(line[1..3]), f[0], f[1], f[2], f[3], f[4], f[5])
end
end
# Práctica 5
# Clase Raytracer, esta contendrá todo lo necesario para generar
# la imagen final a partir de la descripción de escena.
# Los métodos importantes son trace(ray) que se dedica a trazar rayos
# y render_pixel(x, y) que es la que renderiza el pixel en esas coordenadas
# de la imagen. La función render es el ciclo principal de renderizado.
class Raytracer
# El constructor es igual al de la escena pues sólo se encarga de
# construirla y empezar el proceso.
def initialize(filename)
# Guardamos el nombre para utilizarlo como nombre de la imagen
@filename = filename
@scene = Scene.new(filename)
end
# La función trace será la función que emita los rayos y revise las
# colisiones contra los objetos dela escena. Posteriormente se
# convertirá en una función recursiva para el trazado de rayos
# y recibirá el parametro 'depth' o la profundidad de trazado
# para poder detener el proceso. Devuelve el color del objeto.
def trace(ray)
# Iteramos sobre todos los objetos de la escena y probamos la
# intersección del rayo con cada uno, hay que notar que en caso
# de colisión el rayo tendrá la referencia al objeto más cercano
# y la distancia de colisión con el objeto.
# Proyecto 1
# Ray tracer básico
# Guillermo Estrada
# Rene Garcia
# Edgar López
# Luis Ledezma
# Daniel Castillo
#
# Constantes utilizadas dentro del programa
MAX_DISTANCE = 9999999999
PI_OVER_180 = 0.017453292
#SMALL = 0.000000001
# Práctica 1
# Definimos nuestra clase Vector3 y sus métodos
class Vector3
# Se definen x, y, z como de solo lectura publicamente en la clase
attr_accessor :x, :y, :z
# Constructor de la clase, se inicializa Vector3.new(x, y, z)
# los valores predeterminados son 0.0 -> Vector3.new()
def initialize(x = 0.0, y = 0.0, z = 0.0)
@x, @y, @z = x, y, z
end
# Producto punto
def dot(v)
return @x*v.x + @y*v.y + @z*v.z #escalar
end
# Producto cruz
def cross(v)
r = Vector3.new()
r.x = v.y*@z - v.z*@y
r.y = v.z*@x - v.x*@z
r.z = v.x*@y - v.y*@z
return r
end
# Módulo de un vector
def module()
# La raiz cuadrada 'sqrt' esta en la libreria 'Math'
return Math.sqrt(@x*@x + @y*@y + @z*@z)
end
# Normalizar el vector
# En Ruby es común que los metodos que modifican el objeto
# terminen con ! en su nombre
def normalize!()
m = self.module()
# Dividimos cada componente del vector entre el modulo
# si este es diferente de 0. (x /= m) --> (@x = @x/m)
if m != 0.0
@x /= m; @y /= m; @z /= m;
end
return self
end
# Operator overloading, para poder sumar, restar o multiplicar
# vectores utilizando +, -, *
# Suma de Vectores
def +(v)
return Vector3.new(@x+v.x, @y+v.y, @z+v.z)
end
# Resta de vectores
def -(v)
return Vector3.new(@x-v.x, @y-v.y, @z-v.z)
end
# Multiplicación por escalar
def *(s)
return Vector3.new(@x*s, @y*s, @z*s)
end
# to_s opcional para debugging imprime vector en formato texto
#def to_s
# return "[#{@x}, #{@y}, #{@z}]"
#end
end
# Práctica 2
# Definimos nuestra clase Color que incluye las componentes RGB
# que forman un pixel. En nuestro caso de uso, las componentes
# estan en el rango [0.0, 1.0] para poder hacer operaciones compatibles
# con vectores. Para generar la imagen deberemos transformarlas
# al rango [0, 255] que es 8 bits por componente.
class Color
# Definimos acceso publico de solo lectura para las componentes
# RGB que son los canales de color Red Green Blue
attr_reader :r, :g, :b
# Constructor de la clase, se inicializa Color.new(r, g, b)
# los valores predeterminados son 0.0 -> Color.new() color negro
def initialize(r = 0.0, g = 0.0, b = 0.0)
@r, @g, @b = r, g, b
end
# Operator overloading, para poder sumar o multiplicar
# colores utilizando +, *
# Suma de colores, se suman cada componente por separado
# devolvemos un nuevo color sin modificar el original
def +(c)
return Color.new(@r + c.r, @g + c.g, @b + c.b)
end
# Multiplicación de color por escalar (factor), se multiplica cada
# componente por el valor del escalar y devolvemos un nuevo color.
def *(f)
return Color.new(@r *f, @g * f, @b * f)
end
# Sobre escribimos el método to_s (to string) de Ruby, que se usa
# para especificar como convertir el objeto a texto. Aquí es donde
# convertiremos cada color del rango [0.0, 1.0] al rango [0, 255]
# y lo devolveremos en cadena de texto para su escritura en el
# formato PPM.
def to_s()
# Multiplicaremos cada componente por 255 y acotaremos el valor final
# para que se este dentro del rango, sólo regresaremos la parte entera
# del valor final (%d) en cadena de texto.
return "%d %d %d" % [ [[255.0, @r*255.0].min, 0.0].max.to_i,
[[255.0, @g*255.0].min, 0.0].max.to_i,
[[255.0, @b*255.0].min, 0.0].max.to_i]
end
end
# Definimos nuestra clase Image que contendra los datos de todos los pixeles
# (colores) de nuestra imagen y tiene un método para guardarse en formato PPM.
class Image
# Acceso de lectura y escritura a todas las propiedades.
attr_accessor :data, :width, :height
# El constructor recibe ancho y alto de la imagen e inicializa un arreglo de
# tamaño ancho*alto para almacenar la totalidad de los pixeles de la imagen.
# Cada campo de nuestro arreglo será un objeto de la clase Color.
# Guardamos los valores de ancho y alto pues nos serviran para escribir la
# imagen final en PPM.
def initialize(width, height)
@width, @height = width, height
@data = Array.new()
end
# Este método recibe una ruta/nombre de archivo y crea una imágen en formato
# PPM en la ruta indicada.
def to_PPM(filename)
# Basado en la especificación del formato PPM
File.open(filename+'.ppm', 'w') do |f|
f.puts("P3") #componentes por pixel
f.puts("#{@width} #{@height}") #ancho y alto
f.puts("255") #valores posibles por componente
# Escribimos cada renglón de valores consecutivamente separados por espacios.
# R G B R G B R G B ... y al final un salto de línea
(0...@height).each do |y|
# Aqui iteramos sobre cada renglón tomamos el pedazo del arreglo con todos
# Los pixeles, mappeamos la funcion to_s a cada uno y los juntamos (join)
# con un espacio entre ellos.
f.puts(@data[y*@width...y*@width+@width].map{|c| c.to_s}.join(" "))
end
end
end
end
# La clase Material es donde almacenaremos todas las propiedades del objeto
class Material
# Acceso de sólo lectura a las propiedades
attr_reader :color, :diffuse, :specular, :shininess, :reflect, :trasmit, :ior
# Constructor de la clase, por el momento sólo usaremos color, posteriormente
# se usaran (shaders) las demás propiedades de la clase como son los componentes
# difuso, especular, brillo, reflexion, transmitividad, IOR (índice de refraccion).
# https://en.wikipedia.org/wiki/Phong_reflection_model
def initialize(color, diffuse=0.0, specular=0.0, shininess=0.0, reflect=0.0, transmit=0.0, ior=0.0)
@color = color
@diffuse = diffuse
@specular = specular
@shininess = shininess
@reflect = reflect
@transmit = transmit
@ior = ior
end
end
# De la Práctica 5
# Clase de un rayo, este tiene un origen y una dirección
# Además debe de poder almacenar la distancia de intersección
# cuando encuentra una colisión, así como la referencia del objeto
# con el que colisiono.
class Ray
attr_accessor :origin, :direction, :distance, :object
# Constructor de la clase, sólo definimos el origen y la
# dirección del rayo, la distancia inicial será la distancia
# máxima (horizonte) de nuestra escena, y el objeto de colisión
# es nulo cuando se crea el rayo.
# La constante MAX_DISTANCE se define previamente a nivel global
def initialize(origin, direction)
@origin = origin
@direction = direction
@distance = MAX_DISTANCE
@object = nil
end
end
# Práctica 3
# Definimos una clase genérica de objeto de la cual heredaran
# nuestras primitivas y todos los objetos virtuales.
# Todos los objetos deberan implementar el método 'intersect(ray)'
# con sus propias funciones de colisión, así como 'get_normal(point)'
# para calcular la normal en el punto de colisión.
# Nota: La función intersect es de la practica 5
class Object3D
attr_reader :type, :material
# Todos los objetos tienen un tipo ("plano", "esfera", etc...)
# Y un material, que correspondera a una referencia en la lista
# de materiales.
def initialize(type, material)
@type = type
@material = material
end
end
# Clase para la esfera nuestra primera primitiva.}
# La esfera como primitiva tiene una resolución infinita lo que significa
# que tiene infinitas caras, para definirla sólo requerimos de la posición
# o centro de la esfera y de su radio.
class Sphere < Object3D
attr_reader :center, :radius
# Constructor de la esfera, se requiere su material como en cualquier
# objeto, así como su centro (vector) y su radio (escalar)
def initialize(material, center, radius)
super('sphere', material)
@center = center
@radius = radius
end
# Sirve para calcular la normal en un punto de la esfera.
# Es el vector que va desde el centro hasta el punto, normalizado.
def get_normal(point)
return (point - @center).normalize!
end
# De la práctica 5
# La función de intersección del plano, revisa si hay colisiones entre
# 'ray' (el rayo que recibe) y el plano devuelve true o false dependiendo
# y en caso de ser true asigna valores al rayo.
def intersect(ray)
# Calculamos un vector que vaya desde el centro de nuestra esfera hasta
# el origen del rayo.
sph_ray = @center - ray.origin
# Con el producto punto podemos calcular la distancia que hay entre el
# rayo y la esfera. Si le restamos el radio de la esfera y esa distancia
# es mayor a la que ya tenia el rayo, la esfera no es el objeto más cercano
# así que podemos ignorarla.
v = sph_ray.dot(ray.direction)
return false if v - @radius > ray.distance
# Si es menor, resolvemos la ecuación quadrática para revisar colisión
# entre el rayo y el plano de corte de la esfera.
inter = @radius**2 + v**2 - sph_ray.x**2 - sph_ray.y**2 - sph_ray.z**2
# Si es negativa estamos viendo una cara interior de la esfera, ya sea
# la parte trasera de la esfera (segunda colisión) o bien la camara se
# encuentra dentro de la esfera.
return false if inter < 0.0
# Calculamos la distancia del rayo al plano en que el rayo colisiona
# con la esfera pues sabemos que hay intersección.
inter = v - Math.sqrt(inter)
# Si la distancia de intersección es negativa, la colisión se encuentra
# detrás de la cámara, así que la ignoramos. Del mismo modo si la
# distancia de colisión es mayor a una que el rayo haya encontrado antes
# la descartamos pues no es el objeto más cercano.
return false if inter < 0.0
return false if inter > ray.distance
# Si nada de lo anterior se cumple, la colisión es la más cercana hasta
# ahora, así que guardamos en el rayo la distancia y la referencia del
# objeto con el que choco.
ray.distance = inter
ray.object = self
# Devolvemos true pues encontramos una colisión válida.
return true
end
end
# Clase para el plano, segunda primitiva.
# En el caso de un Plano como primitiva se considera que el plano es
# infinito como se define matemáticamente. Lo único que se requiere
# para definirlo es su normal, y la distancia a la que el plano está
# del origen.
class Plane < Object3D
attr_reader :normal, :distance
# Inicializamos el plano con su material, su normal y su distancia.
def initialize(material, normal, distance)
super('plane', material)
@normal = normal
@distance = distance
end
# La normal de un plano en cualquier punto de este siempre es la normal
# del plano, así que simplemente devolvemos el vector almacenado.
def get_normal(point)
return @normal
end
# De la práctica 5
# La función de intersección del plano, revisa si hay colisiones entre
# 'ray' (el rayo que recibe) y el plano devuelve true o false dependiendo
# y en caso de ser true asigna valores al rayo.
def intersect(ray)
# Calculamos el producto punto entre la normal del plano y la dirección
# del rayo, un rayo siempre colisionará con un plano a menos de que sean
# paralelos.
v = @normal.dot(ray.direction)
# Si el producto punto es 0 significa que el ángulo entre el rayo y la
# normal del plano es de 90°. Lo cual implica que el rayo y el plano
# son paralelos y nunca colisionarán así que regresamos false.
return false if v == 0.0
# Calculamos la distancia de intersección entre el origen del rayo y la
# superficie del plano.
inter = -(@normal.dot(ray.origin) + @distance) / v
# Si la distancia de intersección es negativa, la colisión se encuentra
# detrás de la cámara, así que la ignoramos. Del mismo modo si la
# distancia de colisión es mayor a una que el rayo haya encontrado antes
# la descartamos pues no es el objeto más cercano.
return false if inter < 0.0
return false if inter > ray.distance
# Si nada de lo anterior se cumple, la colisión es la más cercana hasta
# ahora, así que guardamos en el rayo la distancia y la referencia del
# objeto con el que choco.
ray.distance = inter
ray.object = self
# Devolvemos true pues encontramos una colisión válida.
return true
end
end
# Práctica 4
# Clase para cargar la escena y guardar todos los objetos,
# luces, materiales, imagen, etc...
# Todo se carga desde un archivo de texto con la definición
# de la escena, es importante que esta clase vaya implementando
# los conceptos adicionales que se vayan utilizando.
class Scene
# Acceso de sólo lectura a las propiedades necesarias
attr_reader :image, :vhor, :vver, :vp, :cam_pos, :objects, :materials
# El constructor de la clase recibe el nombre del archivo de
# la escena y carga todo su contenido en las variables que
# serán utilizadas por el ray tracer
def initialize(filename)
@objects = Array.new()
@materials = Array.new()
#@lights = Array.new() # no implementado
# Valores predeterminados
@image = Image.new(320, 240) # Tamaño 320x240
# FoV (Field of view) o campo de visión es el ángulo de
# apertura de la cámara.
@fov = 60
# No implementado todavía
#@depth = 3 # profundidad de trazado
#@oversampling = 1 # Sobre muestreo (1 = no oversampling)
# Abrimos y leemos el archivo linea por linea
File.open(filename).each do |data|
# Separamos cada linea (split) por espacios (" ") y
# quitamos los espacios adicionales que pudieran
# haber en el archivo (strip)
line = data.split(" ").map{|i| i.strip}
# Seleccionamos el primer elemento line[0] y revisamos los
# posibles casos para cargarlo a memoria
case line[0]
when "#" # comentario en el archivo, lo ignoramos.
next
when "field_of_view" # campo de vision
@fov = line[1].to_f
#when "oversampling"
# @oversampling = line[1].to_i
#when "depth"
# @depth = line[1].to_i
when "image_size" # tamaño de la imagen
@image.width = line[1].to_i
@image.height = line[2].to_i
when "camera_position" # vector de posición de la camara
@cam_pos = parse_vector(line[1..3])
when "camera_look" # vector de hacia donde mira la camara
@cam_look = parse_vector(line[1..3])
when "camera_up" # vector de hacia donde es arriba para la camara
@cam_up = parse_vector(line[1..3])
when "material" # color(r, g, b), diffuse, specular, shininess, reflect, transmit, ior
@materials << parse_material(line)
when "plane" # material, normal(x, y, z), distance
@objects << Plane.new(line[1].to_i, parse_vector(line[2..4]), line[5].to_f)
when "sphere" # material, center(x, y, z), radius
@objects << Sphere.new(line[1].to_i, parse_vector(line[2..4]), line[5].to_f)
else # desconocido
next
end #end case
end # end File
# Continuamos calculando las variables una vez parseado el archivo
# Grid es la cuadricula a través de la cual trazaremos los rayos.
# La cuadricula se multiplica por el oversampling cuando este implementado
@grid_width = @image.width # * oversampling
@grid_height = @image.height # * oversampling
# Hacemos cálculos sobre los vectores de la cámara para trazar la piramide
# formada por la cuadrícula y el origen de la cámara.
@look = @cam_look - @cam_pos
# vhor y vver son los vectores que definen nuestra vista horizontal y vertical
# y los normalizamos, ambos se calculan usando producto cruz para obtener
# los vectores perpendiculares.
@vhor = @look.cross(@cam_up)
@vhor.normalize!
@vver = @look.cross(@vhor)
@vver.normalize!
# La constante PI_OVER_180 se define previamente 3.1415/180
fl = @grid_width / (2 * Math.tan((0.5 * @fov) * PI_OVER_180))
# Copiamos look para normalizarlo
vp = @look
vp.normalize!
vp.x = vp.x * fl - 0.5 * (@grid_width * @vhor.x + @grid_height * @vver.x)
vp.y = vp.y * fl - 0.5 * (@grid_width * @vhor.y + @grid_height * @vver.y)
vp.z = vp.z * fl - 0.5 * (@grid_width * @vhor.x + @grid_height * @vver.z)
@vp = vp
# stats
#puts "Objects #{@objects.length}"
#puts "Materials #{@materials.length}"
#puts "Camera #{@cam_pos}, #{@cam_look}, #{@cam_up}"
end # end initialize
# Funciones auxiliares para parsear el archivo devuelven los objetos convirtiendo
# los valores a flotantes para su contrucción
def parse_vector(line)
return Vector3.new(line[0].to_f, line[1].to_f, line[2].to_f)
end
def parse_color(line)
return Color.new(line[0].to_f, line[1].to_f, line[2].to_f)
end
def parse_material(line)
f = line[4..-1].map{|l| l.to_f}
return Material.new(parse_color(line[1..3]), f[0], f[1], f[2], f[3], f[4], f[5])
end
end
# Práctica 5
# Clase Raytracer, esta contendrá todo lo necesario para generar
# la imagen final a partir de la descripción de escena.
# Los métodos importantes son trace(ray) que se dedica a trazar rayos
# y render_pixel(x, y) que es la que renderiza el pixel en esas coordenadas
# de la imagen. La función render es el ciclo principal de renderizado.
class Raytracer
# El constructor es igual al de la escena pues sólo se encarga de
# construirla y empezar el proceso.
def initialize(filename)
# Guardamos el nombre para utilizarlo como nombre de la imagen
@filename = filename
@scene = Scene.new(filename)
end
# La función trace será la función que emita los rayos y revise las
# colisiones contra los objetos dela escena. Posteriormente se
# convertirá en una función recursiva para el trazado de rayos
# y recibirá el parametro 'depth' o la profundidad de trazado
# para poder detener el proceso. Devuelve el color del objeto.
def trace(ray)
# Iteramos sobre todos los objetos de la escena y probamos la
# intersección del rayo con cada uno, hay que notar que en caso
# de colisión el rayo tendrá la referencia al objeto más cercano
# y la distancia de colisión con el objeto.
@scene.objects.each do |obj|
obj.intersect(ray)
end
# Revisamos si hubo interseccion para devolver el color del objeto
if !ray.object.nil?
# Nuestro objeto guarda el indice de la lista de materiales
# buscamos en la lista el color del material para devolverlo.
return @scene.materials[ray.object.material].color
end
# Si no hubo regresamos el color deafult (negro)
return Color.new()
end
# La función render_pixel(x, y) crea los rayos a partir de la cámara,
# manda llamar trace y almacena el color final en nuestra imagen en
# las coordenadas x, y
def render_pixel(x, y)
# Cálculamos la dirección del rayo interpolando en la cuadricula
# de la cámara.
direction = Vector3.new()
direction.x = x * @scene.vhor.x + y * @scene.vver.x + @scene.vp.x
direction.y = x * @scene.vhor.y + y * @scene.vver.y + @scene.vp.y
direction.z = x * @scene.vhor.z + y * @scene.vver.z + @scene.vp.z
direction.normalize!
# El origen del rayo siempre será la posición de la cámara.
ray = Ray.new(@scene.cam_pos, direction)
#puts direction
# Guardamos el color que trazamos en nuestra imagen
@scene.image.data[y*@scene.image.width+x] = trace(ray)
end
# Render es el ciclo principal, donde iteramos sobre toda la imagen
# y después guardamos el resultado.
def render()
([email protected]).each do |y|
print "Rendering line %.3d of %d\r" % [y+1, @scene.image.height]
([email protected]).each do |x|
# Rendereamos cada pixel
render_pixel(x, y)
end
end
# Guardamos la imagen
@scene.image.to_PPM(@filename)
end
end
# Inicializamos el raytracer con una escena y lo empezamos
filename = ARGV[0] || "scene.txt"
puts "Loading " + filename
rt = Raytracer.new(filename)
rt.render()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment