Last active
February 7, 2017 04:25
-
-
Save phrozen/6ba0c01884ee2fecc7019b589048bf25 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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() | |
([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 | |
@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