Bölüm 5: Yüzey Normalleri ve Çoklu Nesneler
Küreye çarpmayı başardık; ancak küremiz hala düz kırmızı bir daire gibi duruyor. Gerçek bir 3 boyutlu nesne hissi vermek ve ileride ışıklandırma hesaplayabilmek için kürenin her bir noktasının hangi yöne baktığını bilmemiz gerekir.
İşte bu yön bilgisine Yüzey Normali (Surface Normal) denir.
Yüzey Normalleri ile Gölgelendirme (Shading with Surface Normals)
Yüzey normali, çarpışma noktasında yüzeye dik (90 derece açı yapan) bir birim vektördür:

Bir küre için normal vektörü bulmak geometrik olarak son derece kolaydır: Çarpışma noktasından (\(\mathbf{P}\)) kürenin merkezini (\(\mathbf{C}\)) çıkarıp sonucu yarıçapa bölerek birim vektör yapmak!
\[\mathbf{n} = \frac{\mathbf{P} - \mathbf{C}}{r}\]
Normaller birim vektör olduğu için her bir bileşeni (\(x, y, z\)) \([-1.0, +1.0]\) aralığındadır. Bu vektörleri görselleştirmek için her bir bileşeni renklerin \([0.0, 1.0]\) aralığına doğrusal olarak öteleriz:
\[\text{renk} = 0.5 \cdot (\mathbf{n} + 1)\]
Böylece \(+X\) yönüne bakan kısımlar kırmızı, \(+Y\) yönüne bakan üst kısımlar yeşil, \(+Z\) yönüne bakan kısımlar mavi olarak boyanacaktır:
| hit_sphere (Kesişim Noktasını Dönen Versiyon) |
|---|
| double hit_sphere(const point3& center, double radius, const ray& r) {
vec3 oc = center - r.origin();
auto a = dot(r.direction(), r.direction());
auto b = -2.0 * dot(r.direction(), oc);
auto c = dot(oc, oc) - radius*radius;
auto discriminant = b*b - 4*a*c;
if (discriminant < 0) {
return -1.0;
} else {
// En yakın pozitif t kökünü döndür
return (-b - std::sqrt(discriminant)) / (2.0*a);
}
}
|
ray_color fonksiyonumuzu normalleri gösterecek şekilde güncelleyelim:
| ray_color Fonksiyonu |
|---|
| color ray_color(const ray& r) {
auto t = hit_sphere(point3(0,0,-1), 0.5, r);
if (t > 0.0) {
vec3 N = unit_vector(r.at(t) - vec3(0,0,-1));
return 0.5*color(N.x()+1, N.y()+1, N.z()+1);
}
vec3 unit_direction = unit_vector(r.direction());
auto a = 0.5*(unit_direction.y() + 1.0);
return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0);
}
|
Bu kodu çalıştırdığımızda:

Küre artık gerçek bir 3 boyutlu hacim ve zengin bir derinlik kazandı!
Kesişim Kodunu Sadeleştirme (\(b = -2h\))
Kuadratik denklemimizdeki \(b\) katsayısına tekrar bakalım:
\[b = -2 \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q})\]
Fark ettiyseniz \(b\) teriminin çarpanı daima \(2\)'dir. Eğer biz \(h = \mathbf{d} \cdot (\mathbf{C} - \mathbf{Q})\) dersek, \(b = -2h\) olur.
Şimdi bu \(b = -2h\) değerini kuadratik kök formülüne koyalım:
\[\frac{-b \pm \sqrt{b^2 - 4ac}}{2a} = \frac{-(-2h) \pm \sqrt{(-2h)^2 - 4ac}}{2a} = \frac{2h \pm \sqrt{4h^2 - 4ac}}{2a}\]
Karekök içindeki \(4\)'ü parantez dışına \(2\) olarak çıkarabiliriz:
\[\frac{2h \pm 2\sqrt{h^2 - ac}}{2a} = \frac{h \pm \sqrt{h^2 - ac}}{a}\]
Bu muazzam bir cebirsel kazanımdır! Artık:
- Ne \(b\)'yi \(2\) ile çarpmak zorundayız,
- Ne diskriminantta \(4ac\) hesaplamak zorundayız,
- Ne de paydayı \(2a\) yapmak zorundayız!
Işın izleyicimiz bu fonksiyonu saniyede yüz milyonlarca kez çağıracağı için bu sadeleştirme çok ciddi bir performans artışı sağlar:
| Sadeleştirilmiş hit_sphere |
|---|
| double hit_sphere(const point3& center, double radius, const ray& r) {
vec3 oc = center - r.origin();
auto a = r.direction().length_squared();
auto h = dot(r.direction(), oc);
auto c = oc.length_squared() - radius*radius;
auto discriminant = h*h - a*c;
if (discriminant < 0) {
return -1.0;
} else {
return (h - std::sqrt(discriminant)) / a;
}
}
|
hittable Soyut Nesne Sınıfı (An Abstraction for Hittable Objects)
Sahnemize sadece tek bir küre değil; yüzlerce küre, kutular ve ileride üçgen modeller koymak isteyeceğiz. Nesne yönelimli tasarımda her nesnenin bir hit metodu olmalıdır.
Önce bir çarpışma anındaki tüm detayları saklayacak hit_record yapısını tanımlayalım:
| hittable.h |
|---|
| #ifndef HITTABLE_H
#define HITTABLE_H
#include "ray.h"
class hit_record {
public:
point3 p; // Çarpışma noktası P(t)
vec3 normal; // Yüzey normali
double t; // Işın parametresi
bool front_face; // Işın dışarıdan mı çarptı?
void set_face_normal(const ray& r, const vec3& outward_normal) {
// Işın ile dış normal zıt yöndeyse ışın dışarıdan gelmiştir
front_face = dot(r.direction(), outward_normal) < 0;
normal = front_face ? outward_normal : -outward_normal;
}
};
class hittable {
public:
virtual ~hittable() = default;
virtual bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const = 0;
};
#endif
|
Ön Yüz ve Arka Yüz Normalleri (Front Faces Versus Back Faces)
Bir ışın bir küreye hem dışarıdan çarpabilir (ön yüz), hem de kameramız kürenin içindeyse veya nesne şeffaf bir cam küreyse içeriden çarpabilir (arka yüz).

