Photon Engine'le yan yana, prodüksiyon seviyesinde bir path tracer rehberi
Işık taşınımından BVH'ye, malzeme modellerinden renk yönetimine, USD girişinden doğrulama testlerine kadar bir path tracer'ın on beş katmanını tek tek anlatıyorum. İlgili bölümlerin sonuna da C++20 ile yazdığım Photon Engine'in o konuda ne yaptığını not ettim.
Bu rehberde prodüksiyon seviyesindeki bir path tracer'ın hangi parçalardan oluştuğunu baştan sona anlatıyorum. Işık taşınımından BVH'ye, malzeme modellerinden renk yönetimine, USD girişinden doğrulama testlerine kadar on beş bölüm var. Her bölümü tek başına okuyabilirsin; sırayla okursan bir motorun iskeletinden piksel çıktısına kadar bütün yolu görürsün.
Rehberi, kendi render motorum Photon Engine ile yan yana okunsun diye hazırladım. Photon, ürün görselleştirme için C++20 ile yazdığım bir path tracer ve onun üzerine kurduğum bir masaüstü stüdyodan oluşuyor. Bu haritanın çekirdek kısmını Photon da uyguluyor, o yüzden ilgili bölümlerin sonuna Photon'un o konuda ne yaptığını anlatan kısa notlar bıraktım. Motorun genel tanıtımını ve yol boyunca düzelttiğim hataları merak edersen önceki yazıya göz atabilirsin.
Photon Engine bu haritanın neresinde?
| Bölüm | Photon Engine'de |
|---|---|
| 0 · Mimari | Düzenlenebilir SceneGraph, değişmez Scene, arada buildRenderScene() |
| 1 · Işık taşınımı | PT + NEE + MIS (güç sezgiseli), Rus ruleti, HDRI önem örneklemesi |
| 2 · Kesişim | Embree 4; yoksa kendi 12 kovalı SAH BVH'si |
| 3 · Malzeme | Disney Principled, GGX + VNDF buzlu cam, 37 hazır malzeme |
| 4 · Örnekleme ve gürültü | Owen karıştırmalı Sobol, OIDN 2.5 (albedo + normal) |
| 9 · Renk yönetimi | Doğrusal Rec.709; PBR Neutral, AgX, ACES, Hable |
| 10 · Çıktı | PNG, JPEG, doğrusal EXR, turntable |
| 11 · Sahne girişi | OBJ, glTF, GLB; .photon proje dosyası |
Hacimler, out-of-core bellek, USD/Hydra ve GPU arka uçları ise Photon'un bugün kapsadığı alanın dışında kalıyor. O bölümleri, motorun sıradaki adımları için bir yol haritası gibi okuyabilirsin.
Genel bakış
Bir prodüksiyon path tracer'ının merkezinde, rendering equation'ı Monte Carlo ile çözen bir çekirdek var. Bu çekirdeği besleyen katmanlar şunlar: sahne girişi, geometri, malzeme, hacimler, bellek, kamera, kesişim ve örnekleme. Çıktı tarafında da AOV'ler ve renk yönetimi duruyor. Şemadaki her kutunun numarası, o katmanı anlatan bölümün numarasıyla aynı.

motor katmanları · 13 bileşen, 1 çekirdek
Önerdiğim okuma sırası şöyle: önce 0. bölüm, çünkü kodun iskeletini ve modüllerin birbirine nasıl bağlandığını orada görüyorsun. Sonra çekirdek için 1-4. bölümler, ardından prodüksiyon katmanları (5-11), en son da mimari ve doğrulama (12-13). Kod örnekleri C++.
0. Yazılım mimarisi: motorun iskeleti
Bir path tracer, bağımlılıkların hep tek yöne aktığı yedi katmandan oluşur: uygulama, oturum, yazar sahnesi, sahne derleyici, device, kernel ve film. Temel kural şu: üst katmanlar nesne yönelimli, alt katmanlar ise düz, indeks tabanlı ve GPU'ya kopyalanabilir veri. İkisini birbirinden "sahne derleyici" ayırıyor. Öteki bölümlerdeki tekniklerin kodda nereye oturduğunu da bu bölümde görürsün.
Katman haritası

