Rozbudowa sceny¶
Dlaczego trójkąty?¶
Trzy punkty zawsze leżą na jednej płaszczyźnie i jest tylko jedna płaszczyzna przechodząca przez te trzy punkty (tak jak dwa punkty — prostą).
Dowolny wielokąt możemy podzielić na trójkąty.
Dlatego bardziej skomplikowane kształty w 3D będziemy opisywać przy pomocy trójkątów. Podobnie, jak w przypadku dwuwymiarowym krzywe linie rysujemy jako serię prostych odcinków - odcinki są na tyle krótkie, że nie widać różnicy. Z odpowiednio małych trójkątów możemy złożyć dowolny kształt.
Dzisiaj:
Zaimplementujemy sprawdzanie przecięcia trójkąta z promieniem.
Zobaczymy, jak można z trójkątów składać bardziej złożone kształty.
Wektory i układy równań¶
Przypomnienie z wczoraj:

Żeby sprawdzić, czy punkt \(D\) znajduje się w trójkącie ABC, możemy rozwiązać układ równań:
Skrótowo możemy to zapisać jako \(P = u(B - A) + v(C - a)\) — chociaż widzimy jedno równanie, tak na prawdę mamy dwa, po jednym na każdą współrzędną.
Tak samo możemy sprawdzać, czy punkt znajduje się w trójkącie w przypadku trójwymiarowym; wtedy mamy trzy równania na trzy współrzędne (skrótowy zapis pozostaje taki sam).
Zatem układ równań \(P + t(R - P) = A + u(B - A) + v(C - A)\) pozwala nam znaleźć przecięcie promienia z trójkątem. Jeżeli ten układ ma rozwiązanie, to prosta przez punkty \(P, R\) przecina gdzieś płaszczyznę wyznaczona przez punkty \(A, B, C\). Żeby sprawdzić, czy to promień (czyli półprosta) przecina trójkąt, musimy sprawdzić, w jakich przedziałach znajdują się wartości \(t, u, v\).
W kodzie startowym znajduje się również funkcja do rozwiązywania układów równań w Pythonie: solve_linear_system(coefficients, free_terms).
Pierwszy argument to lista list oznaczajaca współczynniki przy zmiennych w kolejnych równaniach, a drugi to wyrazy wolne stojące po drugiej stronie znaku równości.
Przykładowo układ równań:
moglibyśmy przy jej pomocy rozwiązać tak:
from starting_code.w2_simple_raytracer import solve_linear_system
x, y = solve_linear_system([[5, 3], [1, 1]], [11, 3])
print("x = ", x)
print("y = ", x)
Przecięcie promienia z trójkątem¶
Zaimplementuj funkcję compute_ray_plane_intersection(triangle, ray), która dla podanego trójkąta i promienia obliczy, czy/w jakiej odległości promień przecina płaszczyznę przechodzącą przez podane punkty trójkąta. Funkcja powinna zwracać rozwiązanie ukłądu: trzy liczby u, v, t, gdzie u i v to współczynniki przy wektorach rozpinających płaszczyznę, a t to odległość punktu przecięcia od początku promienia.
Wskazówka: przy oznaczeniach jak w przykładowym kodzie (trójkąt \(ABC\), promień \(\vec{PR}\)) funkcja compute_ray_triangle_intersection musi przede wszystkim rozwiązać układ równań $P + t(R - P) = A + u(B - A) + v(C - A).
Wskazówka: odległość od początku promienia do punktu przecięcia wynosi \(\frac{t}{|R - P|}\), gdzie \(|v|\) oznacza długość wektora, a \(t\) to niewiadoma z równania opisującego promień.
Ostrzeżenie
Dzisiejszy kod startowy używa magii do renderowanie sceny. Odkomentuj wywołanie funkcji show na końcu pliku.
Jeżeli widzisz czarny ekran i nie masz pojęcia, czemu, spróbuj ustawić debug=True w wywołaniu funkcji show na końcu pliku. Jeżeli masz dużo obiektów na scenie, to renderowanie bardzo zwolni, ale bez tego nie zobaczysz komunikatów o błędach, jeżeli jakieś by się zdarzyły.
Na podstawie tej jednej funkcji możemy dodać do naszej palety kształtów:
Trójkąt: przecięcie znajduje się w trójkącie gdy
0 <= u and 0 <= v and u + v <= 1.Równoległobok (w tym prostokąt): gdy
0 <= v <= 1 and 0 <= u <= 1.
Więcej informacji o przecięciu¶
Do tej pory funkcja check_hit zwracała tylko informację o odległości od źródła promienia. Do dalszej rozbudowy raytracera będziemy potrzebowali od niej więcej informacji:
Stworzymy w tym celu dodatkową klasę. Dopisanie magicznego @dataclasses.dataclass na początku klasy sprawi, że Python sam stworzy dla nas konstruktor przypisujący wartości zmiennych wymienionych w klasie. Jest to wygodne, kiedy chcemy używać klasy tylko do trzymania wartości zmiennych (bez funkcji).
@dataclasses.dataclass
class HitData:
distance: float # Odległość przecięcia od początku promienia
position: Vec3 # Punkt przecięcia obiektem
ray_direction: Vec3 # Kierunek promienia
surf_normal: Vec3 # Wektor prostopadły do powierzchni obiektu w punkcie przecięcia (normalna)
object: SceneObject # Trafiony obiekt
bounces: int = 0
extra: Any = None # Dodatkowe informacje zależne od kształtu obiektu
# Przykład stworzenia zmiennej trzymającej HitData
hit = HitData(
distance=0.5,
position=Vec3(1, 2, 3),
ray_direction=Vec3(1, 0, 0),
surf_normal=Vec3(0, 1, 0),
object=Sphere(...),
# Pozostałe pola można pominąć
)
Materiały¶
Teraz zamiast przypisywać obiektom po prostu kolory, będziemy przypisywać im materiały. Materiał będzie określał kolor obiektu na podstawie informacji zawartych w HitData:
class Material:
def get_color(self, hit: HitData) -> Color:
raise NotImplementedError
class EmissiveMaterial(Material):
"""Obiekt świeci własnym światłem"""
def __init__(self, color: Color):
self.color = color
def get_color(self, hit: HitData) -> Color:
return self.color
class FakeLightMaterial(Material):
"""Obiekt udaje oświetlenie: powierzchnia jest tym jaśniejsza, im bardziej na wprost patrzymy"""
def __init__(self, color: Color):
self.color = color
def get_color(self, hit: HitData) -> Color:
brightness = max(0, -hit.surf_normal.normalized().dot(hit.ray_direction.normalized()))
return Color(int(brightness * self.color.r), int(brightness * self.color.g), int(brightness * self.color.b))
Zadanie 6
Zmodyfikuj istniejący kod tak, żeby wszystkie obiekty zamist posiadać color miały material.
Funkcja show poradzi sobie z tą zmianą automagicznie.
Jutro dowiemy się, jak wykorzystać dzisiejsze zmiany do wyświetlania tekstur na obiektach. A tymczasem…
Zadanie 7
Zaimplementuj AnimeMaterial! Polega on na modyfikacji FakeLightMaterial tak, żeby brightness zmieniała się skokowo na kilku wartościach.

Zadanie 8
Zmodyfikuj FakeLightMateial i AnimeMaterial w taki sposób, żeby światło dochodziło z ustalonego kierunku, a nie od środka kamery. W tym celu musisz liczyć iloczyn wektrowy między normalną a tym ustalonym wektorem.