Normal vektörü daima kürenin merkezinden dışarıya doğru bakar. Ancak gölgelendirme hesaplarında normalin daima ışının geldiği yöne doğru bakmasını isteriz:
- Eğer dot(ray.direction(), outward_normal) < 0 ise: Işın dışarıdan geliyordur (front_face = true), normal dışarı bakar.
- Eğer pozitif ise: Işın içeriden geliyordur (front_face = false), normal ters çevrilmelidir (-outward_normal).
Bu ayrım özellikle Bölüm 10'daki cam kırılma simülasyonunda hayati bir rol oynayacaktır!
sphere.h Sınıfı
sphere sınıfımızı hittable arayüzünü uygulayacak şekilde yazalım:
| sphere.h |
|---|
| #ifndef SPHERE_H
#define SPHERE_H
#include "hittable.h"
#include "vec3.h"
class sphere : public hittable {
public:
sphere(const point3& static_center, double static_radius)
: center(static_center), radius(std::fmax(0, static_radius)) {}
bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const override {
vec3 oc = center - r.origin();
auto a = r.direction().length_squared();
auto h = dot(r.direction(), oc);
auto c = oc.length_squared() - radius*radius;
auto discriminant = h*h - a*c;
if (discriminant < 0)
return false;
auto sqrtd = std::sqrt(discriminant);
// Kabul edilebilir aralıkta [ray_tmin, ray_tmax] kalan en yakın kökü bul
auto root = (h - sqrtd) / a;
if (root <= ray_tmin || ray_tmax <= root) {
root = (h + sqrtd) / a;
if (root <= ray_tmin || ray_tmax <= root)
return false;
}
rec.t = root;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
return true;
}
private:
point3 center;
double radius;
};
#endif
|
Sayısal Aralık Sınıfı: interval.h
Kesişim mesafelerinin geçerli bir aralıkta kalıp kalmadığını (ray_tmin < t && t < ray_tmax) yönetmek için şık bir interval sınıfı yazalım:
| interval.h |
|---|
| #ifndef INTERVAL_H
#define INTERVAL_H
class interval {
public:
double min, max;
interval() : min(+infinity), max(-infinity) {} // Boş aralık
interval(double _min, double _max) : min(_min), max(_max) {}
double size() const { return max - min; }
bool contains(double x) const { return min <= x && x <= max; }
bool surrounds(double x) const { return min < x && x < max; }
double clamp(double x) const {
if (x < min) return min;
if (x > max) return max;
return x;
}
static const interval empty, universe;
};
const interval interval::empty = interval(+infinity, -infinity);
const interval interval::universe = interval(-infinity, +infinity);
#endif
|
Çoklu Nesneler Listesi: hittable_list.h
Sahnemizdeki tüm nesneleri bir arada tutacak ve fırlatılan bir ışının en yakındaki nesneyle kesişimini bulacak sınıf:
| hittable_list.h |
|---|
| #ifndef HITTABLE_LIST_H
#define HITTABLE_LIST_H
#include "hittable.h"
#include <memory>
#include <vector>
using std::make_shared;
using std::shared_ptr;
class hittable_list : public hittable {
public:
std::vector<shared_ptr<hittable>> objects;
hittable_list() {}
hittable_list(shared_ptr<hittable> object) { add(object); }
void clear() { objects.clear(); }
void add(shared_ptr<hittable> object) {
objects.push_back(object);
}
bool hit(const ray& r, double ray_tmin, double ray_tmax, hit_record& rec) const override {
hit_record temp_rec;
bool hit_anything = false;
auto closest_so_far = ray_tmax;
for (const auto& object : objects) {
// Her çarpışmada closest_so_far güncellenerek yalnızca en öndeki nesne kaydedilir
if (object->hit(r, ray_tmin, closest_so_far, temp_rec)) {
hit_anything = true;
closest_so_far = temp_rec.t;
rec = temp_rec;
}
}
return hit_anything;
}
};
#endif
|
İki Küreli Sahne Renderı
Artık sahnemize biri merkezde küçük bir küre, diğeri ise onun altında zemin oluşturan devasa bir küre (yarıçapı 100 birim) ekleyebiliriz:
| main.cc |
|---|
| #include "rtweekend.h"
#include "hittable.h"
#include "hittable_list.h"
#include "sphere.h"
int main() {
// ... görüntü boyutları ve kamera ayarları ...
// Sahne Nesneleri
hittable_list world;
world.add(make_shared<sphere>(point3(0,0,-1), 0.5));
world.add(make_shared<sphere>(point3(0,-100.5,-1), 100));
// ... render döngüsü ...
}
|
Render çıktımız:

Gördüğünüz gibi devasa küre altında ufka uzanan mükemmel bir zemin oluşturdu! Bir sonraki bölümde bu render mantığını tertemiz bir camera sınıfına taşıyacağız.