Last active
February 6, 2017 20:15
-
-
Save phrozen/58c572d29846d3373e23590b66d35cc3 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 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