Ana içeriğe geç
Ahmet Faruk UzunkayaYaratıcı geliştirici
Tüm yazılar
RehberPhoton Engine · 2/2Mühendislik62 dkÖne çıkan

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.

  • C++20
  • Path Tracing
  • Rehber
  • Bilgisayar Grafikleri
Photon Engine'le yan yana, prodüksiyon seviyesinde bir path tracer rehberi

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ümPhoton Engine'de
0 · MimariDü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şimEmbree 4; yoksa kendi 12 kovalı SAH BVH'si
3 · MalzemeDisney 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önetimiDoğrusal Rec.709; PBR Neutral, AgX, ACES, Hable
10 · ÇıktıPNG, JPEG, doğrusal EXR, turntable
11 · Sahne girişiOBJ, 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

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ı

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ülSorumlulukBağımlı olduğuUnity karşılığı
core/Vektör/matris, Spectrum, RNG, hash, bellek arena'ları, log, asserthiçbiriUnity.Mathematics
kernel/Cihazdan bağımsız render kodu: kamera, BSDF, ışıklar, hacimler, örnekleme, integratorcore (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ıcoreGraphics API (Unreal'da RHI)
scene/Yazar sahnesi: Mesh, Instance, Light, MaterialGraph, Camera, Volume; dirty bayraklarıcoreGameObject + Component
io/USD, glTF/OBJ, OpenImageIO texture, OpenVDB okuyucularscene, coreAssetImporter
compile/Sahne derleyici: tessellation, displacement, BVH kurulumu, light tree, malzeme derleme, texture handle tablosuscene, device, kernel (tip tanımları)Build/bake pipeline
film/Birikim, AOV, varyans, adaptive sampling istatistiği, denoise, OCIO, EXR yazıcıdevice, coreRenderTexture + post-process
session/Oturum, durum makinesi, progressive döngü, iptal, istatistikyukarıdakilerin hepsiSRP render pipeline
app/CLI, viewer, Hydra delegate eklentisi, Python bindingsessionEditor
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, regresyon

kernel/'ı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:

  • Hit ile ShadingPoint ayrı tutulur. Kesişim donanımı (OptiX, Embree) yalnızca t, ID'ler ve barycentric döndürür. Geri kalanını hesaplamak pahalı, üstelik gölge ışınlarında hiç gerekmiyor. Wavefront'ta kuyrukta yalnızca Hit taşındığı için bellek trafiği de düşüyor.
  • Spectrum tek bir tip, ama iki uygulaması var. RGB modunda bir float3, spektral modda dört dalga boyunun değeri. Hangisinin kullanılacağı derleme zamanında seçildiği için (#if veya 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 LightSample dönmeden önce katı açıya çevrilir. MIS hatalarının en yaygın kaynağı bu iki ölçüyü karıştırmak.
  • PathState sı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çimNesne grafiği: node'lar, işaretçiler, string isimlerDüz diziler, 32-bit indeksler, SoA
Kim yazarHydra delegate, USD okuyucu, Python APIYalnızca derleyici
Kim okurDerleyiciYalnızca kernel
BellekHost heap, std::vector, shared_ptrDevice tamponları (VRAM veya pinned RAM)
DeğişimHer an; dirty bayrağı atanırYalnızca render durduğunda, çift tampon ile
GeometriHam mesh, subdiv cage, eğri kontrol noktalarıTessellate edilmiş üçgenler, BLAS/TLAS
MalzemeNode 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:

  1. 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.
  2. 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.
  3. 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şımArtıEksiKim kullanır
Sanal fonksiyonDoğal C++, kolay genişletmeGPU'da yok; CPU'da da inline yokEski CPU motorları, Mitsuba 0.x
Etiket + switchHızlı, CPU/GPU ortak kodYeni tip = merkezi enum'a eklemePBRT-v4, Cycles (SVM node'ları)
OptiX callable program / SBTMalzemeler ayrı derlenir, eklenti dostuYalnızca OptiX; çağrı maliyetiOctane, OptiX tabanlı motorlar
Kod üretimi (OSL/MaterialX → kernel)Malzeme başına tam optimize kodDerleme süresi, karmaşık pipelineArnold GPU, RenderMan XPU
Yorumlanan bytecode (shader VM)Sınırsız node grafiği, derleme yokYavaş, 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, Multi

Bellek 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. KernelScene içindeki DeviceSpan'ler ise sahip olmayan görünümlerdir (işaretçi + uzunluk) ve kernel'a değer olarak kopyalanır.
  • RAII tutamaçları. DeviceBuffer yı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:

  1. generateCameraRays: boş yuvalara yeni yol yazar.
  2. intersectClosest: her yol için Hit bulur; yüzeye çarpanları ShadeItem olarak malzeme türüne göre ayrı kuyruklara (queues[BxDFType]), kaçanları missQueue'ya, hacimdekileri mediumQueue'ya iter.
  3. shadeMiss ve sampleMedium kendi kuyruklarını işler.
  4. Her malzeme türü için bir shade<BxDFType> kernel'ı: BSDF'i değerlendirir, NEE için ShadowItem üretir, BSDF örnekleyip yeni ray'i PathStateSoA'ya yazar, RR ile ölenleri işaretler.
  5. intersectShadow: gölge kuyruğunu toplu test eder, görünenlerin contrib'ini L'ye ekler.
  6. Ö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

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şiklikDirty bayrağıDerleyicide yapılanGöreceli maliyet
Kamera hareketi, odak, pozlamaCameraKernelCamera güncellenir, film sıfırlanırEn ucuz
Film çözünürlüğü, AOV listesiFilmFilm tamponları ve wavefront kuyrukları yeniden ayrılırUcuz
Malzeme parametresi (renk, roughness)MaterialParamsYalnızca o malzemenin parametre bloğu yüklenirUcuz
Işık yoğunluğu veya rengiLightParamsIşık kaydı güncellenir, light tree'de güç değerleri refit edilirUcuz
Nesne transform'uTransformTLAS yeniden kurulur veya refit edilir; emisifse light tree refitOrta
Işık ekleme veya silmeLightSetLight tree ve ışık dağılımı yeniden kurulurOrta
Texture dosyası değiştiTextureTexture cache'te o dosyanın karçaları geçersiz kılınırOrta
Malzeme grafiğinin yapısı (node eklendi)MaterialGraphMalzeme yeniden derlenir; kod üretimi kullanılıyorsa kernel yeniden derlenebilirPahalı
Vertex pozisyonları (deformasyon)PointsBLAS refit; displacement varsa yeniden tessellationPahalı
Topoloji, subdiv seviyesiTopologyTessellation + BLAS sıfırdan kurulurEn 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.

ThreadGöreviSahneye erişimiSenkronizasyon
Host / UI (DCC veya viewer)Kullanıcı düzenlemeleri, Hydra Sync çağrılarıYazar sahnesine yazarSahne 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ürmeYazar sahnesini okur, kernel sahnesini üretirstd::condition_variable ile uyandırılır; iptal bayrağı
CPU worker havuzu (oneTBB)CPU kernel'ı: kareçik başına integratePixelSample; derleyicide paralel BVH kurulumuKernel sahnesini salt okurKareçik kuyruğu; filme kareçik başına yazım (atomik yok)
GPU stream'leriKernel başlatma, kopyalamaDevice tamponlarını okurStream sırası, event'ler; host yalnızca okuma geri alırken bekler
I/O havuzuTexture karçalarını diskten okuma (bölüm 8), EXR yazmaYokKilitsiz 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

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.

Eklenecekscene/compile/kernel/film/Diğer
Yeni BxDF (örn. sheen)Malzeme node'u + parametreleriParametre bloğuna yazmaYeni BxDF struct'ı + switch dalı-Furnace ve χ² testi
Yeni ışık tipi (örn. IES profilli spot)Light alt tipiLight tree'ye güç ve sınır kutususample/pdf/eval-IES okuyucu io/'da
Yeni kamera (örn. fisheye)Camera alt tipiKernelCamera doldurmagenerateRay dalı--
Yeni AOV (örn. motion vector)-Film düzenine kanalIntegrator'da yazma noktasıKanal tanımı + EXR adıHydra'da AOV binding
Yeni geometri tipi (örn. nokta bulutu)NodeBLAS 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.

  1. Yükleme (io/ → scene/). Her şey CLI'da render shot.usd --frame 101 ile başlar. USD okuyucu prim'leri gezer ve Mesh, Light, Camera node'larını NodeRegistry üzerinden üretir. MaterialX ağı bir MaterialGraph olur. Texture'lar henüz okunmaz, yalnızca yolları kaydedilir. Son olarak bütün node'lar Dirty::All ile işaretlenir.
  2. 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ı OpenPBR etiketiyle parametre tamponuna yazar, texture yollarını handle'lara çevirir. LightCompiler HDRI'ın 2D CDF'ini ve alan ışığıyla birlikte light tree'yi kurar.
  3. Yükleme (device/). KernelScene tamponları VRAM'e kopyalanır, film ve wavefront tamponları ayrılır. Session Rendering durumuna geçer.
  4. 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.
  5. Kesişim (device/optix). Işın TLAS'tan şişenin BLAS'ına, oradan da 41.207 numaralı üçgene iner. Kernel'a dönen yalnızca Hit {t, instId, primId, bary}.
  6. Yüzey bilgisi (kernel/). expandHit vertex normallerini barycentric ile karıştırır, instance transform'unu uygular, UV ve türevleri hesaplar. materialId camı gösterir; cam ışık yaymadığı için emisyon eklenmez. İlk sekme olduğu için albedo ve normal AOV'lere yazılır.
  7. Malzeme (kernel/bsdf). Dielectric etiketi switch'te kendi dalına gider. Cam tamamen specular olduğu için NEE atlanır. Fresnel'e göre kırılma seçilir; etaScale güncellenir, ışın şişenin içine girer.
  8. 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.
  9. 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.)
  10. Film (film/). addSample radyansı piksel filtresi ağırlığıyla beauty kanalına, karesini varyans kanalına, light group etiketine göre lg_window kanalına yazar.
  11. 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.
  12. Çı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:

Lo(x,ωo)=Le(x,ωo)+∫Ωfr(x,ωi,ωo) Li(x,ωi) ∣cos⁡θi∣ dωiL_o(x,\omega_o) = L_e(x,\omega_o) + \int_{\Omega} f_r(x,\omega_i,\omega_o)\, L_i(x,\omega_i)\, |\cos\theta_i|\, d\omega_i

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:

⟨F⟩=1N∑k=1Nf(Xk)p(Xk)\langle F \rangle = \frac{1}{N}\sum_{k=1}^{N} \frac{f(X_k)}{p(X_k)}

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:

ws(x)=ps(x)2∑tpt(x)2w_s(x) = \frac{p_s(x)^2}{\sum_t p_t(x)^2}

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ı

AlgoritmaGüçlü olduğu yerZayıf olduğu yerGPU uygunluğu
PT + NEE + MISGenel sahneler, dış mekanDar açıklıklar, kaustiklerÇok iyi
BDPTDolaylı aydınlatılan iç mekanlarSDS kaustikleri, bellekZor
SPPM / photon mappingKaustiklerBias, düşük yoğunluklu bölgelerOrta
VCM / UPSNeredeyse tüm yol tipleriKarmaşıklık, çerçeve başına maliyetZor
PT + path guidingZor iç mekanlarÖğrenme fazı, bellekOrta
MLTÇok zor yollarAnimasyonda titreşimZayı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:

C=Ctrav+ALANLCisect+ARANRCisectC = C_{trav} + \frac{A_L}{A} N_L C_{isect} + \frac{A_R}{A} N_R C_{isect}

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üphanePlatformDonanım hızlandırmaYan araçlar
EmbreeCPU (x86, ARM)SIMDCurves, subdivision, motion blur
OptiXNVIDIA GPURT coreAI denoiser, motion blur, curves
HIP RTAMD GPURay acceleratorSınırlı
DXR / Vulkan RTTüm modern GPU'larRT 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:

f(ωo,ωi)=D(ωh) G(ωo,ωi) F(ωo⋅ωh)4 ∣cos⁡θo∣ ∣cos⁡θi∣f(\omega_o,\omega_i) = \frac{D(\omega_h)\, G(\omega_o,\omega_i)\, F(\omega_o\cdot\omega_h)}{4\,|\cos\theta_o|\,|\cos\theta_i|}

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şımNasıl çalışırArtıEksi
Albedo scalingAlt katman, üst katmanın yansıttığı enerji kadar zayıflatılırUcuz, gürültüsüz, öngörülebilirKatmanlar arası iç yansımaları kaba yaklaşıklıyor
Stokastik katman değerlendirme (Guo ve ark. 2018)Katmanlar arası ışık Monte Carlo ile izlenirFiziksel olarak doğruPahalı 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öntemNasılBiasYan etki
Daha iyi örnekleme (MIS, guiding, MNEE)Yolu daha olası hale getirirYokUygulama maliyeti
Path regularization (Kaplanyan ve Dachsbacher 2013)Diffuse bir sekmeden sonra pürüzlülüğü yapay olarak artırırVar, küçükKaustikler yumuşar
ClampingDolaylı katkıları bir üst sınırla kırparVarEnerji 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 OIDNNVIDIA OptiX Denoiser
DonanımCPU ve GPU (CUDA, HIP, SYCL, Metal)Yalnızca NVIDIA GPU
GirdilerRenk, albedo, normalRenk, albedo, normal, hareket vektörleri
Zamansal modYok, kare kareVar
KullananCycles, V-Ray, Corona, ArnoldOctane, 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:

T(a,b)=exp⁡(−∫abσt(x) dx)T(a,b) = \exp\left(-\int_a^b \sigma_t(x)\, dx\right)

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şımNasılArtıEksi
Sabit tesselasyonHer yüz N seviye bölünürBasitBellek çok hızlı büyür
Adaptif tesselasyonEkranda kenar uzunluğuna göre bölünür (dicing rate)Kalite nereye lazımsa orayaKamera dışı ama yansımada görünen yüzeyler kaba kalır
Doğrudan patch kesişimiIşın limit yüzeyi doğrudan kesişirEn az bellekKesiş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şımNasılArtıEksi
CUDA unified memorySürücü sayfa hatalarıyla veriyi taşırNeredeyse kod değişikliği yokBellek 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çeklenirKarmaşık; bekleme yönetimi gerekir
SıkıştırmaBC formatları, quantize vertex'lerHer şeyle birlikte çalışırTek 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üşümGüçlü yanıZayıf yanıNerede kullanılır
ACES output transformSinema standardı, HDR ekran çıktıları1.x'te doygun renklerde kaymaFilm, VFX; Unreal'ın varsayılan filmic tonemapper'ı bu aileden
AgXDoygun ışıkları doğal şekilde beyaza götürürVarsayılan görünümü biraz solukBlender 4.0'dan beri varsayılan
Khronos PBR NeutralÜrün renklerini ve tonları korurSinematik kontrast vermezÜrün ve e-ticaret render'ı
Kendi filmic eğrinTam kontrolPipeline'ın geri kalanıyla uyumsuzluk riskiCorona 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ü

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

CPUGPUHibrit
BellekYüzlerce GB RAMVRAM ile sınırlı (out-of-core gerekir)En küçük cihaz sınırı belirler
HızTemelKutu başına kat kat hızlıİkisinin toplamına yakın
Shader esnekliğiOSL dahil sınırsızDerleme ve register kısıtlarıEn kısıtlı taraf belirler
EkosistemEmbree, OIIO, OpenVDB doğrudanCUDA ve OptiX'e bağımlılıkİki kod yolu
ÖrneklerCorona, Arnold CPUOctane, RedshiftV-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

KavramUnityUnreal
Path tracing entegratörüHDRP Path TracingPath Tracer (Movie Render Queue referans modu)
Ray tracing APIRayTracingShader, DXRRHI ray tracing katmanı, DXR / Vulkan RT
TLAS / BLASRayTracingAccelerationStructureRHI'daki ray tracing sahnesi ve geometri nesneleri
Katmanlı malzemeHDRP StackLitSubstrate
Malzeme grafıShader GraphMaterial Editor
Fiziksel kameraHDRP Physical CameraCine Camera Actor
Heterojen hacimlerLocal Volumetric FogHeterogeneous Volumes, Sparse Volume Texture
Geometri akışıKarşılığı yokNanite
Sanal textureStreaming Virtual TexturingVirtual Texturing
Renk yönetimiHDRP tonemapping (ACES, Neutral)Filmic tonemapper, OpenColorIO eklentisi
Sahne formatıUSD paketiUSD Stage Actor
Frame/render graphRenderGraphRDG (Render Dependency Graph)
Shader diliHLSL (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.

  1. Isınma: Ray Tracing in One Weekend serisi, C++ ile. Kilometre taşı: üç kitabın sonundaki sahneler.
  2. 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ı.
  3. Kesişim: Kendi BVH'nı SAH ile bir kez yaz, sonra Embree'ye geç ve ikisini karşılaştır.
  4. 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.
  5. GPU: OptiX megakernel, ardından SER; darboğaz kanıtlanırsa wavefront. CPU ile GPU arasında parite testleri.
  6. 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.
  7. Entegrasyon: Hydra render delegate ve artımsal güncellemeli IPR.
  8. Ayrıştırıcılar: Spektral rendering, MNEE veya VCM ile kaustikler, light group'lar, Cryptomatte, deep EXR.

Kaynaklar

Kitaplar ve tezler:

Referans kodlar:

Kütüphaneler ve standartlar:

Makale yıllarını kaynakların kendisinden tek tek doğrulamadım. Bir tekniği uygulamaya geçmeden önce ilgili makaleye bakmanı öneririm.