Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
361,030 changes: 180,515 additions & 180,515 deletions after.ppm

Large diffs are not rendered by default.

Binary file modified before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
216,742 changes: 39,742 additions & 177,000 deletions before.ppm

Large diffs are not rendered by default.

74 changes: 32 additions & 42 deletions camera.py
Original file line number Diff line number Diff line change
@@ -1,78 +1,68 @@
import numpy as np
from src.Ponto import Ponto
from src.Vetor import Vetor
from utils.Scene.sceneSchema import CameraData


class Camera:
"""
Representa uma câmera pinhole para ray casting.

A câmera é definida pelos parâmetros fornecidos no arquivo de cena:
- Posição da câmera (lookfrom)
- Ponto para onde a câmera olha (lookat)
- Vetor "para cima" (up_vector)
- Distância até o plano de projeção (screen_distance)
- Resolução da imagem (image_width, image_height)

A partir desses dados, é construída uma base ortonormal (U, V, W):
- W aponta no sentido oposto à direção de visão (de M para C)
- U representa o eixo horizontal (direita da câmera)
- V representa o eixo vertical (cima da câmera)

Com essa base, define-se um plano de imagem (tela) localizado a uma
distância d da câmera. A tela possui largura normalizada igual a 1.0
e altura proporcional à resolução.

Cada pixel da imagem é mapeado para um ponto nesse plano, e a direção
de um raio é obtida a partir do vetor que liga a câmera a esse ponto.

Esse modelo permite converter coordenadas discretas de pixel (i, j)
em direções contínuas no espaço 3D.
"""

def __init__(self, cam_data: CameraData):
self.C = cam_data.lookfrom
self.M = cam_data.lookat
self.Vup = cam_data.up_vector
self.d = cam_data.screen_distance
# GARANTIA: Converte entradas da cena para Ponto/Vetor caso venham do NumPy
self.C = self._to_ponto(cam_data.lookfrom)
self.M = self._to_ponto(cam_data.lookat)
self.Vup = self._to_vetor(cam_data.up_vector)
self.d = float(cam_data.screen_distance)

self.hres = cam_data.image_width
self.vres = cam_data.image_height

# --- Construção da Base Ortonormal (U, V, W) ---
# W aponta para trás (da cena para a câmera)
direcao_w = self.C - self.M
self.W = direcao_w.normalize()

# U é o eixo horizontal (direita)
direcao_u = self.Vup.cross(self.W)
self.U = direcao_u.normalize()

# V é o eixo vertical (cima)
self.V = self.W.cross(self.U)

# --- Configuração do Plano de Imagem ---
# Largura da tela fixa em 1.0, altura proporcional
self.pixel_size = 1.0 / self.hres
screen_width = 1.0
screen_height = self.vres * self.pixel_size

# Centro da tela projetada a distância d
self.screen_center = self.C - (self.W * self.d)

# Canto superior esquerdo da tela (Ponto de partida para o rastreio)
# Deslocamos metade da largura para a esquerda (-U) e metade da altura para cima (+V)
self.upper_left = self.screen_center - (self.U * (screen_width / 2.0)) + (self.V * (screen_height / 2.0))

