Skip to content

Instantly share code, notes, and snippets.

@phrozen
Last active February 7, 2017 04:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save phrozen/6ba0c01884ee2fecc7019b589048bf25 to your computer and use it in GitHub Desktop.
Save phrozen/6ba0c01884ee2fecc7019b589048bf25 to your computer and use it in GitHub Desktop.
# Proyecto 2
# Ray Tracer Avanzado
# 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_accessor :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, :transmit, :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 6
# Clase para almacenar la información de las fuentes de luz
class Light
# Acceso de sólo lectura a las variables de clase que obtendremos de las
# fuentes de luz, su posición y color en el caso de las fuentes puntuales,
# y sólo el color en el caso de las fuentes de tipo ambiental
attr_reader :position, :color, :type
# El contructor de nuestra clase sólo almacena los datos necesarios
def initialize(type, position, color)
@type = type
@position = position
@color = color
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, :lights, :depth, :oversampling
# 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() # Práctica 6
# 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
@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)
when "light" # Práctica 6 - type, position(x, y, z), color(r, g, b)
@lights << Light.new(line[1], parse_vector(line[2..4]), parse_color(line[5..7]))
else # desconocido
next
end #end case
end # end File
# Auto calculo del vector up si no existe (experimental)
if @cam_up.nil?
@cam_up = @cam_look.cross(Vector3.new(0.0,0.0,1.0)).cross(@cam_look)
end
# 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.z + @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
# Práctica 6
# Método para calcular sombras, recibe el rayo generado a partir del
# punto de colisión y el objeto con el que colisionó. regresa la
# la atenuación de color [0.0. 1.0] o sombra en ese punto.
def calculate_shadow(ray, colision_object)
shadow = 1.0 # Significa que originalmente no hay sombra
@scene.objects.each do |obj|
# Si hay colisión y el objeto es distinto al actual.
if obj.intersect(ray) and obj != colision_object
# Mltiplicamos la sombra por la opacidad del objeto
# colisionado. Objectos traslucidos hacen menos
# sombra detrás de ellos.
shadow *= @scene.materials[obj.material].transmit
end
end
return shadow
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, depth)
# Creamos un color nuevo para ir almacenando el resultado
c = Color.new()
# 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
# ***** Proyecto 2 *****
# *** Práctica 6 *** - revisaremos las luces y calcularemos sombra.
# Primero extraemos el material del objeto colisionado
mat = @scene.materials[ray.object.material]
#c = mat.color # Practica 6
# Cálculamos el punto de intersección con el objeto
inter_point = ray.origin + ray.direction * ray.distance
# Por último la normal en el punto de colisión
inter_normal = ray.object.get_normal(inter_point)
# Calculamos un vector tenga sentido opuesto a la dirección del rayo
# (back origin) de regreso al origen y lo normalizamos
back_origin = ray.direction * -1.0
back_origin.normalize!
# Ahora iteramos sobre todas nuestras fuentes de luz
@scene.lights.each do |light|
# Si la luz es de tipo ambiental, simplemente sumamos
# el color de la luz a nuestro color resultado
if light.type == "ambient"
c += light.color
# Si la luz es de tipo puntual...
elsif light.type == "point"
# Caluclamos la el vector que va de la luz hacia el punto
# de intersección con nuestro objeto para ver si hay sombra
light_dir = light.position - inter_point
light_dir.normalize!
# Creamos un nuevo rayo que va de nuestro punrto de interseccion
# hacia la fuente de luz para revisar colisiones con otros objetos
light_ray = Ray.new(inter_point, light_dir)
# Y calculamos la sombra...
shadow = calculate_shadow(light_ray, ray.object)
#return c *= shadow # Practica 6
# *** Práctica 7 *** - Sombreado de superficies, componente difuso y especular
# Calculamos el producto punto entre la normal y la luz
nl = inter_normal.dot(light_dir)
# Si no son perpendiculares procedemos a calcular sus componentes
# difusa y especular. Si fueran perpendiculares quiere decir que
# la luz es tangente al punto de intersección y no añade al
# sombreado de la superficie en ese punto.
if nl > 0.0
if mat.diffuse > 0.0 #----- Componente difusa
# El color del componente difuso es igual al color de la luz
# multiplicado por el coeficiente difuso y por el coseno del
# ángulo entre la normal y la luz en el punto de intersección
# (producto punto)
diffuse_color = light.color * mat.diffuse * nl
# Despues agregamos el color del material y la sombra a cada
# componente RGB
diffuse_color.r *= mat.color.r * shadow
diffuse_color.g *= mat.color.g * shadow
diffuse_color.b *= mat.color.b * shadow
# Y lo sumamos a nuestro color resultado
c += diffuse_color
end # end diffuse
if mat.specular > 0.0 #----- Componente especular
# calcular el componente especular - Phong
# Primero calculamos el vector reflejado en la superficie
r = (inter_normal * 2 * nl) - light_dir
spec = back_origin.dot(r)
# Si el producto punto es 0.0 son perpendiculares lo que implica
# que el punto de colisión es tangente a la iluminación, lo ignoramos
if spec > 0.0
# Calculamos la componente especular con la dureza o brillo del material
spec = mat.specular * spec**mat.shininess
# Y creamos nuestro color especular agregando la sombra
specular_color = light.color * spec * shadow
# Sumamos este color a nuestro resultado final
c += specular_color
end #end spec
end # end specular
end # end Práctica 7 (nl)
end # end light if
end # end light loop
# *** Práctica 8 **** - Reflexión y Refracción
# A partir de aquí el algoritmo se vuelve recursivo y se reutiliza
# para trazar rayos en los puntos de colisión hasta que lleguemos
# a nuestro máximo valor de profundidad de trazado.
# Y un vector en sentido opuesto al rayo (regresa al origen)
if depth < @scene.depth
if mat.reflect > 0.0 #----- Reflexión
# Producto punto
t = back_origin.dot(inter_normal)
# Si es 0.0 los vectores son perpendiculares lo que implica que
# la reflexión es tangente a la superficie y no importa.
if t > 0.0
# Calculamos la dirección que deberá tener el rayo reflejado
dir_reflect = (inter_normal * 2 * t) - back_origin
# offset sirve para separar de manera insignificante (SMALL)
# el punto de colisión de la superficie del objeto para
# evitar que el rayo reflejado choque con su propia superficie
offset_inter = inter_point + dir_reflect * SMALL
# Creamos el nuevo rayo de reflexión a partir del punto de
# colisión y con la dirección del rayo reflejado en la superficie
reflection_ray = Ray.new(offset_inter, dir_reflect)
# Recursivamente trazamos el nuevo rayo de reflexión con nuestra
# misma función trace e incrementamos la profundidad de trazado.
# En este caso nuestro resultado final lo multiplicamos por el
# factor de reflexión del material.
c += trace(reflection_ray, depth+1.0) * mat.reflect
end # end t
end # end reflect
if mat.transmit > 0.0 #----- Refracción
# Calculamos el vector incidente
incident = inter_point - ray.origin
rn = inter_normal.dot(incident * -1.0)
incident.normalize!
if inter_normal.dot(incident) > 0.0
inter_normal = inter_normal * -1.0
rn = -rn
n1 = mat.ior
n2 = 1.0
else
n1 = 1.0
n2 = mat.ior
end # end incident
if n1 != 0.0 and n2 != 0.0
par_sqrt = Math.sqrt(1 -(n1*n1/n2*n2)*(1-rn*rn))
dir_refract = incident + (inter_normal*rn) * (n1/n2) - (inter_normal*par_sqrt)
offset_inter = inter_point + dir_refract * SMALL
refraction_ray = Ray.new(offset_inter, dir_refract)
c += trace(refraction_ray, depth+1.0) * mat.transmit
end
end # end transmit
end # end depth
end # end object loop
# Si no hubo regresamos el color deafult (negro)
return c
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)
# *** Práctica 9 *** - Oversampling
c = Color.new()
x *= @scene.oversampling
y *= @scene.oversampling
@scene.oversampling.times do
@scene.oversampling.times do
# 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)
c += trace(ray, 1.0)
y += 1
end # end j loop
x += 1
end # end i loop
sqr_os = @scene.oversampling * @scene.oversampling
c.r /= sqr_os
c.g /= sqr_os
c.b /= sqr_os
# Regresamos el color que trazamos en nuestra imagen
return c
end
# Render es el ciclo principal, donde iteramos sobre toda la imagen
# y después guardamos el resultado.
def render()
(0...@scene.image.height).each do |y|
print "Rendering line %.3d of %d\r" % [y+1, @scene.image.height]
(0...@scene.image.width).each do |x|
# Rendereamos cada pixel
@scene.image.data[y*@scene.image.width+x] = render_pixel(x, y)
#print "[", 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