katman haritası · 9 modül, 1 bellek sınırı
Kesik çizgi, host belleğiyle device belleği arasındaki sınır; bu sınırı yalnızca derleyicinin çıktısı geçiyor. Kernel yazar sahnesini hiç görmez. Session da kernel'a doğrudan dokunmaz, onu integrator ve device arayüzü üzerinden sürer.
Modüller, dizin yapısı ve bağımlılık kuralı
Kod tabanını dokuz modüle bölünce her tekniğin nereye ait olduğu netleşiyor. Kural tek: bağımlılık yalnızca aşağı doğru akar, kernel/ de yalnızca core/'u görür.
| Modül | Sorumluluk | Bağımlı olduğu | Unity karşılığı |
|---|---|---|---|
core/ | Vektör/matris, Spectrum, RNG, hash, bellek arena'ları, log, assert | hiçbiri | Unity.Mathematics |
kernel/ | Cihazdan bağımsız render kodu: kamera, BSDF, ışıklar, hacimler, örnekleme, integrator | core (yalnızca header, heap yok, STL yok) | Shader kodu / Burst job |
device/ | CPU, CUDA/OptiX, HIP, Metal arka uçları: tampon, kernel başlatma, ivme yapısı | core | Graphics API (Unreal'da RHI) |
scene/ | Yazar sahnesi: Mesh, Instance, Light, MaterialGraph, Camera, Volume; dirty bayrakları | core | GameObject + Component |
io/ | USD, glTF/OBJ, OpenImageIO texture, OpenVDB okuyucular | scene, core | AssetImporter |
compile/ | Sahne derleyici: tessellation, displacement, BVH kurulumu, light tree, malzeme derleme, texture handle tablosu | scene, device, kernel (tip tanımları) | Build/bake pipeline |
film/ | Birikim, AOV, varyans, adaptive sampling istatistiği, denoise, OCIO, EXR yazıcı | device, core | RenderTexture + post-process |
session/ | Oturum, durum makinesi, progressive döngü, iptal, istatistik | yukarıdakilerin hepsi | SRP render pipeline |
app/ | CLI, viewer, Hydra delegate eklentisi, Python binding | session | Editor |
renderer/
├── CMakeLists.txt
├── core/ # math.h, spectrum.h, rng.h, arena.h, log.h
├── kernel/ # yalnızca header; CPU ve GPU aynı kodu derler
│ ├── types.h # Ray, Interaction, PathState, LightSample
│ ├── camera/ # pinhole, thin_lens, realistic
│ ├── bsdf/ # diffuse, ggx, sheen, coat, layered, openpbr
│ ├── light/ # area, point, spot, env, light_tree
│ ├── volume/ # delta/ratio tracking, phase
│ ├── sampling/ # sobol, owen, blue_noise, warp
│ └── integrator/ # path.h (megakernel), wavefront/ (kernel aşamaları)
├── device/
│ ├── device.h # soyut arayüz
│ ├── cpu/ # Embree + oneTBB
│ ├── optix/ # CUDA + OptiX programları
│ └── hip/ metal/
├── scene/ # node'lar, dirty bayrakları, sahne grafiği
├── io/ # usd/, gltf/, image/, vdb/
├── compile/ # geometry_compiler, material_compiler, light_compiler
├── film/ # film.h, aov.h, denoise/, ocio/, exr_writer
├── session/ # session.h, render_loop, stats
├── app/ # cli/, viewer/, hydra_delegate/, python/
└── tests/ # furnace, chi2, referans, regresyonkernel/'ın bu kadar kısıtlı olmasının sebebi şu: aynı dosya hem g++ hem nvcc ile derleniyor. Cihaz kodunda std::vector, new, sanal fonksiyon ve istisna kullanamazsın. Bu yüzden kernel bütün veriyi önceden ayrılmış düz tamponlardan, indeksle okur. Bu kuralı CMake'te zorlamak da mümkün: kernel bir INTERFACE kütüphanesi olur ve yalnızca core'a bağlanır, böylece yanlış bir #include doğrudan derleme hatası verir. Cycles'taki intern/cycles/kernel ile PBRT-v4'ün cpu/ ve gpu/ ayrımı, aynı fikrin iki farklı uygulaması.
Çekirdek veri tipleri
Kernel'ın tamamı altı küçük tipin etrafında döner. Bunları doğru tasarlamak önemli, çünkü sonraki her modülün imzası bu tiplere göre şekilleniyor. Hepsi POD (düz veri) olmalı ve GPU'ya kopyalanabilmeli.
// kernel/types.h — CPU ve GPU aynı tanımı kullanır
#define KERNEL_FN __host__ __device__ inline // CPU derlemesinde boş makro
struct Ray {
Vec3 o, d; // başlangıç, yön (normalize)
float tMin, tMax; // geçerli aralık; self-intersection için tMin > 0 değil, offset kullanılır (bölüm 2)
float time; // motion blur için [0,1) shutter zamanı
RayCone cone; // mip seçimi için yayılma açısı + genişlik (bölüm 8)
};
struct Hit { // kesişim katmanının tek çıktısı: küçük tutulur
float t;
uint32_t instId, primId;
Vec2 bary; // üçgen içi barycentric
};
struct ShadingPoint { // Hit'ten "genişletilir": normal, UV, türevler, malzeme
Vec3 p, ng, ns; // pozisyon, geometrik normal, shading normal
Frame frame; // ns etrafında yerel taban (BSDF yerel uzayda çalışır)
Vec2 uv;
Vec3 dpdu, dpdv; // anizotropi ve texture filtresi için
uint32_t materialId, lightId; // lightId: yüzey aynı zamanda ışık mı?
MediumInterface media; // iç ve dış hacim (bölüm 6)
};
struct BSDFSample {
Spectrum f; // BSDF değeri
Vec3 wi;
float pdf;
LobeFlags lobe; // Diffuse | Glossy | Specular | Transmission
float eta; // kırılmada göreceli kırıcılık
};
struct LightSample {
Spectrum Le; // gelen radyans
Vec3 wi;
float dist; // gölge ışını tMax'ı
float pdf; // katı açı ölçüsünde (MIS için tek ölçü)
bool isDelta; // nokta/spot ışık: MIS uygulanmaz
};
struct PathState { // bir yolun sekmeler arasında taşıdığı her şey
Spectrum throughput; // Π f·cos/pdf
Spectrum L; // birikmiş radyans
SampledWavelengths lambda; // spektral modda 4 dalga boyu (bölüm 3)
float prevBsdfPdf; // bir sonraki emisyon vuruşunda MIS ağırlığı için
LobeFlags prevLobe; // specular sonrası MIS kapatılır
uint16_t depth, diffuseDepth, transmissionDepth;
uint32_t pixel, sampleIndex; // RNG'nin tohumu: determinizm buradan gelir
float etaScale; // Russian roulette için kırılma düzeltmesi
MediumId medium; // ışının şu an içinde bulunduğu hacim (yoksa NoMedium)
};Bu tiplerin arkasındaki kararlar ve sebepleri:
HitileShadingPointayrı tutulur. Kesişim donanımı (OptiX, Embree) yalnızcat, ID'ler ve barycentric döndürür. Geri kalanını hesaplamak pahalı, üstelik gölge ışınlarında hiç gerekmiyor. Wavefront'ta kuyrukta yalnızcaHittaşındığı için bellek trafiği de düşüyor.Spectrumtek bir tip, ama iki uygulaması var. RGB modunda birfloat3, spektral modda dört dalga boyunun değeri. Hangisinin kullanılacağı derleme zamanında seçildiği için (#ifveya template) kernel kodu iki modda da aynı kalıyor.- Tüm pdf'ler katı açı ölçüsünde. Alan ışıkları alan ölçüsünde örneklenir, ama
LightSampledönmeden önce katı açıya çevrilir. MIS hatalarının en yaygın kaynağı bu iki ölçüyü karıştırmak. PathStatesınıf olarak değil, düz bir kayıt olarak tasarlanır. Megakernel'da register'larda, wavefront'ta SoA tamponda yaşar (bkz. Wavefront veri düzeni). Bu yüzden ona metot eklemek yerine onu işleyen serbest fonksiyonlar yazarsın.
Unity'den geliyorsan bu sana tanıdık gelecek: DOTS'taki IComponentData struct'larıyla aynı disiplin. Davranışı veriden ayrı tutarsın ki veri önbellek dostu dizilerde yaşayabilsin.
İki sahne: yazar sahnesi ve kernel sahnesi
Motor aynı sahneyi iki ayrı biçimde tutar ve ikisi arasındaki tek köprü sahne derleyicisidir. Artımlı güncelleme de CPU/GPU paritesi de bu temel ayrıma dayanıyor.
Yazar sahnesi (scene/) | Kernel sahnesi (compile/ çıktısı) | |
|---|---|---|
| Biçim | Nesne grafiği: node'lar, işaretçiler, string isimler | Düz diziler, 32-bit indeksler, SoA |
| Kim yazar | Hydra delegate, USD okuyucu, Python API | Yalnızca derleyici |
| Kim okur | Derleyici | Yalnızca kernel |
| Bellek | Host heap, std::vector, shared_ptr | Device tamponları (VRAM veya pinned RAM) |
| Değişim | Her an; dirty bayrağı atanır | Yalnızca render durduğunda, çift tampon ile |
| Geometri | Ham mesh, subdiv cage, eğri kontrol noktaları | Tessellate edilmiş üçgenler, BLAS/TLAS |
| Malzeme | Node grafiği (MaterialX/OSL) | Sabit düzende parametre bloğu + bytecode veya derlenmiş fonksiyon ID'si |
// scene/ — yazar tarafı: zengin, değişebilir
class Mesh : public Node {
public:
std::vector<Vec3> positions; std::vector<int> faceVertexCounts, indices;
Primvars primvars; // UV, normal, renk; USD'den gelen adıyla
SubdivScheme subdiv = SubdivScheme::None;
Ref<MaterialGraph> material;
DirtyBits dirty; // Topology | Points | Primvars | Material | Transform
};
// compile/ çıktısı — kernel tarafı: düz, salt okunur, device'ta
struct KernelScene {
DeviceSpan<KernelInstance> instances; // transform, blasId, materialOffset, visibility mask
DeviceSpan<KernelMaterial> materials; // tag + parametre bloğunun ofseti
DeviceSpan<float> materialParams;
DeviceSpan<KernelLight> lights;
DeviceSpan<LightTreeNode> lightTree;
DeviceSpan<TextureHandle> textures; // texture cache'e indeks (bölüm 8)
AccelHandle tlas; // OptiX/Embree'nin opak tutamacı
KernelCamera camera;
KernelFilm film; // AOV ofsetleri, çözünürlük
KernelIntegratorSettings settings; // maxDepth, clamp, RR derinliği
};Derleyici üç alt derleyiciden oluşur ve her biri yalnızca kendi dirty bayraklarına bakar:
- GeometryCompiler: subdiv ve displacement'ı uygular, üçgenleri üretir, BLAS'ları kurar ya da refit eder ve TLAS'ı yeniden kurar. En pahalı adım bu, o yüzden topoloji değişmediyse yalnızca refit yapılır.
- MaterialCompiler: malzeme grafiğini kernel'ın anlayacağı biçime çevirir. Bu biçim ya sabit bir über-malzemenin (OpenPBR) parametre bloğu olur ya da OSL/MaterialX'ten üretilmiş kod. Texture referanslarını da handle'lara dönüştürür.
- LightCompiler: emisif üçgenleri ve analitik ışıkları toplar, güçlerini hesaplar, light tree'yi kurar ve ortam ışığı için önem örnekleme tablosunu (2D CDF) üretir.
Unity'de bunun karşılığı, GameObject sahnesiyle DOTS'taki baking adımının ürettiği entity chunk'ları arasındaki ilişki. Unreal'da da aynı deseni UPrimitiveComponent (oyun thread'i) ile FPrimitiveSceneProxy (render thread'i) arasında görürsün: oyun tarafı nesneyi değiştirir, render tarafı kendi kopyasını okur.
GPU'da polimorfizm: virtual yerine etiketli işaretçi
Kernel'da "bu malzeme hangi tür?" sorusunu sanal fonksiyonla yanıtlayamazsın; onun yerine bir etikete (tag) bakan bir switch kullanılır. Sebebi şu: GPU'da vtable işaretçisi host adresini gösterir, yani çalışmaz. Çalışsa bile dolaylı çağrı inline edilemez ve register baskısı artar.
// PBRT-v4'ün TaggedPointer fikrinin sadeleştirilmiş hali
enum class BxDFType : uint8_t { Diffuse, Conductor, Dielectric, OpenPBR, Hair, Count };
struct BSDFRef {
BxDFType type;
uint32_t paramOffset; // KernelScene::materialParams içinde
};
KERNEL_FN BSDFSample sampleBSDF(const BSDFRef& b, const KernelScene& s,
const ShadingPoint& sp, Vec3 wo, Vec3 u) {
const float* p = &s.materialParams[b.paramOffset];
switch (b.type) { // derleyici her dalı inline eder
case BxDFType::Diffuse: return DiffuseBxDF::load(p).sample(sp, wo, u);
case BxDFType::Conductor: return ConductorBxDF::load(p).sample(sp, wo, u);
case BxDFType::Dielectric: return DielectricBxDF::load(p).sample(sp, wo, u);
case BxDFType::OpenPBR: return OpenPBRBxDF::load(p).sample(sp, wo, u);
case BxDFType::Hair: return HairBxDF::load(p).sample(sp, wo, u);
}
return {};
}Işıklar, kameralar, texture node'ları ve hacimler için de aynı desen tekrarlanır. PBRT-v4 bunu bir template ile genelleştirir: TaggedPointer<DiffuseBxDF, ConductorBxDF, …> işaretçinin üst bitlerinde tipi saklar ve Dispatch() derleme zamanında switch'i üretir.
| Yaklaşım | Artı | Eksi | Kim kullanır |
|---|---|---|---|
| Sanal fonksiyon | Doğal C++, kolay genişletme | GPU'da yok; CPU'da da inline yok | Eski CPU motorları, Mitsuba 0.x |
Etiket + switch | Hızlı, CPU/GPU ortak kod | Yeni tip = merkezi enum'a ekleme | PBRT-v4, Cycles (SVM node'ları) |
| OptiX callable program / SBT | Malzemeler ayrı derlenir, eklenti dostu | Yalnızca OptiX; çağrı maliyeti | Octane, OptiX tabanlı motorlar |
| Kod üretimi (OSL/MaterialX → kernel) | Malzeme başına tam optimize kod | Derleme süresi, karmaşık pipeline | Arnold GPU, RenderMan XPU |
| Yorumlanan bytecode (shader VM) | Sınırsız node grafiği, derleme yok | Yavaş, register baskısı | Cycles SVM |
Benim önerim şu: çekirdek BxDF'ler için etiket + switch, kullanıcıların kurduğu node grafikleri için de Cycles tarzı bir SVM (Shader Virtual Machine) ya da OSL. Unity tarafından bakarsan, Shader Graph'ın tek bir HLSL'e derlenmesi "kod üretimi" yaklaşımına giriyor; Unreal'ın Material Editor'ü de aynı şekilde HLSL üretiyor.
Device katmanı ve bellek sahipliği
Device katmanı, "kernel'ı nerede çalıştıracağım" sorusunu motorun geri kalanından gizleyen ince bir arayüz. Üst katmanlar bu arayüz üzerinden yalnızca tampon ayırır, veri kopyalar, ivme yapısı kurar ve kernel başlatır; CUDA'yı ya da Embree'yi hiç görmezler.
// device/device.h
class Device {
public:
virtual ~Device() = default; // host tarafında virtual serbest
virtual DeviceInfo info() const = 0; // isim, VRAM, SER/OptiX desteği
// Bellek
virtual DeviceBuffer alloc(size_t bytes, MemoryKind kind) = 0; // Device | Pinned | Managed
virtual void upload(DeviceBuffer& dst, const void* src, size_t bytes, Stream s) = 0;
virtual void download(void* dst, const DeviceBuffer& src, size_t bytes, Stream s) = 0;
// İvme yapıları (bölüm 2)
virtual AccelHandle buildBLAS(const GeometryDesc&, BuildFlags) = 0;
virtual void refitBLAS(AccelHandle, const GeometryDesc&) = 0;
virtual AccelHandle buildTLAS(Span<const InstanceDesc>) = 0;
// Çalıştırma
virtual void launch(KernelId k, const LaunchParams& p, uint32_t workItems, Stream s) = 0;
virtual void synchronize(Stream s) = 0;
};
std::unique_ptr<Device> createDevice(DeviceType); // CPU, OptiX, HIP, Metal, MultiBellek sahipliğinde şu üç kural, seni sonradan sızıntı ve yarış durumu avlamaktan kurtarır:
- Device tamponlarının tek sahibi derleyicinin ürettiği
KernelSceneStorage'dır.KernelSceneiçindekiDeviceSpan'ler ise sahip olmayan görünümlerdir (işaretçi + uzunluk) ve kernel'a değer olarak kopyalanır. - RAII tutamaçları.
DeviceBufferyıkıcısında serbest bırakılır, ama bu iş o tampona erişen son kernel bitene kadar ertelenir (bunun için stream'e bağlı bir "silinecekler" kuyruğu tutulur). Bunu yapmazsan, render sürerken sahne değiştiğinde GPU çoktan serbest bırakılmış belleği okur. - Çift tamponlu sahne. Derleyici yeni
KernelScene'i ayrı tamponlara yazar; render kesildiğinde yeni sahneye tek bir işaretçi değişimiyle geçilir. Malzeme parametresi gibi küçük değişikliklerde yerinde güncelleme yapılır, ama yalnızca render durdurulmuşken.
MultiDevice de aynı arayüzü uygulayan ve çağrıları altındaki cihazlara dağıtan bir bileşim. Sahne her cihaza kopyalanır, çerçeve karçalara bölünür, sonunda da filmler birleştirilir (bölüm 8, Çoklu GPU). Unreal'da bunun karşılığı D3D12, Vulkan ve Metal'i aynı şekilde gizleyen RHI (Render Hardware Interface); Unity'de ise GraphicsBuffer + CommandBuffer ikilisi.
Integrator: bir yolu kodda izlemek
Birinci bölümdeki tekniklerin hepsi tek bir döngüde buluşur. Aşağıdaki megakernel sürümü, motorda en çok okuyacağın 60 satır; her satırın hangi bölüme ait olduğunu yorumlarda bulabilirsin.
// kernel/integrator/path.h — bir piksel, bir örnek
KERNEL_FN void integratePixelSample(const KernelScene& S, uint32_t pixel, uint32_t sampleIndex,
FilmWriter& film) {
Sampler rng(pixel, sampleIndex, S.settings.seed); // Sobol + Owen (bölüm 4)
CameraSample cs = generateCameraSample(rng, pixel, S.film); // piksel filtresi için jitter
Ray ray = S.camera.generateRay(cs); // thin lens, zaman (bölüm 5)
PathState ps = PathState::start(pixel, sampleIndex, rng); // throughput = 1, L = 0
AOVRecorder aov; // ilk sekmede albedo/normal/derinlik
for (;;) {
Hit hit;
bool found = traceClosest(S.tlas, ray, hit); // OptiX/Embree (bölüm 2)
// --- Hacim: ray boyunca bir ortam varsa önce o örneklenir (bölüm 6)
if (ps.medium != NoMedium) {
MediumEvent ev = sampleMedium(S, ps, ray, found ? hit.t : INF, rng);
if (ev.scattered) { scatterInMedium(S, ps, ray, ev, rng); continue; }
}
if (!found) { // kaçan ışın: ortam ışığı
addEmission(ps, evalEnvironment(S, ray), envPdf(S, ray)); // MIS ağırlıklı
break;
}
ShadingPoint sp = expandHit(S, hit, ray); // normal, UV, türevler
if (sp.lightId != NoLight) // yüzey ışık yayıyorsa
addEmission(ps, evalAreaLight(S, sp, -ray.d), lightPdf(S, sp, ray));
BSDF bsdf = evalMaterial(S, sp, rng); // texture, node grafiği (bölüm 3)
if (ps.depth == 0) aov.recordPrimary(sp, bsdf); // denoiser girdileri
if (++ps.depth > S.settings.maxDepth) break;
// --- NEE: bir ışık seç, gölge ışını at (bölüm 1, light tree)
if (!bsdf.isPureSpecular()) {
LightSample ls = sampleLights(S, sp, rng.get3D());
if (ls.pdf > 0 && !traceOcclusion(S.tlas, spawnRay(sp, ls.wi, ls.dist))) {
Spectrum f = bsdf.eval(-ray.d, ls.wi) * absDot(ls.wi, sp.ns);
float w = ls.isDelta ? 1.f : powerHeuristic(ls.pdf, bsdf.pdf(-ray.d, ls.wi));
ps.L += ps.throughput * f * ls.Le * (w / ls.pdf);
}
}
// --- BSDF örnekle, yolu uzat
BSDFSample bs = bsdf.sample(-ray.d, rng.get1D(), rng.get2D());
if (bs.pdf == 0) break;
ps.throughput *= bs.f * absDot(bs.wi, sp.ns) / bs.pdf;
ps.prevBsdfPdf = bs.pdf; ps.prevLobe = bs.lobe;
if (bs.lobe & Transmission) ps.etaScale *= sqr(bs.eta);
// --- Russian roulette (bölüm 1)
if (ps.depth > S.settings.rrStartDepth) {
float q = max(0.05f, 1 - maxComponent(ps.throughput * ps.etaScale));
if (rng.get1D() < q) break;
ps.throughput /= 1 - q;
}
ray = spawnRay(sp, bs.wi); // offset ile (bölüm 2, kararlılık)
}
film.addSample(pixel, cs.filmPos, clampFirefly(ps.L, S.settings), aov); // bölüm 4 + 10
}Bu fonksiyonun çevresinde yalnızca iki parça kalıyor: CPU'da parallel_for onu kareçiklere (tile, örneğin 32×32 piksel) dağıtıyor, GPU'da ise OptiX'in ray generation programı onu her piksel için çağırıyor. MIS ağırlığı addEmission içinde hesaplanır: ps.prevBsdfPdf, o noktaya ışık örneklemesiyle ulaşmanın pdf'iyle karşılaştırılır. İlk sekmede ve specular bir sekmeden sonra ağırlık 1'dir. Light group ve LPE (bölüm 10) devreye girince ps.L tek bir değer olmaktan çıkar; FilmWriter her katkıyı yol etiketine göre ilgili AOV'ye yazar.
Wavefront veri düzeni: SoA yol durumu ve iş kuyrukları
Wavefront'ta yukarıdaki döngüyü parçalara ayırırsın: her // --- bloğu ayrı bir kernel olur, aralarındaki veri de PathState dizilerinde ve kuyruklarda taşınır. Mantık aynı kalıyor; değişen tek şey verinin nerede yaşadığı.
// SoA: her alan ayrı bir dizi; warp'taki 32 thread bitişik adresleri okur (coalesced)
struct PathStateSoA {
DeviceSpan<float> throughputR, throughputG, throughputB;
DeviceSpan<float> LR, LG, LB;
DeviceSpan<float> prevBsdfPdf;
DeviceSpan<uint16_t> depth;
DeviceSpan<uint32_t> pixel, sampleIndex;
DeviceSpan<Vec3> rayO, rayD;
// ...
};
template <typename T>
struct WorkQueue { // atomik sayaçlı sabit kapasiteli kuyruk
DeviceSpan<T> items;
uint32_t* count; // device belleğinde
KERNEL_FN void push(const T& t) { items[atomicAdd(count, 1)] = t; }
};
struct ShadeItem { uint32_t pathIdx; Hit hit; };
struct ShadowItem { uint32_t pathIdx; Ray ray; Spectrum contrib; }; // ışık örneğinin bekleyen katkısıBir sekmenin kernel sırası ve aralarındaki kuyruklar:
generateCameraRays: boş yuvalara yeni yol yazar.intersectClosest: her yol içinHitbulur; yüzeye çarpanlarıShadeItemolarak malzeme türüne göre ayrı kuyruklara (queues[BxDFType]), kaçanlarımissQueue'ya, hacimdekilerimediumQueue'ya iter.shadeMissvesampleMediumkendi kuyruklarını işler.- Her malzeme türü için bir
shade<BxDFType>kernel'ı: BSDF'i değerlendirir, NEE içinShadowItemüretir, BSDF örnekleyip yeni ray'iPathStateSoA'ya yazar, RR ile ölenleri işaretler. intersectShadow: gölge kuyruğunu toplu test eder, görünenlerincontrib'iniL'ye ekler.- Ölen yolların sonucu filme yazılır ve yuvaları boş listesine geri gider; döngü 1. adımdan devam eder.
Aktif yol sayısı her sekmede azalır. Bu yüzden 1. adım boşalan yuvaları yeni piksellerle doldurur ("path regeneration") ve GPU hep dolu kalır. Tampon boyutu (örneğin 1-2 milyon yol), VRAM ile doluluk arasında yaptığın bir takas; başlangıçta bir kez ayrılır. Kuyruk sayaçlarını her kernel'dan sonra host'a okumazsın. Onun yerine ya dolaylı başlatma (indirect dispatch) kullanılır ya da kernel sabit boyutla başlatılır ve işi olmayan thread'ler erkenden çıkar. Bu düzenin okuması kolay örneklerini görmek istersen Cycles X'in integrator_state'ine ve PBRT-v4'ün wavefront/ dizinine göz atabilirsin.
Oturum yaşam döngüsü
Session bir durum makinesidir. IPR ile final render arasındaki tek fark, "Yakınsadı" durumundan sonra nereye gidildiği: IPR yeni bir değişiklik bekler, CLI ise çıktıyı yazıp çıkar.

oturum yaşam döngüsü · 7 durum
Durum geçişlerini açık bir enum class SessionState ve tek bir geçiş fonksiyonuyla yazarsan, "render sürerken sahne silindi" gibi yarış durumlarını tek bir yerde çözersin. İptal de aynı kanaldan geçer: kernel'lar her batch'in sonunda bir bayrağa bakar, yani uzun bir kernel yarıda kesilmez. Unreal'ın Movie Render Queue'su da Blender'ın render oturumu da aynı şemayı izler.
Değişiklik yayılımı: ne değişince ne yeniden kurulur
IPR'ın hızı, her değişikliğin olabilecek en dar yeniden kurulumu tetiklemesine bağlı. Aşağıdaki tabloda her dirty bayrağının derleyicide neye dönüştüğünü görebilirsin. Amaç, sanatçının en sık yaptığı üç işin (kamera, ışık, malzeme parametresi) tablonun en ucuz satırlarında kalması.
| Değişiklik | Dirty bayrağı | Derleyicide yapılan | Göreceli maliyet |
|---|---|---|---|
| Kamera hareketi, odak, pozlama | Camera | KernelCamera güncellenir, film sıfırlanır | En ucuz |
| Film çözünürlüğü, AOV listesi | Film | Film tamponları ve wavefront kuyrukları yeniden ayrılır | Ucuz |
| Malzeme parametresi (renk, roughness) | MaterialParams | Yalnızca o malzemenin parametre bloğu yüklenir | Ucuz |
| Işık yoğunluğu veya rengi | LightParams | Işık kaydı güncellenir, light tree'de güç değerleri refit edilir | Ucuz |
| Nesne transform'u | Transform | TLAS yeniden kurulur veya refit edilir; emisifse light tree refit | Orta |
| Işık ekleme veya silme | LightSet | Light tree ve ışık dağılımı yeniden kurulur | Orta |
| Texture dosyası değişti | Texture | Texture cache'te o dosyanın karçaları geçersiz kılınır | Orta |
| Malzeme grafiğinin yapısı (node eklendi) | MaterialGraph | Malzeme yeniden derlenir; kod üretimi kullanılıyorsa kernel yeniden derlenebilir | Pahalı |
| Vertex pozisyonları (deformasyon) | Points | BLAS refit; displacement varsa yeniden tessellation | Pahalı |
| Topoloji, subdiv seviyesi | Topology | Tessellation + BLAS sıfırdan kurulur | En pahalı |
Uygulamada her node bir DirtyBits taşır. Session::sync() sahneyi gezip bu bayrakları toplar ve derleyiciye tek bir SceneDelta olarak verir. Derleyici de bu delta'yı yukarıdaki satırlara eşler, işi bitince bayrakları temizler. Bayrakları, yayılması gereken bağımlılara da işlemen gerekiyor: bir texture değişince onu kullanan malzemeler, emisif bir malzeme değişince de light tree dirty olur. Hydra'nın HdChangeTracker'ı aynı bilgiyi delegate'e zaten bu biçimde veriyor (bölüm 11).
İş parçacığı modeli
Motor beş tür thread ile çalışır. Bunların ortak kullandığı veri yapıları yalnızca sahne ve film tamponu, ikisine erişim de belirli senkronizasyon noktalarıyla sınırlı. Bu disiplini kurmazsan IPR'da "sahne değişti, kernel eski tampondan okudu" türünden, nadir görülen ve yeniden üretemediğin çökmeler er geç karşına çıkar.
| Thread | Görevi | Sahneye erişimi | Senkronizasyon |
|---|---|---|---|
| Host / UI (DCC veya viewer) | Kullanıcı düzenlemeleri, Hydra Sync çağrıları | Yazar sahnesine yazar | Sahne mutex'i; düzenleme sırasında render durdurma isteği |
| Session (1 adet) | Durum makinesi, derleyiciyi çağırma, render döngüsünü sürme | Yazar sahnesini okur, kernel sahnesini üretir | std::condition_variable ile uyandırılır; iptal bayrağı |
| CPU worker havuzu (oneTBB) | CPU kernel'ı: kareçik başına integratePixelSample; derleyicide paralel BVH kurulumu | Kernel sahnesini salt okur | Kareçik kuyruğu; filme kareçik başına yazım (atomik yok) |
| GPU stream'leri | Kernel başlatma, kopyalama | Device tamponlarını okur | Stream sırası, event'ler; host yalnızca okuma geri alırken bekler |
| I/O havuzu | Texture karçalarını diskten okuma (bölüm 8), EXR yazma | Yok | Kilitsiz istek kuyruğu; render thread'i eksik karçayı beklemez, düşük mip kullanır |
Session thread'inin ana döngüsü, progressive render'ın tamamını özetliyor:
void Session::run() {
while (!stopRequested) {
if (SceneDelta d = scene.collectChanges(); !d.empty()) {
device->synchronize(renderStream); // uçuştaki kernel'lar biter
compiler.apply(d, kernelScene); // yalnızca dirty olanlar (tablo yukarıda)
film.reset(); sampleIndex = 0; // birikim sıfırlanır
}
if (sampleIndex >= settings.maxSamples || adaptive.converged()) { waitForChanges(); continue; }
uint32_t batch = scheduler.nextBatchSize(); // IPR'da 1 spp, final'de 16–64 spp
integrator->render(kernelScene, film, sampleIndex, batch, renderStream);
sampleIndex += batch;
if (display.wantsUpdate()) film.resolveToDisplay(displayStream); // ayrı stream, render'ı durdurmaz
if (adaptive.enabled) adaptive.update(film, sampleIndex); // bölüm 4
}
}Batch boyutu önemli bir ayar. IPR'da küçük tutulur ki kullanıcı bir ışığı sürüklediğinde değişikliği birkaç milisaniye içinde görsün. Final render'da ise büyük tutulur ki kernel başlatma ve senkronizasyon maliyeti gizlensin. Unreal aynı problemi Game Thread, Render Thread ve RHI Thread ayrımıyla çözüyor; Unity'de bunun karşılığı ana thread, render thread ve Job System worker'ları.
Film ve çıktı hattı
Kernel'ın yazdığı tek yer film. Film her şeyi doğrusal, ham toplamlar olarak tutar; bölme, denoise ve renk dönüşümü ancak okuma anında yapılır. Bu sayede aynı birikim hem viewer'ı hem dosyayı besler, render'ı da istediğin anda kesip kaldığı yerden devam ettirebilirsin.

film ve çıktı hattı · 2 yol
Toplam ve ağırlık ayrı tutulduğu için iki render'ı (mesela iki makinede yapılmış iki yarıyı) basitçe toplayarak birleştirebilirsin. CPU'da her kareçik kendi bölgesine yazdığı için atomik işleme gerek kalmıyor. Geniş bir piksel filtresi komşu kareçiğe taşan örnekler üretiyorsa iki seçeneğin var: ya kareçiğin kenarında bir kenar bandı tutarsın ya da filtreyi örneklemeyle uygularsın (filter importance sampling: örnek konumu filtreye göre dağıtılır, her örnek de tek bir piksele yazılır). PBRT-v4 ikinci yolu seçiyor; bu yol determinizm açısından da daha güvenli.
Genişletme noktaları: yeni bir özellik nereye eklenir
Bir mimarinin iyi olup olmadığını, yeni bir özellik eklerken kaç modüle dokunman gerektiğinden anlarsın. Aşağıda sık eklenen özellikler için hangi yerlere dokunulduğunu topladım. Bir satır beşten fazla dosya gerektiriyorsa sınırlar yanlış çizilmiş demektir.
| Eklenecek | scene/ | compile/ | kernel/ | film/ | Diğer |
|---|---|---|---|---|---|
| Yeni BxDF (örn. sheen) | Malzeme node'u + parametreleri | Parametre bloğuna yazma | Yeni BxDF struct'ı + switch dalı | - | Furnace ve χ² testi |
| Yeni ışık tipi (örn. IES profilli spot) | Light alt tipi | Light tree'ye güç ve sınır kutusu | sample/pdf/eval | - | IES okuyucu io/'da |
| Yeni kamera (örn. fisheye) | Camera alt tipi | KernelCamera doldurma | generateRay dalı | - | - |
| Yeni AOV (örn. motion vector) | - | Film düzenine kanal | Integrator'da yazma noktası | Kanal tanımı + EXR adı | Hydra'da AOV binding |
| Yeni geometri tipi (örn. nokta bulutu) | Node | BLAS kurulumu (custom primitive) | Kesişim + expandHit | - | OptiX'te intersection programı |
| Yeni backend (örn. Vulkan) | - | - | Gerekirse makro uyarlaması | - | device/vulkan/ |
Kayıt (registry) desenini yalnızca sahne ve uygulama tarafında kullanırsın, kernel'da yeri yok:
// scene/node_registry.h — isimden node üretmek (USD/Hydra ve Python için)
class NodeRegistry {
public:
using Factory = std::function<Ref<Node>()>;
void add(std::string_view typeName, Factory f, ParamSchema schema);
Ref<Node> create(std::string_view typeName) const; // "OpenPBRSurface" → node
const ParamSchema& schema(std::string_view typeName) const; // UI ve doğrulama için
};
// Her node tipi kendini statik olarak kaydeder
REGISTER_NODE(OpenPBRSurface, {
param<Color>("base_color", {0.8f, 0.8f, 0.8f}),
param<float>("specular_roughness", 0.3f, Range{0, 1}),
// ...
});Parametrelerin asıl tanımı ParamSchema'da duruyor. Hydra'dan gelen parametreler onunla doğrulanıyor, Python binding'leri ondan üretiliyor, viewer'daki UI ona bakılarak çiziliyor ve derleyicinin parametre bloğu düzeni de ondan çıkıyor. Kernel tarafında ise genişletmeyi bilerek derleme zamanında yaparsın (enum + switch). Çalışma zamanında eklenti yüklemek istiyorsan yol OSL'den ya da OptiX callable program'larından geçiyor. Unreal'da bu işi UCLASS + reflection sistemi yapıyor; Unity'de [SerializeField] ve Inspector.
Tek bir örneği izlemek: USD dosyasından EXR pikseline
Mimarinin tamamını kafanda tek seferde oturtmanın en iyi yolu, tek bir piksel örneğinin peşine takılıp onu baştan sona izlemek. Örnek sahnede masanın üstünde cam bir şişe var; ışık, pencereden giren bir HDRI ile bir alan ışığından geliyor.
- Yükleme (
io/→scene/). Her şey CLI'darender shot.usd --frame 101ile başlar. USD okuyucu prim'leri gezer veMesh,Light,Cameranode'larınıNodeRegistryüzerinden üretir. MaterialX ağı birMaterialGrapholur. Texture'lar henüz okunmaz, yalnızca yolları kaydedilir. Son olarak bütün node'larDirty::Allile işaretlenir. - Derleme (
compile/). Session derleyiciyi çağırır. GeometryCompiler şişenin subdiv cage'ini OpenSubdiv ile tessellate eder ve BLAS kurar; masa ve şişe instance'ları TLAS'a girer. MaterialCompiler camıDielectric, ahşabıOpenPBRetiketiyle parametre tamponuna yazar, texture yollarını handle'lara çevirir. LightCompiler HDRI'ın 2D CDF'ini ve alan ışığıyla birlikte light tree'yi kurar. - Yükleme (
device/).KernelScenetamponları VRAM'e kopyalanır, film ve wavefront tamponları ayrılır. SessionRenderingdurumuna geçer. - Kamera ışını (
kernel/camera). Piksel (812, 455), örnek 37 için Sobol + Owen beş boyut üretir: piksel içi konum (2), lens üzerindeki nokta (2), zaman (1). Thin lens kamerası bir ray üretir, ray cone genişliği piksel açısından hesaplanır. - Kesişim (
device/optix). Işın TLAS'tan şişenin BLAS'ına, oradan da 41.207 numaralı üçgene iner. Kernel'a dönen yalnızcaHit {t, instId, primId, bary}. - Yüzey bilgisi (
kernel/).expandHitvertex normallerini barycentric ile karıştırır, instance transform'unu uygular, UV ve türevleri hesaplar.materialIdcamı gösterir; cam ışık yaymadığı için emisyon eklenmez. İlk sekme olduğu için albedo ve normal AOV'lere yazılır. - Malzeme (
kernel/bsdf).Dielectricetiketiswitch'te kendi dalına gider. Cam tamamen specular olduğu için NEE atlanır. Fresnel'e göre kırılma seçilir;etaScalegüncellenir, ışın şişenin içine girer. - Sonraki sekmeler. Işın camın arka yüzünden çıkar, ahşap masaya çarpar. Texture cache, ray cone'un bu mesafedeki genişliğine göre mip 3'ü ister; karça bellekte yoksa I/O havuzuna istek gider ve bu örnek mip 5 ile devam eder. Ahşap glossy olduğu için bu kez NEE çalışır: light tree alan ışığını seçer, gölge ışınının önü açık çıkar ve katkı MIS ağırlığıyla
L'ye eklenir. BSDF örneklemesi de ışını pencereye yönlendirir. - Kaçış. Işın hiçbir şeye çarpmaz. HDRI değerlendirilir, ortam ışığı örneklemesinin pdf'iyle MIS ağırlığı hesaplanır ve yol burada biter. (Bu yol cam üzerinden kırılmış bir kaustik taşıdığı için yüksek değerli bir firefly olabilir; clamp burada devreye girer.)
- Film (
film/).addSampleradyansı piksel filtresi ağırlığıylabeautykanalına, karesini varyans kanalına, light group etiketine görelg_windowkanalına yazar. - Adaptif döngü (
session/). 64 örnekte bir adaptive sampling, bu pikselin varyansının henüz yüksek olduğunu görür ve onu aktif listede tutar; masanın üstündeki düz bölgeler çoktan durmuştur. - Çıktı (
film/). Örnek bütçesi dolunca birikimler ağırlığa bölünür, OIDN beauty + albedo + normal ile denoise eder ve çok katmanlı bir EXR (ACEScg, doğrusal) diske yazılır. Viewer'da aynı veri OCIO görüntüleme dönüşümünden geçip ekrana çıkar, ama EXR'a bu görüntüleme dönüşümü uygulanmaz.
1. Işık taşınımı
Render dediğimiz şey, bir integral denklemini çözmek. Path tracing bu integrali rastgele yollarla tahmin eder. Bu bölümdeki öteki tekniklerin hepsi de o tahminin varyansını (yani gürültüyü) düşürmek için var.
Rendering equation
Kajiya'nın 1986'da yazdığı denklem, bir noktadan çıkan ışığın kendi yaydığı ışık artı tüm yönlerden gelip yansıyan ışık olduğunu söyler:
Gelen ışık L_i, başka bir noktanın giden ışığıdır; denklem de bu yüzden özyinelemeli. Veach'ın path space formülasyonu (1997) onu "tüm olası ışık yolları üzerinde tek bir integral" olarak yeniden yazar. BDPT, VCM ve MLT gibi ileri algoritmaların hepsi bu bakış açısından türetilir; o yüzden Veach'ın tezini mutlaka okumanı öneririm.
Monte Carlo tahmincisi
İntegrali analitik çözemediğimiz için rastgele örneklerle tahmin ederiz:
Hata O(1/√N) hızında azalır, yani gürültüyü yarıya indirmek için dört kat örnek gerekir. Bu yüzden emeğin çoğu, örnekleme dağılımı p'yi integrand f'ye benzetmeye gider (importance sampling). Burada iki terimi birbirine karıştırmamak lazım. Unbiased bir tahmincinin beklenen değeri her N için doğrudur. Consistent bir tahminci ise sonlu N'de yanlı olabilir ama N büyüdükçe doğruya yakınsar; photon mapping buna bir örnek.
Path tracing
Kameradan bir ışın atılır, çarptığı yüzeyde BSDF'ten yeni bir yön örneklenir ve yol devam eder. Throughput (kodda beta) her sekmede f·cos/pdf ile çarpılır. Saf path tracing, ancak ışık kaynağına tesadüfen çarparsa katkı alır. Sahnede küçük bir ampul ya da nokta ışık varsa sonuç neredeyse tamamen gürültü olur. Sıradaki iki tekniği zorunlu kılan da bu.
NEE (Next Event Estimation)
Yolun her köşesinde, bir ışık kaynağının üzerinden doğrudan bir nokta örneklenir ve o noktaya bir shadow ray atılır. Nokta ve directional ışıkları görmenin tek yolu bu. Alan ışıklarında pdf alan ölçüsünde çıkar; BSDF ile birleştirebilmek için onu katı açı ölçüsüne çevirmen gerekir: p_ω = p_A · d² / |cos θ_l|. Bu dönüşümü unutmak, yeni yazılan path tracer'larda en sık görülen enerji hatası.
MIS (Multiple Importance Sampling)
Aynı ışık katkısına iki stratejiyle ulaşabilirsin: BSDF'ten örnekleyip ışığa çarparak ya da ışığı doğrudan örnekleyerek. Parlak ve dar bir BSDF'te ışık örneklemesi kötü sonuç verir, büyük bir ışıkta ise BSDF örneklemesi. MIS ikisini ağırlıklandırarak birleştirir; pratikte standart olan da Veach'ın power heuristic'i:
Uygularken iki kurala dikkat etmen lazım. Birincisi, BSDF ile sekip bir ışığa çarptığında da ağırlığı uygularsın; yoksa direkt ışık iki kez sayılır. İkincisi, delta dağılımlarda (mükemmel ayna, nokta ışık) MIS yoktur, çünkü öteki strateji o katkıyı hiçbir zaman bulamaz.
Russian roulette
Yol q olasılığıyla sonlandırılır; devam ederse throughput 1/(1−q) ile büyütülür. Bu sayede sonsuz derinliği bias eklemeden kısaltmış olursun. Hayatta kalma olasılığı genelde throughput'un en büyük bileşeninden türetilir ve ilk 3-5 sekmede uygulanmaz. Bunun yanında sabit bir maksimum derinlik de kullanılır. Bu teknik olarak bias'lı, ama pratikte gözle fark edilmez.
BDPT (Bidirectional Path Tracing)
Hem kameradan hem ışıktan birer yol üretilir. Sonra iki yolun bütün köşe çiftleri birbirine bağlanır ve her bağlantı stratejisi MIS ile ağırlıklandırılır. Işığın dar bir aralıktan girdiği sahnelerde (aralık kalmış bir kapıdan giren ışık, abajurun içindeki ampul) unidirectional PT'den kat kat iyi sonuç verir. Eksileri de var tabii: GPU'da verimli yazmak zor, bellek tüketimi yüksek ve kameradan görülen kaustikleri (specular-diffuse-specular, SDS yolları) o da çözemiyor.
Photon mapping ve VCM
Photon mapping'de ışıktan fotonlar atılır ve yüzeylerde depolanır. Kamera yolu bir yüzeye çarptığında da yakındaki fotonlar toplanır (density estimation). Havuzun dibindeki ya da camın ardından görülen kaustikler gibi SDS yollarını çözmenin pratik yolu bu. Yöntem bias'lı, ama progressive photon mapping (PPM, SPPM) toplama yarıçapını küçülterek consistent hale geliyor.
VCM (Vertex Connection and Merging, Georgiev ve ark. 2012) ile UPS (Hachisuka ve ark. 2012) aynı fikre farklı yollardan vardı: BDPT bağlantılarını ve foton birleştirmeyi tek bir MIS çatısı altında toplamak. Bu çatı altında her yol tipi için en iyi strateji otomatik seçiliyor. Corona'nın kaustik ve zor iç mekan modları da bu aileden besleniyor.
MLT (Metropolis Light Transport)
İyi bir yol bulunduktan sonra, küçük mutasyonlarla onun çevresi keşfedilir; pratikte kullanılan sürümü Kelemen'in PSSMLT'si. Çok zor sahnelerde iyi sonuç verir, ama görüntü düzensiz yakınsar ve animasyonda titreşim yaratır. Bu yüzden prodüksiyon motorlarında pek görmezsin; daha çok araştırmada ve referans render için kullanılıyor.
Path guiding
Render sürerken önceki örneklerden "ışık bu bölgeye hangi yönlerden geliyor" bilgisi öğrenilir, sonra bu dağılım BSDF ile karıştırılarak örneklenir. Yayınlanmış ana yaklaşımlar Practical Path Guiding (Müller ve ark. 2017, uzaysal-yönsel SD-tree) ve parallax-aware von Mises-Fisher karışımları (Ruppert ve ark. 2020). Intel'in Open PGL kütüphanesi bunu hazır olarak veriyor, Cycles da onu kullanıyor. Dolaylı ışığın baskın olduğu iç mekanlarda gürültüyü belirgin biçimde azaltır. Bedeli ise bir öğrenme fazı, ek bellek ve GPU'da zor bir uygulama.
MNEE (Manifold Next Event Estimation)
Shadow ray bir su ya da cam yüzeyinden geçmek zorundaysa, normal NEE o yüzeyin karşısında kör kalır. MNEE (Hanika ve ark. 2015) ve specular manifold sampling (Zeltner ve ark. 2020), ışığa giden geçerli kırılma yolunu Newton iterasyonuyla bulur. Cycles'taki "shadow caustics" özelliği de bu. Tek bir kırıcı arayüzde çok iyi çalışıyor, çok katmanlı cam yığınlarında ise zayıflıyor.
Many-lights: light tree ve ReSTIR
Binlerce ışığı olan bir şehir sahnesinde, NEE için ışığı uniform seçersen varyans patlar. Light BVH ya da light tree, ışıkları güçleri, konumları ve yön konileriyle bir hiyerarşide toplar. Shading noktasından bakarak her düğümün önemi tahmin edilir ve ağaçta stokastik olarak aşağı inilir (Conty ve Kulla 2018). Arnold da Cycles da bunu kullanıyor.
ReSTIR (Bitterli ve ark. 2020) başka bir yol izler: ucuz aday ışıklar üretir, reservoir sampling ile bunlardan birini seçer ve bu seçimi komşu piksellerle ve önceki frame'le yeniden kullanır. NVIDIA'nın RTXDI'si, ReSTIR GI ve ReSTIR PT de bu aileden. Offline final render'da light tree daha güvenli bir seçim. ReSTIR'in zamansal yeniden kullanımı bias'a eğilimli, ama interaktif önizlemede (viewport) çok işe yarıyor.
Algoritma karşılaştırması
| Algoritma | Güçlü olduğu yer | Zayıf olduğu yer | GPU uygunluğu |
|---|---|---|---|
| PT + NEE + MIS | Genel sahneler, dış mekan | Dar açıklıklar, kaustikler | Çok iyi |
| BDPT | Dolaylı aydınlatılan iç mekanlar | SDS kaustikleri, bellek | Zor |
| SPPM / photon mapping | Kaustikler | Bias, düşük yoğunluklu bölgeler | Orta |
| VCM / UPS | Neredeyse tüm yol tipleri | Karmaşıklık, çerçeve başına maliyet | Zor |
| PT + path guiding | Zor iç mekanlar | Öğrenme fazı, bellek | Orta |
| MLT | Çok zor yollar | Animasyonda titreşim | Zayıf |
2. Kesişim
Bir path tracer zamanının büyük kısmını "bu ışın neye çarptı?" sorusuna harcar. Hızı da çoğunlukla iki şey belirler: BVH'nin kalitesi ve traversal'ın donanıma ne kadar iyi oturduğu.
BVH (Bounding Volume Hierarchy)
Primitifleri iç içe eksen hizalı kutularda (AABB) gruplarsın; ışın bir kutuyu kaçırırsa o kutunun altındaki her şey atlanır. Böylece kesişim maliyeti, primitif sayısına göre doğrusal olmaktan çıkıp logaritmik hale gelir. Eskiden kd-tree de rakipti, ama BVH'yi güncellemek kolay ve bellek tüketimi öngörülebilir; bu yüzden bugün tek standart o. Traversal çekirdeğinde üç parça var: slab yöntemiyle ray-AABB testi, bir stack ve önce yakın çocuğu ziyaret etmek.
SAH (Surface Area Heuristic)
BVH'yi nereden böleceğine karar veren maliyet modeli. Dayandığı gözlem şu: rastgele bir ışının bir kutuya çarpma olasılığı, kutunun yüzey alanıyla orantılı. Buna göre her aday bölünme için beklenen maliyeti hesaplarsın:
Binned SAH (eksen başına 16-32 kova) hem hızlı kurulur hem de kaliteli bir ağaç verir. Mimari sahneler gibi uzun ince üçgenlerin bol olduğu yerlerde SBVH (Stich ve ark. 2009) primitifleri uzamsal olarak da bölüp kaliteyi artırır. GPU'da her frame yeniden kurulması gereken dinamik geometri içinse LBVH veya PLOC gibi hızlı build yöntemleri var. Hangisini seçersen seç, yaptığın takas build süresiyle trace hızı arasında. Offline render'da build bir kez yapıldığı için en kaliteli yapıyı seçmek neredeyse her zaman doğru karar. Animasyonda tam rebuild yerine refit yapmak (kutuları güncelle, topolojiyi koru) hızlıdır, ama ağacın kalitesi zamanla düşer.
Wide BVH ve SIMD
BVH4 ve BVH8'de her düğüm 4 ya da 8 çocuk taşır. Bu çocukların kutuları SoA düzeninde tutulduğu için hepsini tek bir SIMD komutuyla test edebilirsin. Embree, AVX2/AVX-512 ile BVH8 kullanır. GPU tarafında referans kabul edilen yapı, Ylitie ve ark.'nın (2017) sıkıştırılmış 8-wide BVH'si (CWBVH). Ray packet'ler (aynı yöne giden ışın grupları) birincil ışınlarda iyi çalışır, ama path tracing'in dağınık ikincil ışınlarında faydaları hızla düşer. Bu yüzden bugünkü CPU motorları tek-ışın + geniş düğüm yaklaşımını tercih ediyor.
Embree
Embree, Intel'in açık kaynaklı CPU ışın izleme çekirdekleri. Üçgen, quad, eğri (saç), subdivision yüzeyi, instancing, motion blur ve kullanıcı tanımlı geometri desteği var; ARM işlemcilerde de çalışıyor. Corona, V-Ray CPU, Arnold ve Cycles, CPU tarafında Embree kullanır. Kendi BVH'ni yazmak çok öğretici, ama prodüksiyonda Embree'yi geçmen yıllar alır.
OptiX
OptiX, NVIDIA'nın GPU ray tracing framework'ü ve RT core'lara ulaşmanın en doğrudan yolu. Programlama modeli birkaç programdan oluşur: raygen (ışın üretimi), closest-hit (en yakın kesişimde shading), any-hit (alfa maskesi gibi kesişimi reddetme), miss (çevre ışığı) ve intersection (özel primitifler). Hangi geometrinin hangi programı çağıracağına Shader Binding Table (SBT) karar verir. Octane, Redshift, V-Ray GPU ve Cycles OptiX kullanır. Yerleşik bir AI denoiser'la geliyor; en büyük eksisi de yalnızca NVIDIA'da çalışması.
HIP RT
HIP RT, AMD'nin HIP üzerine kurduğu ray tracing kütüphanesi ve RDNA kartlarındaki ray accelerator'ları kullanıyor. Cycles'ın AMD desteği de bunun üzerinde duruyor. Ekosistemi OptiX'e göre daha genç; denoiser gibi yan araçlar yok.
DXR ve Vulkan Ray Tracing
Bunlar üreticiden bağımsız API'ler: D3D12 üzerinde DirectX Raytracing ve Vulkan'ın VK_KHR_ray_tracing_pipeline / VK_KHR_ray_query uzantıları. İki şekilde kullanabilirsin: SBT'li tam bir ray tracing pipeline'ı kurarsın ya da bir compute shader'ın içinden inline ray query yaparsın. Unreal'ın ve Unity HDRP'nin path tracer'ları DXR üzerinde çalışır. Eksilerine gelince, OptiX'teki gibi hazır bir denoiser yok ve motion blur gibi özellikler üretici uzantılarına bağlı. Buna karşılık aynı kod NVIDIA, AMD ve Intel'de çalışıyor.
| Kütüphane | Platform | Donanım hızlandırma | Yan araçlar |
|---|---|---|---|
| Embree | CPU (x86, ARM) | SIMD | Curves, subdivision, motion blur |
| OptiX | NVIDIA GPU | RT core | AI denoiser, motion blur, curves |
| HIP RT | AMD GPU | Ray accelerator | Sınırlı |
| DXR / Vulkan RT | Tüm modern GPU'lar | RT donanımı | API düzeyinde, yardımcı yok |
TLAS / BLAS (iki seviyeli yapı)
BLAS (bottom-level) her mesh için, o mesh'in kendi koordinat uzayında kurulur. TLAS (top-level) ise instance'ları tutar; her instance bir transform ve bir BLAS referansından ibaret. Yani bin ağaçlık bir orman, bellekte tek bir ağaç BLAS'ı ve bin transform demek. Animasyonda TLAS her frame ucuzca yeniden kurulur, BLAS'lar ise yalnızca deforme olan mesh'ler için refit edilir. Bu yapının da zayıf bir noktası var: instance'ların sınırları çok örtüşürse (örneğin çapraz duran uzun nesneler) TLAS'ın kalitesi düşer. Unreal'da Nanite olmayan mesh'lerin ray tracing temsili, Unity'de ise RayTracingAccelerationStructure bu yapının karşılığıdır.
Motion blur BVH
Üç tür hareket var ve her birini farklı ele alırsın. Transform hareketinde (dönen pervane) instance transform'u ışının zamanına göre interpole edilir; dönmeyi doğru yakalamak için matris yerine ölçek-dönme-öteleme (SRT) ayrıştırması kullanılır. Deformasyon hareketinde (karakter) vertex'ler birden fazla zaman anahtarında saklanır. BVH düğümleri iki uç zamanın her biri için birer kutu tutar, aradaki kutu da ışının zamanına göre interpole edilir. Hızlı ve doğrusal olmayan hareketlerde bu kutular şişer. Bunun çaresi hareketi zaman dilimlerine bölmek (multi-segment); Embree ve OptiX bunu yerleşik olarak destekliyor.
Sayısal kararlılık
Burada üç klasik tuzak var. İlki self-intersection (shadow acne): yeni ışın az önce çıktığı yüzeye tekrar çarpıyor. Sabit bir epsilon sahnenin ölçeğine bağlı kalır ve büyük sahnelerde işe yaramaz hale gelir. Doğru çözüm, Ray Tracing Gems'teki Wächter ve Binder yöntemi: noktayı normal boyunca ULP uzayında kaydırmak.
İkincisi üçgen kenarlarından sızıntı: ışın, iki komşu üçgenin arasındaki çizgiden geçip gidiyor. Woop ve ark.'nın (2013) watertight üçgen kesişim algoritması bunun olmayacağını garanti eder; popüler Möller-Trumbore algoritması bu garantiyi vermez. AABB testini de yuvarlama hatalarına karşı muhafazakar yazman gerekir.
Üçüncüsü orijinden uzak koordinatlar: kilometrelerce büyüklükteki sahnelerde 32-bit float hassasiyeti gözle görülür titreşime ve kesişim hatalarına yol açar. Çaresi kamera-göreli koordinatlar ya da dünya transform'ları için double kullanmak. Unreal 5'in Large World Coordinates sistemi (double hassasiyetli transform'lar) aynı problemin oyun motoru tarafındaki çözümü.
3. Malzeme ve görünüş
Işık taşınımı ne kadar doğru olursa olsun, fotogerçekliğin çoğu malzemenin ışığı doğru dağıtmasından gelir. Aradığın şey enerji korunumlu, karşılıklı (reciprocal), iyi örneklenebilen ve DCC araçlarıyla aynı parametreleri kullanan bir über-malzeme.
BSDF
BSDF (Bidirectional Scattering Distribution Function), yansıma (BRDF) ve iletim (BTDF) bileşenlerinin toplamıdır. Fiziksel bir BSDF üç şarta uyar: pozitiflik, enerji korunumu (yansıyan enerji gelen enerjiyi aşmaz) ve Helmholtz karşılıklılığı (ışık ve kamera yönü yer değiştirince değer aynı kalır). Sonuncusu özellikle BDPT ve photon mapping'de önemli, çünkü bu yöntemlerde yollar ışıktan da üretilir.
Kodda arayüz üç fonksiyondan ibaret. Bütün hesapları, yüzey normalinin z ekseni olduğu yerel bir koordinat sisteminde yaparsın:
struct BSDFSample { Vec3 wi; Spectrum f; float pdf; uint32_t flags; }; // flags: specular, transmission...
struct BSDF {
Spectrum eval(Vec3 wo, Vec3 wi) const; // f(wo, wi)
float pdf (Vec3 wo, Vec3 wi) const; // MIS için gerekli
std::optional<BSDFSample> sample(Vec3 wo, Vec2 u, float uLobe) const;
};Normal map'lerin getirdiği bir tuzak var: shading normali geometrik normalden saptığında siyah lekeler ve ışık sızıntıları oluşur. Schüssler ve ark.'nın (2017) microfacet tabanlı normal mapping yöntemi bunu temiz biçimde çözer.
GGX microfacet modeli
Bu model yüzeyi, her biri mükemmel ayna olan mikroskobik yüzeylerden oluşmuş kabul eder. Cook-Torrance biçimi şöyle:
D, mikro normallerin dağılımı; GGX (Trowbridge-Reitz) uzun kuyruğu sayesinde gerçekçi bir parlama halesi verir. G, Smith masking-shadowing terimi; bunun da height-correlated sürümü tercih edilir. F ise Fresnel terimi: dielektriklerde tam Fresnel denklemini, metallerde karmaşık kırılma indisini ya da sanatçı dostu F82-tint formülünü kullanırsın. Anizotropi (fırçalanmış metal) için iki ayrı pürüzlülük değeri (alpha_x, alpha_y) gerekir.
Örnekleme tarafında standart yöntem VNDF sampling (Heitz 2018): yalnızca bakış yönünden görünen mikro normalleri örneklediği için boşa giden örnek kalmaz. Dupuy ve Benyoub'un (2023) küresel başlık formülü aynı dağılımı daha basit ve hızlı üretir.
Enerji kompanzasyonu
Tek saçılımlı microfacet modeli, ışığın mikro yüzeyler arasında birden fazla kez sekmesini yok sayar. Bu yüzden pürüzlülük arttıkça malzeme enerji kaybeder ve pürüzlü metaller olması gerekenden koyu görünür. Kulla ve Conty (2017) önceden hesaplanmış albedo tablolarından bir telafi lobu ekler; Turquin'in (2019) yöntemi aynı işi tek bir ölçek çarpanıyla, daha basit biçimde yapar. Bu kaybı en kolay furnace testinde görürsün (bölüm 13).
Katmanlama
Gerçek malzemeler katmanlıdır: arabada vernik altında metalik boya, kumaşın üstünde tüy. İki ana yaklaşım var:
| Yaklaşım | Nasıl çalışır | Artı | Eksi |
|---|---|---|---|
| Albedo scaling | Alt katman, üst katmanın yansıttığı enerji kadar zayıflatılır | Ucuz, gürültüsüz, öngörülebilir | Katmanlar arası iç yansımaları kaba yaklaşıklıyor |
| Stokastik katman değerlendirme (Guo ve ark. 2018) | Katmanlar arası ışık Monte Carlo ile izlenir | Fiziksel olarak doğru | Pahalı ve ek gürültü |
Autodesk Standard Surface ve OpenPBR albedo scaling kullanır; prodüksiyon için doğru seçim de bu.
Coat, sheen, SSS ve thin-film
Coat, kendi pürüzlülüğü ve kırılma indisi (yaklaşık 1.5) olan dielektrik bir GGX katmanı. Verniğin alttaki katmanı koyulaştırıp rengini doyurmasını renkli soğurmayla modellersin.
Sheen, kadife ve kumaşın kenarlarındaki yumuşak parlamadır. Estevez ve Kulla'nın (2017) "Charlie" modeli yaygın; OpenPBR ise Zeltner ve ark.'nın (2022) LTC tabanlı modelini benimsiyor.
Subsurface scattering (SSS) cilt, mum ve mermer gibi malzemelerde gerekir ve iki yaklaşımı var. Diffusion profil yöntemleri (Christensen-Burley normalized diffusion) hızlıdır, ama ince geometride ve keskin köşelerde yanlış sonuç verir. Random walk SSS ise nesnenin içinde gerçek bir hacimsel yol izler; doğru sonuç verir ama yavaştır. Arnold ve Cycles prodüksiyonda random walk'a geçti.
Thin-film, sabun köpüğü ve yağ lekesindeki gökkuşağı yansımalarını (iridescence) verir. Belcour ve Barla (2017), girişimi spektrum üzerinde analitik olarak entegre ederek bu etkiyi RGB render'da da doğru üretir.
OpenPBR
OpenPBR, Academy Software Foundation çatısı altında Autodesk Standard Surface ile Adobe Standard Material birleştirilerek ortaya çıkan über-malzeme spesifikasyonu; 1.0 sürümü 2024'te yayımlandı. Lob yapısını (base, specular, coat, fuzz, thin-film, emission) ve parametre adlarını tanımlar. Amacı, aynı malzemenin farklı render motorlarında aynı görünmesi. Benim tavsiyem, über-shader'ını doğrudan OpenPBR olarak yazman; o zaman kullanıcılar Maya ve Substance'tan gelen malzemeyi çeviriyle uğraşmadan kullanabilir.
MaterialX
MaterialX, ILM'de başlayıp ASWF'ye taşınan, malzeme grafları için açık bir standart. İçinde düğüm tanımları ve bu graflardan GLSL, OSL, MDL veya MSL kodu üreten ShaderGen var. OpenPBR ve Standard Surface birer MaterialX düğüm grafı olarak tanımlanır ve USD'de UsdShade üzerinden taşınır. Motorun için iki seçenek var: grafı kendi shading diline derlemek (GPU'da tercih edilen yol) ya da çalışma zamanında yorumlamak (esnek ama yavaş). Unreal'ın Material Editor'ü ve Unity'nin Shader Graph'ı aynı fikrin motor içi karşılıkları.
OSL (Open Shading Language)
OSL, Sony Pictures Imageworks'ün shading dili. Bu dilde shader renk yerine bir closure döndürür; yani "burada şu ağırlıkla GGX, şu ağırlıkla diffuse var" der. Işık entegrasyonu render motorunda kaldığı için shader yazarı örneklemeyi bozamaz. Arnold, Cycles, V-Ray ve RenderMan OSL'i destekler; OptiX üzerinden bir GPU arka ucu da var. Alternatifi NVIDIA'nın MDL'i. Kullanıcıların kendi shader'larını yazmasını istiyorsan, prodüksiyonda OSL desteği neredeyse beklenen bir şey.
Spektral rendering
RGB fiziksel bir büyüklük değil. Işık da yansıtıcılık da birer spektrum ve RGB üçlülerini çarpmak, spektrumları çarpmakla aynı sonucu vermez. Bu fark iki yerde belirginleşir: doygun renkler arasında çok sayıda sekmede (örneğin yeşil yapraklar arasında kalan ışık) ve dispersiyonda (prizma, elmas).
Standart teknik hero wavelength sampling (Wilkie ve ark. 2014). Her yol birkaç dalga boyu taşır; "hero" dalga boyu yönü belirler, diğerleri ona eşlik eder. Dispersiyonlu bir kırılmada yönler ayrıştığı için eşlik edenler bırakılır. RGB texture'ları spektruma çevirmek için Jakob ve Hanika'nın (2019) sigmoid-polinom yöntemini kullanırsın; kırılma indisi de Cauchy veya Sellmeier denklemleriyle dalga boyuna bağlanır. Octane, Weta'nın Manuka'sı, Mitsuba 3 ve PBRT-v4 spektral motorlar. Bedeli orta düzeyde bir ek maliyet ve daha karmaşık bir renk pipeline'ı.
4. Örnekleme ve gürültü
Aynı algoritmayı iyi dağıtılmış sayılarla beslersen belirgin şekilde daha hızlı yakınsar. Geriye kalan gürültüyü adaptif örnekleme ve AI denoiser temizler; prodüksiyon motorlarındaki "hızlı" his de büyük ölçüde bu katmandan gelir.
QMC ve Sobol dizisi
Sıradan sözde rastgele sayılarla (PCG gibi) hata O(N^−1/2) hızında düşer. Low-discrepancy diziler uzayı çok daha düzgün doldurur ve pürüzsüz integrandlarda neredeyse O(N^−1) hızına yaklaşır; buna Quasi-Monte Carlo (QMC) denir. Sobol dizisi taban 2'de çalışır ve bit işlemleriyle çok hızlı üretilir (Joe-Kuo yön sayıları). Üstelik progressive'dir, yani herhangi bir ön eki de iyi dağılımlıdır. "İstediğin an durdur" diye çalışan bir render için bu şart.
Boyutları da dikkatli yönetmen gerekiyor, çünkü her yol köşesi sırayla boyut tüketir (lens 2, zaman 1, BSDF yönü 2, lob seçimi 1, ışık 2, Russian roulette 1). Yüksek boyutlarda Sobol'ün kalitesi düşer ve boyutlar arası korelasyon artefakt üretir; bu yüzden boyutları karman (padding/shuffling) gerekir.
Owen scrambling
Bütün piksellerde aynı Sobol noktalarını kullanırsan görüntüde desenler belirir, o yüzden diziyi pikseller arasında rastgeleştirmen gerekir. Cranley-Patterson rotasyonu (rastgele kaydırma) ucuzdur ama tabakalanmayı bozar. Owen scrambling ise dizinin yapısal özelliklerini koruyan rastgele permütasyonlardan oluşur ve pürüzsüz integrandlarda yakınsamayı da hızlandırır. Burley'nin (2020) hash tabanlı pratik Owen scrambling yöntemi bunu birkaç tamsayı işlemine indiriyor; Cycles ve PBRT-v4 bu aileden.
Blue noise
Buradaki amaç, hatayı pikseller arasında yüksek frekanslı (blue noise) dağıtmak. Böyle dağılmış bir hata gözü daha az rahatsız eder ve denoiser'ın işini de kolaylaştırır. Heitz ve Belcour (2019) ekran uzayında bu dağılımı elde etmeyi gösterdi; Ahmed ve Wonka'nın (2020) Morton sıralı Sobol'ü (PBRT-v4'teki ZSobolSampler) aynı etkiyi ek tabloya gerek kalmadan verir. Etki en çok düşük örnek sayılarında (viewport önizleme) hissedilir; final render'ın yüksek örnek sayılarında fark azalır.
Adaptive sampling
Her piksel için hatayı tahmin edip örnekleri hatanın yüksek olduğu yerlere yönlendirirsin; yakınsamış pikseller de durdurulur. Yaygın yöntem, örnekleri iki yarı buffer'a ayırıp aralarındaki farkı ölçmek. Hatayı göreli olarak ve tonemap sonrası parlaklığa göre ölçmelisin, çünkü göz karanlık bölgelerdeki gürültüye daha duyarlı. Nadir yolları henüz bulamamış bir piksel ise yanlışlıkla "yakınsamış" görünebilir; bu yüzden bir minimum örnek sayısı şart. Cycles ve V-Ray'deki "noise threshold" ayarı tam olarak bu.
Firefly kontrolü
Firefly'lar, düşük olasılıklı ama çok yüksek enerjili yollardan gelen tek tek parlak piksellerdir (tipik örnek: diffuse → ayna → küçük ışık). Üç çözüm var:
| Yöntem | Nasıl | Bias | Yan etki |
|---|---|---|---|
| Daha iyi örnekleme (MIS, guiding, MNEE) | Yolu daha olası hale getirir | Yok | Uygulama maliyeti |
| Path regularization (Kaplanyan ve Dachsbacher 2013) | Diffuse bir sekmeden sonra pürüzlülüğü yapay olarak artırır | Var, küçük | Kaustikler yumuşar |
| Clamping | Dolaylı katkıları bir üst sınırla kırpar | Var | Enerji kaybı, parlak bölgeler söner |
Prodüksiyon motorları burada pragmatik davranır: Cycles'taki "Filter Glossy" ikinci yöntemin, "Clamp Indirect" de üçüncüsünün karşılığı. Redshift'in hızının bir kısmı da bu tür kontrollü bias'tan geliyor.
Denoiser: OIDN ve OptiX
Bugünkü denoiser'lar, path traced görüntüler üzerinde eğitilmiş U-Net tipi ağlar. Renk buffer'ının yanında albedo ve normal AOV'lerini de alırlar ve texture detayını gürültüden bu ek kanallar sayesinde ayırabilirler.
| Intel OIDN | NVIDIA OptiX Denoiser | |
|---|---|---|
| Donanım | CPU ve GPU (CUDA, HIP, SYCL, Metal) | Yalnızca NVIDIA GPU |
| Girdiler | Renk, albedo, normal | Renk, albedo, normal, hareket vektörleri |
| Zamansal mod | Yok, kare kare | Var |
| Kullanan | Cycles, V-Ray, Corona, Arnold | Octane, Redshift, V-Ray GPU, Cycles |
Diffuse ve specular katkıyı ayrı AOV'lerde denoise edip sonra toplarsan detay belirgin biçimde korunur. Animasyonda ise kare kare denoising titreşim yaratır, bu yüzden ya zamansal bir denoiser ya da daha yüksek örnek sayısı gerekir. Sinema stüdyolarının kendi kernel-predicting ağlarını (Bako ve ark. 2017 KPCN çizgisi) kullanmasının sebebi de bu.
5. Kamera modelleri
Görüntüyü "fotoğraf gibi" yapan şeylerin bir kısmı kamerada: alan derinliği, bokeh, hareket bulanıklığı ve pozlama. Hepsi kamera ışınının nasıl üretildiğiyle ilgili ve hiçbirinin maliyeti yüksek değil.
Pinhole ve thin lens
Pinhole kamerada bütün ışınlar tek bir noktadan çıkar ve her şey keskindir. Thin lens modelinde ise ışın, diyafram diski üzerinde örneklenen bir noktadan çıkıp odak düzlemindeki aynı hedef noktaya yönelir; böylece odak düzleminin dışındaki nesneler bulanıklaşır. Diyafram yarıçapını fotoğrafçılık terimlerinden şöyle hesaplarsın: r = odak uzaklığı / (2 · f-sayısı).
Ray thinLensRay(const Camera& c, Vec2 pFilm, Vec2 uLens, float time) {
Vec3 dir = normalize(c.pinholeDir(pFilm)); // kamera uzayı
float tFocus = c.focusDistance / dir.z;
Vec3 pFocus = dir * tFocus; // odak düzlemindeki nokta
Vec2 pLens = c.apertureRadius * sampleAperture(uLens, c.blades, c.bladeRotation);
Vec3 origin = Vec3(pLens.x, pLens.y, 0);
return c.cameraToWorld(time) * Ray(origin, normalize(pFocus - origin), time);
}Bokeh
Odak dışındaki parlak noktalar diyaframın şeklini alır. Daha gerçekçi bir sonuç için diyafram diski yerine bir çokgen örneklersin; bıçak sayısı ve dönüşü de birer parametre olur. Bazı ileri etkiler de var: kenarlarda kedi gözü şeklinde bokeh (lens gövdesinin yarattığı optik vinyetleme), anamorfik lenslerde oval bokeh ve spektral render varsa renk sapması (chromatic aberration). Bunları kompozitte taklit etmek mümkün, ama render'da derinlik ve örtüşme bilgisi elinde olduğu için doğru biçimde üretebilirsin.
Gerçek lens sistemleri
Bir adım ötesi, ışını gerçek bir objektifin cam elemanları boyunca izlemek (Kolb ve ark. 1995; PBRT'deki RealisticCamera). Lens reçetesini (yüzey eğrilikleri, kalınlıklar, cam türleri) verirsin; distorsiyon, vinyetleme ve bokeh kendiliğinden ortaya çıkar. Verimlilik için çıkış göz bebeğini (exit pupil) önceden hesaplaman gerekir, yoksa ışınların çoğu lensin içinde engellenir. VFX'te CG'yi gerçek çekime oturtmak için çok işe yarar.
Motion blur
Her kamera ışınına deklanşör aralığından bir zaman değeri atanır ve sahneyle kamera bu zamana göre değerlendirilir (BVH tarafı bölüm 2'de). Gerçek deklanşörler açılıp kapanırken ışığı kutu biçiminde değil, yumuşak bir eğriyle geçirir; bu yüzden zamanı bu eğriye göre önem örneklemesiyle seçersin. Sinemada deklanşör açısı (180° = kare süresinin yarısı) parametre olarak verilir.
Rolling shutter
Telefonlar ve birçok dijital sinema kamerası sensörü satır satır okur, bu yüzden hızlı hareket eğilmiş görünür. Modellemesi kolay, ışının zamanını satıra bağlarsın: t = t₀ + (satır / yükseklik) · okuma süresi + deklanşör içi rastgele değer. Gerçek çekimle eşleştirme yapacaksan buna ihtiyacın olur.
Fiziksel pozlama
ISO, enstantane ve f-sayısı tek bir pozlama değerine (EV) çevrilir ve radyometrik görüntü bu değerle ölçeklenir. Böylece sanatçı gerçek bir fotoğrafçı gibi düşünebilir: f-sayısını değiştirdiğinde hem alan derinliği hem parlaklık tutarlı biçimde değişir. Unreal'daki Cine Camera Actor ve Unity HDRP'deki Physical Camera aynı modeli kullanır, yani Unreal'dan bildiğin parametreleri kendi motoruna birebir taşıyabilirsin.
Özel projeksiyonlar
Equirectangular (360° panorama), fisheye ve VR için omni-directional stereo (ODS) kameralar yalnızca ışın üretim fonksiyonunu değiştirir. Rasterizer'da zor olan bu projeksiyonlar path tracer'da neredeyse bedavadır.
6. Volumetrik
Duman, bulut, ateş, sis ve bulanık su "yüzeyi olmayan" malzemeler; ışık hacmin içindeki her noktada soğurulabilir ya da saçılabilir. Heterojen hacimleri hem doğru hem hızlı render etmek, prodüksiyon motorunu oyuncaktan ayıran en zor konulardan biri.
Temel kavramlar
Bir ortam dört büyüklükle tanımlanır: soğurma katsayısı σ_a, saçılma katsayısı σ_s, toplamı olan extinction σ_t ve yayılan ışık (ateş için). Single-scattering albedo σ_s/σ_t, ortamın ne kadar "beyaz" olduğunu söyler; bulutlarda 1'e çok yakındır. Bir ışının hacmi kayıpsız geçme olasılığına da transmittance denir:
Homojen ortamda bu integral analitiktir ve bir sonraki etkileşim mesafesi doğrudan örneklenir: t = −ln(1−u)/σ_t. Heterojen ortamda (yoğunluğu bir voxel grid'inden gelen duman) integral kapalı formda çözülemez; aşağıdaki tracking yöntemleri bu yüzden var.
Delta tracking
Delta tracking'in diğer adı Woodcock tracking. Önce hacmin her yerinde gerçek yoğunluktan büyük ya da ona eşit bir üst sınır (majorant, σ̄) seçersin. Sonra ortam bu sabit yoğunlukta homojenmiş gibi aday çarpışma noktaları örneklersin. Her adayda σ_t/σ̄ olasılığıyla gerçek bir etkileşim kabul edilir; kabul edilmezse "null collision" sayılır ve ışın yoluna devam eder. Yöntem bias'sızdır ve yoğunluğun integralini hiç hesaplamaz. Ama transmittance tahmini olarak kullandığında sonuç ya 0 ya 1 çıkar, bu da gölge ışınlarında yüksek varyans demek.
Ratio tracking
Novák ve ark. (2014) gölge ışınları için daha iyi bir tahminci önerdi: her aday noktada ışını öldürmek yerine ağırlığını (1 − σ_t/σ̄) ile çarpmak. Böylece sonuç 0 ile 1 arasında sürekli bir değer olur ve varyans belirgin biçimde düşer. Residual ratio tracking, bilinen bir kontrol yoğunluğunu analitik olarak çıkarıp yalnızca kalanı stokastik izleyerek bunu daha da iyileştirir. Renkli (dalga boyuna bağlı) ortamlar için Kutz ve ark.'nın (2017) spectral tracking yöntemi kullanılır.
Null-scattering yol integrali
Miller, Georgiev ve Jarosz (2019), null collision'ları yol integraline açıkça dahil eden bir formülasyon sundu. Bunun pratik karşılığı, heterojen hacimlerde de MIS ve NEE'yi doğru ağırlıklarla kullanabilmek. PBRT-v4'ün hacim entegratörü bu çatı üzerine kurulu ve okunabilir bir referans uygulama.
Majorant grid
Yoğun bir duman çekirdeği ve seyrek kenarları olan bir hacimde tek bir global majorant çok gevşek kalır ve sayısız boş null collision üretir. Çözüm, hücre başına maksimum yoğunluğu tutan düşük çözünürlüklü bir grid. Işın bu grid'de DDA ile ilerler ve her hücrede sıkı bir majorant kullanır; hacim performansındaki en büyük kaldıraç da bu.
Phase function
Phase function, yüzeylerdeki BSDF'in hacimdeki karşılığı: ışığın hangi açıyla saçılacağını belirler. Standart seçim, tek bir g parametresiyle (−1 geri, 0 izotropik, +1 ileri) çalışan Henyey-Greenstein. Bulut ve sisteki su damlacıkları ise Mie saçılması yapar; bu da çok sivri bir ileri saçılma ve gökkuşağı benzeri halo'lar üretir. Jendersie ve d'Eon'un (2023) yaklaşık Mie modeli bunu pratik bir maliyetle verir.
Bulutlar ve çoklu saçılma
Albedosu 1'e yakın yoğun bir bulutta ışık yüzlerce kez saçılır; bulutun parlak, "pamuksu" görünümü buradan gelir. Bu yolları sonuna kadar izlemek pahalı. Wrenninge ve ark.'nın (2013) oktav tabanlı yaklaşımı çoklu saçılmayı birkaç ucuz terimle taklit eder ve bunu daha düşük derinlikte yapılan tam izlemeyle birleştirir. Path guiding hacimlerde de büyük fark yaratır.
Emisyon ve ateş
Simülasyon genelde bir sıcaklık grid'i verir. Sıcaklığı Planck yasasıyla siyah cisim spektrumuna çevirirsin; spektral bir motor bunu doğrudan kullanır, RGB bir motor ise renk uzayına entegre eder. Işık yayan hacimler de birer ışık kaynağıdır ve NEE için örneklenebilmeleri gerekir, yoksa ateşin çevreyi aydınlatması çok gürültülü çıkar.
OpenVDB ve NanoVDB
OpenVDB, DreamWorks'ün geliştirdiği, endüstri standardı haline gelmiş seyrek hiyerarşik voxel formatı ve kütüphanesi; Houdini, Blender ve EmberGen gibi araçlar çıktıyı bu formatta verir. NanoVDB ise aynı yapının GPU'ya uygun, tek bir bellek bloğuna düzleştirilmiş salt okunur sürümü ve artık OpenVDB projesinin bir parçası. Tipik akış: dosyayı OpenVDB ile yükle, NanoVDB'ye çevir, GPU'ya yükle ve shading sırasında doğrudan sorgula.
İç içe hacimler
Su dolu bir bardağın içindeki buz küpü gibi durumlarda ışının hangi ortamda olduğu belirsizleşir. Çözüm, her malzemeye bir öncelik vermek ve ışın boyunca aktif ortamların bir yığınını tutmak (Schmidt ve Budge'ın nested dielectrics yaklaşımı). Arnold ve V-Ray'deki "priority" ayarları bunun için var. Unreal'daki Heterogeneous Volumes ve Sparse Volume Texture sistemleri ile Unity HDRP'nin Local Volumetric Fog'u aynı konuya gerçek zamanlı taraftan bakar.
7. Geometri
Prodüksiyon sahnelerinde milyarlarca efektif primitif olur: subdivision ile pürüzsüzleşen karakterler, mikro-displacement'lı kayalar, milyonlarca saç teli ve on binlerce ağaçlı ormanlar. Hepsinde dert aynı: belleği patlatmadan görüntü kalitesini sağlamak.
Subdivision yüzeyleri ve OpenSubdiv
Modelciler düşük poligonlu bir kafes çizer; render sırasında bu kafes Catmull-Clark (quad ağırlıklı) veya Loop (üçgen) kurallarıyla pürüzsüz bir limit yüzeye dönüşür. Pixar'ın OpenSubdiv kütüphanesi endüstri standardı ve DCC araçlarıyla birebir aynı sonucu verir; yarı keskin kıvrımlar (creases), sınır kuralları ve UV'lerin doğru enterpolasyonu da buna dahil. Kendi subdivision'ını yazmak yerine OpenSubdiv kullanırsan "Maya'da başka, render'da başka görünüyor" sorunlarını baştan kesersin.
| Yaklaşım | Nasıl | Artı | Eksi |
|---|---|---|---|
| Sabit tesselasyon | Her yüz N seviye bölünür | Basit | Bellek çok hızlı büyür |
| Adaptif tesselasyon | Ekranda kenar uzunluğuna göre bölünür (dicing rate) | Kalite nereye lazımsa oraya | Kamera dışı ama yansımada görünen yüzeyler kaba kalır |
| Doğrudan patch kesişimi | Işın limit yüzeyi doğrudan kesişir | En az bellek | Kesişim pahalı; Embree gibi bir kütüphane gerektirir |
Cycles adaptif tesselasyon kullanıyor ve kamera dışında kalan yüzeyler sorununu ayrı bir "dicing camera" ile minimum bölünme oranı koyarak çözüyor.
Mikro-displacement
Normal map yalnızca shading normalini değiştirir, siluet düz kalır. Gerçek displacement'ta ise yüzey piksel altı üçgenlere (mikropoligon) bölünür ve her vertex, bir texture'dan okunan değer kadar yerinden oynatılır. Bu fikir RenderMan'ın REYES mimarisinden geliyor. Vector displacement (ZBrush çıktısı) vertex'leri yalnızca normal boyunca değil, her yöne taşıyabilir.
Bu yaklaşımın derdi bellek. Prodüksiyon motorları bu yüzden lazy tesselasyon kullanır: bir ışın nesnenin sınır kutusuna ilk kez çarptığında o bölge tesselasyon edilip bir önbelleğe konur, bellek dolunca da en az kullanılan parçalar atılır. Sınır kutularında da bir tuzak var: kutuları maksimum displacement miktarı kadar genişletmezsen yüzeyin parçaları kaybolur.
Unreal 5'in Nanite sistemi aynı problemin rasterizer tarafındaki cevabı: geometriyi kümelerden oluşan bir detay seviyesi hiyerarşisine böler ve yalnızca ekranda gereken detayı diskten belleğe getirir. Nanite'ın küme mimarisine göz atmak, out-of-core geometri tasarlarken de iyi fikirler verir.
Eğriler ve saç
Bir karakterin saçı yüz binlerce, bir hayvanın kürkü milyonlarca teldir. Bunları üçgene çevirmek bellek açısından imkansız, o yüzden eğrileri doğrudan primitif olarak kesiştirirsin. Genelde iki seçenek arasında seçim yaparsın: kameraya dönük düz şeritler (ucuz, uzaktan yeterli) ya da yuvarlak tüpler (yakın çekimde doğru). Tel geometrisi B-spline, Bezier veya Catmull-Rom eğrileri olarak saklanır. Embree ve OptiX ikisini de yerleşik olarak destekler.
Uzun ince eğriler eksen hizalı kutulara çok kötü oturur (çapraz bir tel devasa ve boş bir kutu yaratır). Çözüm, eğriyi daha kısa parçalara bölmek ve yönlendirilmiş kutular (OBB) kullanmak.
Saçın kendine ait bir BSDF'i var. Marschner modeli ışığı üç lobda inceler: yüzeyden yansıma (R), telin içinden geçiş (TT) ve iç yansıma (TRT). Saçta gördüğün iki ayrı parlama çizgisi buradan gelir. Chiang ve ark.'nın (2016) Disney modeli bunu sanatçı dostu hale getirir ve rengi melanin yoğunluğuyla parametreler; PBRT-v4'te de bu model var. Hayvan kürkü için Yan ve ark.'nın (2015) medulla içeren modeli daha doğrudur.
Instancing
Aynı geometrinin sayısız kopyası, bellekte yalnızca bir mesh ve bir transform listesi olarak tutulur (TLAS/BLAS, bölüm 2). USD'nin PointInstancer'ı bu yüzden prodüksiyonun gündelik aracı; orman, kalabalık ya da çakıl zemini gibi sahneler onunla kurulur. İç içe instancing'i (ağaç instance'ının içinde yaprak instance'ı) Embree'nin çok seviyeli instancing'i doğrudan destekler. Kopyalar birbirinin klonu gibi görünmesin diye instance başına özellikler (renk varyasyonu, rastgele ID) shading'e taşınır. Kesişim maliyeti instance sayısıyla yalnızca logaritmik arttığı için milyarlarca üçgenlik sahneleri render edebilirsin.
8. Bellek ve out-of-core
Bir film sahnesinin texture'ları yüzlerce gigabayt tutabilir; en üst seviye tüketici GPU'ları bile bunun ancak bir kısmını taşıyabilir. Out-of-core rendering, sahnenin yalnızca o an gereken kısmını bellekte tutarak bu açığı kapatır. GPU motorlarının prodüksiyonda kabul görmesinin yolunu da bu teknik açtı.
Tiled ve mipmap'li texture'lar
Texture'lar önceden küçük karolara (örneğin 64×64 piksel) bölünür ve bütün mip seviyeleri dosyaya yazılır. OpenImageIO'nun maketx aracı bunu .tx formatında yapar; RenderMan'in .tex dosyaları aynı fikirdir. Render sırasında yalnızca ışınların gerçekten dokunduğu karolar ve mip seviyeleri diskten okunur.
Texture cache
Bu işin referans uygulaması OpenImageIO'nun TextureSystem'i: sabit bir bellek bütçesi, istek üzerine karo yükleme ve LRU (en uzun süredir kullanılmayanı at) politikası. Arnold, Cycles ve RenderMan, CPU tarafında bunu kullanır. Önbelleğin verimli çalışması için iş parçacıkları arasındaki kilit çekişmesinin az olması gerekir; bu yüzden her iş parçacığına küçük, yerel bir önbellek verilir.
Doğru mip seviyesini seçmek: ray differentials ve ray cones
Texture cache'in bellekten ne kadar kazandıracağı, her ışının doğru (çoğu zaman kaba) mip seviyesini istemesine bağlı. Bunun için ışının yüzeydeki ayak izini bilmen gerekir. Ray differentials (Igehy 1999), komşu piksel ışınlarının nasıl ayrıştığını her sekme boyunca izler. Ray cones (Akenine-Möller ve ark.) aynı bilgiyi tek bir koni açısıyla çok daha ucuza yaklaşıklar ve GPU'da tercih edilir. Pürüzlü ya da diffuse bir sekmeden sonra ayak izi büyüdüğü için en kaba mip'leri kullanabilirsin; dolaylı ışınların texture talebi de böylece büyük ölçüde düşer.
GPU'da out-of-core
Bu konuda öncü Redshift oldu: texture ve geometriyi sistem RAM'i ile VRAM arasında sayfalar. Bunu yapmanın üç yolu var:
| Yaklaşım | Nasıl | Artı | Eksi |
|---|---|---|---|
| CUDA unified memory | Sürücü sayfa hatalarıyla veriyi taşır | Neredeyse kod değişikliği yok | Bellek sınırı aşılınca performans çöker |
| Yazılım sayfalaması (demand loading) | Shader eksik karoyu bir istek listesine yazar, bir sonraki geçişte yüklenir | Öngörülebilir, ölçeklenir | Karmaşık; bekleme yönetimi gerekir |
| Sıkıştırma | BC formatları, quantize vertex'ler | Her şeyle birlikte çalışır | Tek başına yeterli değil |
İkinci yaklaşımın hazır bir uygulamasını istersen, NVIDIA'nın OptiX ile birlikte dağıttığı demand loading kütüphanesi var. Unreal'ın Virtual Texturing sistemi ve Unity'nin Streaming Virtual Texturing'i de aynı "talep geri bildirimi" mantığıyla çalışır, yani Unreal'dan tanıdığın kavram burada da geçerli.
Geometri sayfalaması
Geometriyi sayfalamak texture'dan daha zor, çünkü BVH traversal'ı geometrinin bellekte olmasını bekler. Pratik çözüm, sahneyi parçalara bölüp her parça için ayrı bir BLAS kurmak ve parçaları talebe göre yüklemek. Bellekten tasarruf etmek için de vertex pozisyonlarını quantize edersin, normalleri oktahedral kodlamayla 32 bite sıkıştırırsın ve BVH düğümlerini sıkıştırılmış biçimde saklarsın.
UDIM
VFX'te tek bir model çok sayıda texture karosu kullanır; UV uzayı 1001, 1002 gibi numaralı karolara bölünür. Karakter başına onlarca 4K-8K texture normaldir. Motorunun UDIM'i desteklemesi pratikte zorunlu. Bu iş texture cache ile de doğrudan bağlantılı, çünkü kullanılmayan karolar hiç yüklenmemeli.
Çoklu GPU
GPU'lar genelde belleği paylaşmaz; sahne her GPU'ya ayrı yüklenir, iş de karolara ya da örneklere bölünür. NVLink gibi bağlantılar belleği karşılıklı erişilebilir yapabilir, ama tüketici kartlarda bu seçenek artık nadir. Aynı sistemde farklı hızda kartlar olabilir; yükü dengelemek için iş küçük parçalara bölünüp dinamik olarak dağıtılır.
9. Renk yönetimi
Motor fiziksel olarak doğru radyometrik değerler hesaplasa bile, renk pipeline'ı yanlışsa görüntü "CG gibi" durur. Kural basit: içeride sahneye referanslı doğrusal değerlerle çalış, görüntüyü ekrana yalnızca en sonda ve bilinçli bir dönüşümle çevir.
Doğrusal iş akışı ve çalışma renk uzayı
Render içindeki bütün hesaplar doğrusal ve sınırsız (HDR) değerlerle, belirli bir çalışma renk uzayında yapılır. Hangi uzayı seçtiğin sonucu değiştirir, çünkü RGB çarpımı seçilen ana renklere (primaries) bağlı: aynı sahne, birden fazla sekmeden sonra Rec.709 primaries'inde ve ACEScg'de farklı renk verir. ACEScg gibi daha geniş uzaylar spektral sonuca genelde daha iyi yaklaşır; VFX'te standart çalışma uzayı da ACEScg.
Girdi dönüşümleri
Renk texture'ları (albedo) genelde sRGB kodlamasıyla gelir ve çalışma uzayına doğrusallaştırılır. Veri texture'ları (roughness, metalness, normal, displacement) ise ham değerlerdir ve asla renk dönüşümünden geçmez. Bu ikisini karıştırmak, yeni motorlarda en yaygın görünüm hatalarından biri. Unity'nin texture import ayarlarındaki "sRGB (Color Texture)" işareti ve Unreal'da texture'ın sRGB kutusu tam olarak bu ayrımı yapar.
OpenColorIO (OCIO)
OCIO, Sony Imageworks'te doğan ve bugün ASWF çatısı altında duran renk yönetimi kütüphanesi. Renk uzaylarını ve aralarındaki dönüşümleri bir config dosyasında tanımlarsın; Nuke, Houdini, Maya, Blender ve compositing araçları aynı config'i okur. OCIO 2'de ACES config'leri yerleşik geliyor ve kütüphane viewport için GPU shader kodu da üretebiliyor. Motorun bütün dönüşümleri OCIO üzerinden yaparsa, render çıktın stüdyonun geri kalanıyla birebir aynı renkleri gösterir.
ACES
Academy Color Encoding System, sinema endüstrisinin renk standardı. Birkaç parçadan oluşur: arşiv için ACES2065-1 (AP0 primaries), render ve compositing için ACEScg (AP1 primaries) ve sahne değerlerini belirli bir ekrana çeviren output transform'lar. ACES 1.x'in output transform'ları doygun parlak renklerdeki ton kaymaları yüzünden eleştirilirdi; ACES 2.0'ın yeni output transform'ları tam bu sorunu hedef alarak tasarlandı.
Tonemapping ve görüntüleme dönüşümü
Sahne değerlerinin üst sınırı yok, ekranın ise var. Tonemapping parlak bölgeleri sıkıştırır ve doygun renkleri kontrollü biçimde beyaza doğru götürür (path to white). Bir görüntünün "fotoğraf gibi" mi yoksa "CG gibi" mi algılanacağını tek başına en çok etkileyen adım bu.
| Dönüşüm | Güçlü yanı | Zayıf yanı | Nerede kullanılır |
|---|---|---|---|
| ACES output transform | Sinema standardı, HDR ekran çıktıları | 1.x'te doygun renklerde kayma | Film, VFX; Unreal'ın varsayılan filmic tonemapper'ı bu aileden |
| AgX | Doygun ışıkları doğal şekilde beyaza götürür | Varsayılan görünümü biraz soluk | Blender 4.0'dan beri varsayılan |
| Khronos PBR Neutral | Ürün renklerini ve tonları korur | Sinematik kontrast vermez | Ürün ve e-ticaret render'ı |
| Kendi filmic eğrin | Tam kontrol | Pipeline'ın geri kalanıyla uyumsuzluk riski | Corona ve V-Ray'in kendi tonemapping katmanları |
Benim önerim şu: motorun tonemapping'i çıktı dosyasına gömmesin, yalnızca önizlemede uygulasın. Asıl görüntü doğrusal EXR olarak çıksın, ton kararı compositing'de verilsin.
Spektral çıktı
Spektral motorda her örnek, CIE 1931 renk eşleştirme fonksiyonlarıyla XYZ'ye entegre edilir ve oradan çalışma uzayına çevrilir. RGB texture'ların spektruma çevrilmesini (spectral upsampling) bölüm 3'te anlattım. Burada dikkat etmen gereken, dönüşümün tersine çevrilebilir olması: beyaz ışık altındaki bir texture, spektral render'dan sonra da aynı RGB değerini vermeli.
Beyaz dengesi
Işıkları renk sıcaklığıyla (Kelvin) tanımlarsın. Kameranın beyaz dengesi Bradford veya CAT16 gibi bir kromatik adaptasyon dönüşümüyle uygulanır; RGB kanallarını ayrı ayrı ölçeklemek doğru sonuç vermez. Bunu fiziksel pozlamayla (bölüm 5) birleştirince sanatçı, gerçek bir çekimdeki gibi tungsten ışığıyla gün ışığını karıştırabilir.
10. Çıktı: AOV, light group, Cryptomatte, deep EXR
Prodüksiyonda render, compositing'in hammaddesi; son görüntü orada şekillenir. Bir süpervizör "arka plandaki lambayı yarı yarıya kıs" dediğinde bunu yeniden render almadan yapabilmek, motorunun ne kadar ciddiye alınacağını belirler.
AOV (Arbitrary Output Variables)
İki tür AOV var. Veri AOV'leri ilk çarpışmadaki değerleri yazar: derinlik, normal, dünya pozisyonu, UV, hareket vektörleri, albedo, nesne ID'si. Aydınlatma AOV'leri ise güzel görüntüyü (beauty) parçalarına ayırır: diffuse direkt ve indirekt, specular, transmission, SSS, emisyon, hacim. Aydınlatma AOV'lerini topladığında beauty'yi elde etmelisin; bunu otomatik bir testle kontrol etmek iyi bir alışkanlık.
Filtrelemede ayrı bir tuzak var. Beauty piksel filtresiyle (Gaussian, Blackman-Harris) yumuşatılır, ama normal veya nesne ID'si gibi veri AOV'lerini ortalamak anlamsız değerler üretir. Bu AOV'ler için filtresiz ya da en yakın örnek yaklaşımını kullanırsın. Piksel filtresinin kendisi için bugünkü tercih filter importance sampling: örnek konumları filtre şeklinde dağıtılır ve her örneğin ağırlığı 1 olur. Komşu piksellere dağıtma (splatting) gerekmediği için GPU'da yarış koşulu riski de ortadan kalkar.
Light Path Expressions (LPE)
Aydınlatma AOV'lerini tek tek kodlamak yerine, OSL'den gelen LPE'lerle yol olayları üzerinde düzenli ifade (regex) yazabilirsin. Örneğin C<RD>L direkt diffuse aydınlatmayı, C<RS>.*L specular ile başlayan tüm yolları tanımlar (C kamera, R yansıma, D diffuse, S specular, L ışık). Uygulamada her ifade bir sonlu durum makinesine derlenir; yol izlenirken her sekmede durum bir adım ilerler ve katkı, eşleşen buffer'lara eklenir. Arnold ve RenderMan bu sistemi kullandığı için kullanıcılar AOV'lerini kendileri tanımlayabiliyor.
Light group (LightMix)
Işık taşınımı, ışık kaynaklarının yayımına göre doğrusaldır. Bu yüzden her ışık grubunun katkısını ayrı bir buffer'a yazarsan, render bittikten sonra bu buffer'ları ölçekleyerek her grubun parlaklığını ve rengini fiziksel olarak doğru biçimde değiştirebilirsin. Bunun en bilinen örneği Corona'nın LightMix özelliği: gündüz ve gece versiyonu tek bir render'dan çıkar. Bedeli, grup başına bir buffer kadar bellek.
Cryptomatte
Cryptomatte, Psyop'un 2015'te yayınladığı açık bir standart. Her piksel için, o pikseli en çok kaplayan birkaç nesnenin (ID hash'i, kapsama oranı) çiftlerini EXR kanallarına yazar; nesne, malzeme veya asset adı MurmurHash3 ile bir float değere çevrilir. Böylece compositing'de herhangi bir nesnenin maskesini tıklayarak çıkarabilirsin; üstelik kenar yumuşatma, hareket bulanıklığı ve şeffaflık da maskeye dahil. Nuke, Fusion ve Blender'ın compositor'ü Cryptomatte'i destekliyor; prodüksiyonda beklenen bir özellik.
Deep EXR
Normal bir görüntüde her piksel tek bir renk taşır. Deep görüntüde ise her piksel, derinlik aralığı ve alfa değeri olan bir örnek listesi taşır. Böylece ayrı ayrı render edilmiş duman ile karakteri compositing'de, hangisinin önde olduğunu doğru hesaplayarak birleştirebilirsin; maske render'ı almana da gerek kalmaz. Bedeli dosya boyutu, o yüzden genelde yalnızca hacimler ve karmaşık örtüşmeler için kullanılır.
Dosya formatı ve devam ettirme
Standart çıktı çok katmanlı OpenEXR: beauty ve aydınlatma AOV'leri half float, pozisyon ve derinlik full float olarak yazılır. Sıkıştırmada kayıpsız ZIP veya PIZ, son çıktıda kayıplı DWAA kullanılır. Kamera matrisleri ve render ayarları EXR başlığına metadata olarak yazılır, çünkü compositing'de 3D projeksiyon için bunlara ihtiyaç var. Uzun render'ları durdurup sonra devam ettirebilmek için de birikmiş buffer'lar ve örnek sayıları belirli aralıklarla diske yazılır (checkpoint).
11. Sahne girişi: USD ve Hydra
Kullanıcının sahnesine ulaşamayan bir render motorunu kimse kullanmaz. Eskiden bu, her DCC için ayrı bir eklenti yazmak demekti (Octane, Redshift ve Corona'nın 3ds Max ve Cinema 4D eklentileri gibi). Bugün kısa yol USD ve Hydra: tek bir render delegate yazıp onlarca araca aynı anda girebilirsin.
USD (Universal Scene Description)
USD, Pixar'ın açık kaynaklı sahne tanım sistemi. USD'yi değerli kılan composition sistemi: sahne birbirinin üzerine binen katmanlardan oluşur, böylece layout, animasyon ve aydınlatma departmanları birbirinin çalışmasını bozmadan aynı sahneye katkı yapabilir. Composition araçları references (başka bir dosyayı içeri almak), payloads (gecikmeli yüklenen references), variants (aynı nesnenin alternatif sürümleri) ve inherits. Bunların öncelik sırası (LIVRPS), USD'yi anlamak için öğrenmen gereken ilk kavram. Dosya formatı olarak metin tabanlı .usda, ikili .usdc ve paketli .usdz var. 2023'te Pixar, Apple, Adobe, Autodesk ve NVIDIA tarafından kurulan Alliance for OpenUSD (AOUSD), USD'yi resmi bir standarda dönüştürmek için çalışıyor.
Render motoru için önemli olan şemalar şunlar: geometri için UsdGeom (mesh, curves, points, subdivision bilgisi), malzeme için UsdShade (MaterialX ve UsdPreviewSurface ile), ışıklar için UsdLux ve fiziksel kamera parametreleri için UsdGeomCamera. Point instancer da USD'nin parçasıdır (bölüm 7).
Hydra ve render delegate
Hydra, USD'nin render mimarisidir. Sahne tarafını (UsdImaging) render tarafından ayırır; ortadaki render index (Hydra 2.0'da scene index) sahneyi standart primitiflere çevirir. Senin işin bir HdRenderDelegate yazmak ve şu nesneleri kendi motorunun temsiline çevirmek:
- Rprim'ler: mesh, curves, points, volume gibi görünen geometri.
- Sprim'ler: kamera, ışıklar, malzemeler.
- Bprim'ler: render buffer'ları ve texture'lar gibi kaynaklar.
- HdRenderPass: render'ı başlatan ve sonucu buffer'lara yazan nesne.
Karşılığında usdview, Houdini Solaris, Maya, Blender ve NVIDIA Omniverse içinde çalışan bir viewport ve final render elde edersin. Arnold, RenderMan (hdPrman), Cycles, Karma, Redshift, V-Ray ve Octane'ın hepsinin Hydra delegate'i var. Yeni bir motorun prodüksiyona girmesi için en gerçekçi yol da bu.
Artımsal güncelleme: en önemli mimari gereksinim
Hydra değişiklikleri "dirty bit"lerle bildirir: "bu mesh'in yalnızca transform'u değişti" ya da "bu malzemenin bir parametresi değişti". Motorunun bunlara bütün sahneyi yeniden kurmadan tepki verebilmesi gerekir: yalnızca TLAS'ı güncellemek, yalnızca o malzemeyi yeniden derlemek, sonra birikimi sıfırlayıp render'ı yeniden başlatmak. Corona ve Octane'ı sevdiren özellik de interaktif IPR (sanatçı ışığı sürüklerken görüntünün anında güncellenmesi). Bu yeteneği sonradan eklemek en zor işlerden biri, o yüzden sahne katmanını ilk günden artımsal güncellemeyi düşünerek tasarla.
Unity ve Unreal ile bağlantısı
Unreal, USD Stage Actor ile USD sahnelerini doğrudan açar ve canlı düzenler; Unity'nin de bir USD paketi var. Unreal'ın içine bir delegate ile giremezsin, ama aynı USD sahnesini hem Unreal'da hem kendi motorunda açıp sonuçları karşılaştırabilirsin; bu çok işe yarayan bir test düzeneği. Unreal'ın Movie Render Queue path tracer çıktısı da iyi bir karşılaştırma referansı.
Komut satırı ve render farm
Prodüksiyonda render'lar arayüzden alınmaz, render farm'da alınır. Bu yüzden motorunun bir USD dosyasını komut satırından render edip EXR yazabilmesi, kare aralığı ve çıktı yolları gibi parametreleri alabilmesi ve Deadline gibi farm yöneticileriyle çalışabilmesi gerekir.
12. Mimari kararlar
Bu bölümdeki kararlar, sonradan değiştirmesi en pahalı olanlar: kodun GPU'da nasıl çalışacağı, bias'a ne kadar izin verileceği ve hangi donanımın hedefleneceği. Bunların çoğunu motorun ilk aylarında, bilerek vermen gerekir.
Megakernel ve wavefront
Megakernel yaklaşımında bir yolun bütün sekmeleri tek bir GPU kernel'ında, bir döngü içinde izlenir. Basittir ve yol durumu register'larda kalır. Ama GPU'da 32 iş parçacığı (warp) aynı komutu çalıştırır; bir warp'taki ışınlar farklı malzemelere çarparsa malzeme kodları sırayla çalışmak zorunda kalır (divergence). Bir de en karmaşık malzemenin register ihtiyacı, aynı anda çalışabilen iş parçacığı sayısını herkes için düşürür.
Wavefront yaklaşımında (Laine, Karras ve Aila 2013, "Megakernels Considered Harmful") yol durumu bellekte SoA düzeninde tutulur ve her aşama ayrı bir kernel olur: ışın üret, kesişim bul, ışınları malzemeye göre kuyruklara ayır, her malzeme için ayrı shading kernel'ı çalıştır, gölge ışınlarını test et, sonucu biriktir. Her kernel küçük ve tutarlı olur. Bedeli iki şey: yol durumunun yediği bellek bant genişliği ve GPU'yu doyurmak için aynı anda milyonlarca yolun havada olması gerekmesi. Cycles 2021'deki Cycles X yeniden yazımıyla wavefront'a geçti; PBRT-v4'ün GPU modu da bu mimaridedir.

wavefront akışı · 6 aşama, 1 döngü
Bu akıştaki en önemli adım 3: aynı malzemeye çarpan ışınlar aynı kernel'da toplandığı için warp içindeki bütün iş parçacıkları aynı kodu çalıştırır.
SER (Shader Execution Reordering)
SER, NVIDIA'nın Ada nesliyle (2022) getirdiği bir özellik: kesişimden sonra iş parçacıkları, verilen bir anahtara (örneğin malzeme ID'si) göre donanım desteğiyle yeniden gruplanır. Böylece megakernel yapısını koruyup wavefront'un tutarlılık avantajının büyük kısmını alırsın. OptiX'te ve Vulkan'ın NVIDIA uzantısında var; DXR 1.2 ile DirectX tarafına da standart olarak geldi. Yeni bir GPU motoru için bence pratik orta yol şu: OptiX megakernel + SER ile başla, darboğaz kanıtlanırsa wavefront'a geç.
Biased ve unbiased
Unbiased bir motor (Octane, Corona'nın varsayılanı, Cycles) ne kadar uzun beklersen referansa o kadar yakınsar. Bu, sanatçı için tahmin edilebilir bir durum: tek ayar "ne kadar bekleyeceğim". Biased motorlar ise önbellekler ve kestirmelerle hız kazanır: irradiance cache, V-Ray'in ikincil sekmeler için kullandığı Light Cache, photon map, clamping ve NVIDIA'nın Neural Radiance Caching'i (2021) gibi öğrenilmiş önbellekler. Bunun bedeli animasyonda titreşim, sızıntı ve leke artefaktları, bir de ayar karmaşıklığı.
Bugün unbiased path tracing + path guiding + AI denoiser üçlüsü, eski önbellek tekniklerinin çoğunun yerini almış durumda. Yine de V-Ray'in Light Cache'i gibi kendini kanıtlamış önbellekler iç mekan render'ında hâlâ ciddi hız kazandırıyor. Unreal'da Lumen ile Path Tracer arasındaki fark, bu ikiliğin gerçek zamanlı taraftaki karşılığı.
CPU, GPU veya hibrit
| CPU | GPU | Hibrit | |
|---|---|---|---|
| Bellek | Yüzlerce GB RAM | VRAM ile sınırlı (out-of-core gerekir) | En küçük cihaz sınırı belirler |
| Hız | Temel | Kutu başına kat kat hızlı | İkisinin toplamına yakın |
| Shader esnekliği | OSL dahil sınırsız | Derleme ve register kısıtları | En kısıtlı taraf belirler |
| Ekosistem | Embree, OIIO, OpenVDB doğrudan | CUDA ve OptiX'e bağımlılık | İki kod yolu |
| Örnekler | Corona, Arnold CPU | Octane, Redshift | V-Ray, Cycles |
Hibritte en büyük maliyet bakım, çünkü her özelliğin iki yerde de aynı sonucu vermesi gerekir. Cycles'ın bunu nasıl çözdüğüne bakmak öğretici: kernel kodu, makrolar sayesinde CPU, CUDA, HIP, Metal ve oneAPI için derlenebilen ortak bir C++ alt kümesinde yazılıyor. PBRT-v4 de aynı kodu CPU ve CUDA için derler. Apple Silicon'ın birleşik belleği (CPU ve GPU aynı RAM'i paylaşır) büyük sahnelerde ilginç bir avantaj.
Dil ve derleme
Pratik seçim C++20, GPU tarafında da CUDA/OptiX. Takip etmeye değer bir gelişme de Slang: NVIDIA'da doğan ve artık Khronos çatısı altında duran bir shading dili. Tek kaynaktan HLSL, SPIR-V, CUDA ve Metal üretiyor ve otomatik türev almayı (autodiff) destekliyor; bu da differentiable rendering'e kapı açıyor. CPU tarafında iş planlaması için standart seçim Intel oneTBB.
Determinizm
Render farm'da aynı kare iki kez render edildiğinde aynı sonuç çıkmalı, yoksa yeniden render edilen kareler animasyonda göze batar. Bunun için rastgele sayıları piksel koordinatından ve örnek indeksinden türetmelisin (bölüm 4). Paralel toplamalar sıradan bağımsız olmalı, atomik float toplamadan da kaçınmalısın.
13. Doğrulama
Path tracer hataları sinsidir: görüntü "güzel" görünür, ama yüzde 15 koyudur ya da yalnızca belirli bir açıda hatalıdır. "Gerçekten ayırt edilemez" hedefine ulaşmak için gözüne güvenmek yetmez, ölçüme dayanan bir test disiplini gerekir.
Furnace testi
Nesneyi her yönden parlaklığı 1 olan düzgün bir ortam ışığının içine koyarsın ve malzemesine hiç enerji soğurmayan değerler verirsin (albedo 1 diffuse, Fresnel'i 1 olan pürüzlü metal, soğurmasız cam). Enerji korunuyorsa nesne arka planda tamamen kaybolmalı. Koyu görünüyorsa enerji kaybı var (örneğin enerji kompanzasyonu eksik); parlak görünüyorsa enerji üretiliyor demektir (genelde MIS ağırlık veya pdf hatası). Her yeni malzeme lobu için ilk yapacağın test bu.
BSDF birim testleri
Her BSDF için üç otomatik test yazarsın. Karşılıklılık: f(wo, wi) ile f(wi, wo) eşit mi? Enerji: Monte Carlo ile hesaplanan albedo 1'i aşıyor mu? Örnekleme tutarlılığı: sample() fonksiyonunun ürettiği yönlerin histogramı pdf() fonksiyonuyla örtüşüyor mu? Sonuncusu ki-kare (χ²) testiyle yapılır ve Mitsuba ile PBRT'de hazır örnekleri var. Bunlara ek olarak sample()'ın döndürdüğü ağırlığın eval/pdf oranına eşit olup olmadığına da bakılır. Bu testler, bir görüntüde haftalarca arayacağın hataları saniyeler içinde yakalar.
Referans karşılaştırma
Aynı sahneyi Mitsuba 3 veya PBRT-v4 ile render edip farkı ölçersin. Göreli MSE sayısal farkı verir; NVIDIA'nın FLIP metriği ise insan gözünün fark edeceği farkı ölçer ve hatanın nerede olduğunu bir ısı haritasıyla gösterir. Cornell Box gibi ölçüm verisi olan klasik sahnelerde sonucu gerçek dünyayla da karşılaştırabilirsin.
Yakınsama testleri
Unbiased iddiasını şöyle test edersin: düşük örnek sayılı çok sayıda bağımsız render'ın ortalaması, yüksek örnek sayılı referansa eşit çıkmalı. Bir de hatanın örnek sayısına karşı log-log grafiğindeki eğimini izlersin: sıradan Monte Carlo için −0.5, iyi bir QMC sampler için daha dik. Eğim aniden değişirse sampler'da bir hata var demektir.
Regresyon testleri
Her özellik için bir "altın görüntü" tutarsın; CI her commit'te sahneleri düşük örnek sayısında render eder ve bu görüntülerle FLIP veya göreli MSE üzerinden, belli bir toleransla karşılaştırır. Hibrit bir motorda aynı testleri CPU ile GPU arasında da yaparsın. Unity ve Unreal'ın kendi grafik test framework'leri (Unity'nin Graphics Test Framework'ü, Unreal'ın Automation screenshot testleri) aynı fikrin oyun motoru karşılıklarıdır.
Performansı doğru ölçmek
Algoritmaları aynı örnek sayısında değil, aynı hataya ulaşma sürelerine göre karşılaştırmalısın. Örneğin path guiding örnek başına daha yavaştır ama aynı kaliteye daha erken ulaşır; örnek başına karşılaştırma bunu gizler. Doğru ölçü verimliliktir: 1 / (varyans × süre). Ham kesişim performansı için saniye başına ışın sayısı (Mray/s) ayrıca izlenir.
Debug araçları
Motorun içine gömülü görselleştirme modları şunları gösterebilmeli: piksel başına yol derinliği, BVH traversal adım sayısı (yavaş bölgeleri ortaya çıkarır), örnek sayısı ve varyans haritası. En önemlisi de bir NaN/Inf dedektörü. Tek bir NaN örnek biriktirilen pikseli kalıcı olarak bozar ve dedektör olmadan kaynağını bulmak çok zordur. Bu iş için GPU tarafında Nsight Graphics ve Nsight Compute, CPU tarafında Intel VTune var.
14. Unity/Unreal karşılıkları, yol haritası ve kaynaklar
Bu rehberdeki kavramların çoğunun Unity ve Unreal'da bir karşılığı var. Prodüksiyon seviyesindeki bir render sisteminin bu parçaları nasıl birleştirdiğini görmek istersen, en erişilebilir örnek Unreal'ın kaynak kodu.
Kavram eşleştirmesi
| Kavram | Unity | Unreal |
|---|---|---|
| Path tracing entegratörü | HDRP Path Tracing | Path Tracer (Movie Render Queue referans modu) |
| Ray tracing API | RayTracingShader, DXR | RHI ray tracing katmanı, DXR / Vulkan RT |
| TLAS / BLAS | RayTracingAccelerationStructure | RHI'daki ray tracing sahnesi ve geometri nesneleri |
| Katmanlı malzeme | HDRP StackLit | Substrate |
| Malzeme grafı | Shader Graph | Material Editor |
| Fiziksel kamera | HDRP Physical Camera | Cine Camera Actor |
| Heterojen hacimler | Local Volumetric Fog | Heterogeneous Volumes, Sparse Volume Texture |
| Geometri akışı | Karşılığı yok | Nanite |
| Sanal texture | Streaming Virtual Texturing | Virtual Texturing |
| Renk yönetimi | HDRP tonemapping (ACES, Neutral) | Filmic tonemapper, OpenColorIO eklentisi |
| Sahne formatı | USD paketi | USD Stage Actor |
| Frame/render graph | RenderGraph | RDG (Render Dependency Graph) |
| Shader dili | HLSL (ShaderLab içinde) | HLSL (.usf / .ush dosyaları) |
Unreal'da path tracer shader'ları Engine/Shaders/Private/PathTracing/ altında. MIS, light sampling ve malzeme değerlendirme kodunu bu rehberin 1. ve 3. bölümleriyle yan yana okursan iyi bir egzersiz olur.
Yol haritası
Sırayı bilerek böyle kurdum: önce doğruluk, sonra hız, en son prodüksiyon özellikleri. Her adımın sonunda da ölçülebilir bir kilometre taşı var.
- Isınma: Ray Tracing in One Weekend serisi, C++ ile. Kilometre taşı: üç kitabın sonundaki sahneler.
- Doğru bir CPU çekirdeği: PBRT-v4'ü izleyerek PT + NEE + MIS, GGX + VNDF, Sobol + Owen scrambling, OIDN. Furnace ve χ² testleri bu aşamada yazılır. Kilometre taşı: Cornell Box'ta Mitsuba 3 ile FLIP farkının ihmal edilebilir olması.
- Kesişim: Kendi BVH'nı SAH ile bir kez yaz, sonra Embree'ye geç ve ikisini karşılaştır.
- Gerçek sahneler: USD yükleme, OpenPBR über-malzemesi, OCIO, çok katmanlı EXR ve temel AOV'ler. Kilometre taşı: Blender'dan dışa aktarılan bir sahnenin Cycles ile yan yana doğru görünmesi.
- GPU: OptiX megakernel, ardından SER; darboğaz kanıtlanırsa wavefront. CPU ile GPU arasında parite testleri.
- Prodüksiyon çekirdeği: NanoVDB + ratio tracking ile hacimler, saç primitifleri ve BSDF'i, OpenSubdiv + displacement, texture cache, light tree, Open PGL ile path guiding.
- Entegrasyon: Hydra render delegate ve artımsal güncellemeli IPR.
- Ayrıştırıcılar: Spektral rendering, MNEE veya VCM ile kaustikler, light group'lar, Cryptomatte, deep EXR.
Kaynaklar
Kitaplar ve tezler:
- Physically Based Rendering, 4. baskı: ücretsiz çevrimiçi; alanın temel kitabı.
- Ray Tracing in One Weekend serisi: ısınma için.
- Eric Veach'ın doktora tezi: MIS, BDPT ve path space formülasyonunun kaynağı.
- Ray Tracing Gems I ve II: ücretsiz; self-intersection, ray cones ve daha fazlası.
- SIGGRAPH Physically Based Shading kurs notları: malzeme modelleri için.
Referans kodlar:
- Mitsuba 3: spektral, differentiable, araştırma odaklı.
- Blender (Cycles
intern/cyclesaltında): prodüksiyon seviyesinde açık kaynak motor.
Kütüphaneler ve standartlar:
- Embree, Open Image Denoise, Open PGL
- OpenUSD, OpenPBR, MaterialX
- OpenColorIO, OpenVDB
- FLIP: görüntü karşılaştırma metriği
Makale yıllarını kaynakların kendisinden tek tek doğrulamadım. Bir tekniği uygulamaya geçmeden önce ilgili makaleye bakmanı öneririm.