def get_ray_direction(self, i: int, j: int) -> Vetor:
"""
Calcula a direção do raio correspondente ao pixel (i, j).

O ponto central do pixel é obtido deslocando-se a partir do canto
superior esquerdo da tela ao longo dos eixos U (horizontal) e V (vertical),
considerando o tamanho de cada pixel.
def _to_ponto(self, p):
"""Helper para blindagem contra NumPy."""
if isinstance(p, np.ndarray):
return Ponto(p[0], p[1], p[2])
return p

O deslocamento usa (i + 0.5) e (j + 0.5) para amostrar o centro do pixel,
evitando viés de amostragem nas bordas.
def _to_vetor(self, v):
"""Helper para blindagem contra NumPy."""
if isinstance(v, np.ndarray):
return Vetor(v[0], v[1], v[2])
return v

A direção do raio é então o vetor que liga a posição da câmera ao ponto
calculado na tela, sendo normalizado antes do retorno.
def get_ray_direction(self, i: int, j: int) -> Vetor:
"""
Calcula a direção do raio para o pixel (i, j).
Garante o retorno de um objeto da classe Vetor.
"""
# (i + 0.5) amostra o centro do pixel para evitar aliasing de borda
deslocamento_x = self.U * ((i + 0.5) * self.pixel_size)
deslocamento_y = self.V * ((j + 0.5) * self.pixel_size)

# Ponto no mundo 3D correspondente ao pixel na tela
pixel_center = self.upper_left + deslocamento_x - deslocamento_y

# Direção: do centro da câmera (C) para o ponto na tela
direcao = pixel_center - self.C
return direcao.normalize()
54 changes: 42 additions & 12 deletions geometria.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,26 @@
from src.Ponto import Ponto
from src.Vetor import Vetor

def garantir_ponto(p):
"""Converte array numpy para Ponto se necessário."""
if isinstance(p, np.ndarray):
return Ponto(p[0], p[1], p[2])
return p

def intersect_sphere(origem: Ponto, direcao: Vetor, centro: Ponto, raio: float) -> float:
def garantir_vetor(v):
"""Converte array numpy para Vetor se necessário."""
if isinstance(v, np.ndarray):
return Vetor(v[0], v[1], v[2])
return v

def intersect_sphere(origem, direcao, centro, raio: float) -> float:
"""
Calcula a interseção entre um raio e uma esfera.
"""
origem = garantir_ponto(origem)
direcao = garantir_vetor(direcao)
centro = garantir_ponto(centro)

v = origem - centro

v_dot_d = v.dot(direcao)
Expand All @@ -28,13 +43,18 @@ def intersect_sphere(origem: Ponto, direcao: Vetor, centro: Ponto, raio: float)

return float('inf')


def intersect_plane(origem: Ponto, direcao: Vetor, p0: Ponto, normal: Vetor) -> float:
def intersect_plane(origem, direcao, p0, normal) -> float:
"""
Calcula a interseção entre um raio e um plano.
"""
origem = garantir_ponto(origem)
direcao = garantir_vetor(direcao)
p0 = garantir_ponto(p0)
normal = garantir_vetor(normal)

denom = direcao.dot(normal)

# Agora denom é garantidamente um float, abs() funcionará
if abs(denom) > 1e-6:
p0_origem = p0 - origem
t = p0_origem.dot(normal) / denom
Expand All @@ -44,15 +64,15 @@ def intersect_plane(origem: Ponto, direcao: Vetor, p0: Ponto, normal: Vetor) ->

return float('inf')


def intersect_triangles_numpy(origem: Ponto, direcao: Vetor,
v0: np.ndarray, v1: np.ndarray, v2: np.ndarray) -> float:
v0: np.ndarray, v1: np.ndarray, v2: np.ndarray) -> tuple:
"""
Möller–Trumbore vetorizado — testa N triângulos de uma vez com numpy.
v0, v1, v2 são arrays (N, 3) pré-computados no pré-processamento.
Retorna o menor t válido ou inf se não houver interseção.
Möller–Trumbore vetorizado — retorna (menor t, índice do triângulo).
Se não houver interseção: (inf, -1)
"""

EPSILON = 1e-8

orig = np.array([origem.x, origem.y, origem.z])
dire = np.array([direcao.x, direcao.y, direcao.z])

Expand All @@ -63,20 +83,30 @@ def intersect_triangles_numpy(origem: Ponto, direcao: Vetor,
a = np.einsum('ij,ij->i', aresta1, h) # (N,)

mask = np.abs(a) > EPSILON

f = np.where(mask, 1.0 / np.where(mask, a, 1), 0)

s = orig - v0 # (N, 3)
u = f * np.einsum('ij,ij->i', s, h) # (N,)
mask &= (u >= 0.0) & (u <= 1.0)

q = np.cross(s, aresta1) # (N, 3)
v = f * (q @ dire) # (N,) ← corrigido
v = f * (q @ dire) # (N,)
mask &= (v >= 0.0) & ((u + v) <= 1.0)

t = f * np.einsum('ij,ij->i', aresta2, q) # (N,) ← corrigido
t = f * np.einsum('ij,ij->i', aresta2, q) # (N,)
mask &= (t > 0.001)

# ============================================================
# RESULTADO
# ============================================================

if not np.any(mask):
return float('inf')
return float('inf'), -1

valid_indices = np.where(mask)[0]

idx_local = np.argmin(t[mask])
idx_global = valid_indices[idx_local]

return float(np.min(t[mask]))
return float(t[idx_global]), int(idx_global)
50 changes: 50 additions & 0 deletions inputs/input1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"camera": {
"lookfrom": [8.0, 4.0, 12.0],
"lookat": [0.0, 0.0, 0.0],
"upVector": [0.0, 1.0, 0.0],
"image_width": 500,
"image_height": 500,
"screen_distance": 0.75
},

"globalLight": [0.1, 0.1, 0.1],

"lights": [
{
"position": [10.0, 10.0, 10.0],
"color": [1.0, 1.0, 1.0],
"name": ""
}
],

"materials": {
"red": {
"name": "red",
"color": [1.0, 0.0, 0.0],
"ks": [0, 0, 0],
"ka": [1, 0, 0],
"kr": [0, 0, 0],
"kt": [0, 0, 0],
"ns": 1,
"ni": 1,
"d": 1
}
},

"objects": [
{
"type": "mesh",
"relativePos": [0, 0, 0],
"material": "red",
"path": "utils/input/cubo.obj",

"transform": [
{
"type": "translation",
"vector": [5, 0, 0]
}
]
}
]
}
50 changes: 50 additions & 0 deletions inputs/input10.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"camera": {
"lookfrom": [8, 6, 10],
"lookat": [0, 0, 0],
"upVector": [0, 1, 0],
"image_width": 500,
"image_height": 500,
"screen_distance": 0.75
},

"globalLight": [0.1, 0.1, 0.1],

"lights": [
{
"position": [10, 10, 10],
"color": [1, 1, 1],
"name": ""
}
],

"materials": {
"green": {
"name": "green",
"color": [0, 1, 0],
"ks": [0, 0, 0],
"ka": [0, 1, 0],
"kr": [0, 0, 0],
"kt": [0, 0, 0],
"ns": 1,
"ni": 1,
"d": 1
}
},

"objects": [
{
"type": "plane",
"relativePos": [0, 0, 0],
"material": "green",
"normal": [0, 1, 0],

"transform": [
{
"type": "rotation",
"angle": [90, 0, 0]
}
]
}
]
}
50 changes: 50 additions & 0 deletions inputs/input11.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"camera": {
"lookfrom": [8, 6, 10],
"lookat": [0, 0, 0],
"upVector": [0, 1, 0],
"image_width": 500,
"image_height": 500,
"screen_distance": 0.75
},

"globalLight": [0.1, 0.1, 0.1],

"lights": [
{
"position": [10, 10, 10],
"color": [1, 1, 1],
"name": ""
}
],

"materials": {
"green": {
"name": "green",
"color": [0, 1, 0],
"ks": [0, 0, 0],
"ka": [0, 1, 0],
"kr": [0, 0, 0],
"kt": [0, 0, 0],
"ns": 1,
"ni": 1,
"d": 1
}
},

"objects": [
{
"type": "plane",
"relativePos": [0, 0, 0],
"material": "green",
"normal": [0, 1, 0],

"transform": [
{
"type": "scaling",
"factors": [100, 100, 100]
}
]
}
]
}
Loading