holodepth

WebGL · Textures · UV · Sampling

WebGL: Texture Mapping ve veri okuma mantığı

Texture mapping, 2D bir görüntü dosyasını (texture) 3D bir geometrinin yüzeyine matematiksel bir sözleşmeyle yerleştirme sürecidir. GPU açısından texture, “renkli bir resim” olmanın ötesinde; koordinatlara göre sorgulanan büyük bir veri tablosudur.

Bu sayfanın odağı, doku yerleşimini “malzeme ayarı” gibi değil; veri akışı gibi okumaktır: UV adresi vertex’ten gelir, rasterizasyon onu üçgene dağıtır, fragment shader o adresle texture’dan veri çeker. Sonuçta gördüğünüz şey bir “resim yapıştırma” değil, GPU’nun her fragment için yaptığı sistematik bir lookup işlemidir.

Texture mapping nedir? (2D → 3D eşleme)

En basit haliyle texture mapping, iki boyutlu bir düzlemi üç boyutlu bir yüzeyin üzerine “sarmaktır”. Kritik nokta şudur: 3D yüzey üzerindeki bir noktayı, 2D doku üzerindeki hangi noktaya eşleyeceğinizi açıkça tanımlamazsanız, GPU hangi veriyi okuyacağını bilemez.

Bu yüzden texture mapping, geometri kadar adresleme problemidir: “Bu yüzeyin şu köşesi, dokunun hangi koordinatını temsil ediyor?” sorusuna sayısal bir cevap verir.

Bunu “yüzey → doku” çeviren bir eşleme fonksiyonu gibi düşünün. Geometri size “neredeyim?” (konum/normal gibi) bilgisini getirir; texture ise “hangi veriyi okumalıyım?” sorusuna cevap verebilmek için bir adres ister. Texture mapping, bu adresi üretme/taşıma sözleşmesidir — yani 3D noktayı 2D koordinat sisteminde anlamlı bir yere bağlar.

Bu yüzden aynı mesh üzerinde iki farklı şeyi birbirinden ayırmak önemlidir: (1) mesh’in UV yerleşimi (adres haritası), (2) o adreslerle okunacak texture içeriği (veri). Aynı UV setiyle tamamen farklı görselleri okuyabilirsiniz; tersine, texture aynı kalsa bile UV’yi değiştirirseniz desen yüzeyde bambaşka bir şekilde “akar”.

Mini teşhis sezgisi: doku “yanlış yerde” görünüyorsa çoğu zaman sorun renklerde değil, adreslemededir. Doku yüzeyde kaymış, köşeler ters dönmüş, bir bölgede aşırı gerilmiş veya bir yerde sıkışmış hissediyorsanız, “hangi veri okunuyor?”dan önce “hangi UV adresine bakılıyor?” sorusunu sorun.

UV koordinat mantığı (adresleme sistemi)

Texture’ın yüzey üzerinde nereye düşeceğini UV koordinatları belirler. 3D uzaydaki x, y, z gibi; doku düzleminde de yatay eksen U, dikey eksen V diye adlandırılır.

  • Eksenler: 2D düzlemde yatay eksen U, dikey eksen V’dir.
  • Aralık: Koordinatlar çoğu akışta \((0,0)\) ile \((1,1)\) arasına normalize edilir. \((0,0)\) doku köşelerinden birini, \((1,1)\) karşı köşeyi temsil eder; “hangi köşe?” detayı (üst-alt tersliği gibi) pipeline ve doku yükleme sözleşmesiyle ilgilidir.
  • Vertex başına veri: Her vertex, 3D konumunun yanında hangi UV adresine sahip olduğunu da taşır; bu yüzden UV, tıpkı pozisyon gibi bir attribute girdisidir.

Altın cümle

Texture’ın yüzey üzerinde nereye düşeceğini UV belirler.

Önemli kavramsal ayrım: UV, “dünyada” veya “kameraya göre” ölçülen bir koordinat değildir; çoğu modelleme akışında mesh’in kendisine gömülü yerel bir adres bilgisidir. Bu yüzden modeli sahnede taşısanız veya döndürseniz bile UV değişmez; değişen, o UV ile örneklediğiniz verinin ekrana taşınma şeklidir.

Pratikte 3B bir yüzeyi 2B bir düzleme açarken her şeyi tek parça hâlinde ve bozulmasız “sermek” mümkün olmayabilir. Bu yüzden modellerde sıkça UV adaları (ayrık parçalar) ve seam (kesim çizgileri) görürsünüz: amaç, yüzeyi 2B alanda daha yönetilebilir parçalara bölmek ve gerilmeyi/çarpıtmayı kontrol etmektir. Doku deseni bir bölgede uzuyorsa, çoğu zaman mesele shader’dan önce bu açılım sözleşmesindedir.

UV’nin \([0,1]\) aralığı “yaygın” bir sözleşmedir; zorunlu bir duvar değildir. \((1.2, -0.3)\) gibi değerler de anlamlı olabilir; bu durumda texture’ın sınırın ötesinde nasıl davranacağı wrapping kuralıyla belirlenir (tekrar mı, kenara yapışma mı?).

Interpolation: vertex’ten fragment’a yolculuk

Doku yerleşimi tek bir aşamada “bitmez”; pipeline’ın birkaç halkası birlikte çalışır. Üçgenin köşelerinde UV bilinir; ama üçgenin içindeki her fragment için “hangi UV?” sorusu yeniden cevaplanmalıdır.

  • Vertex shader: Her vertex’e ait UV’yi alır ve bir sonraki aşamaya taşınacak şekilde (genelde varying ile) dışarı verir.
  • Rasterizasyon: GPU, köşelerdeki UV değerlerini üçgenin yüzeyine dağıtır; yani aradaki noktalar için UV’yi interpolate eder.
  • Fragment shader: Üçgen içindeki her fragment, “kendisine düşen” yumuşatılmış UV adresini hazır alır.

Buradaki “interpolate”, pratikte şunu yapar: üçgenin içindeki her nokta, köşelerin bir karışımı gibi davranır ve UV de bu karışıma göre ara değer alır. Böylece texture’dan okuyacağınız adres her fragment için ayrı ayrı oluşur; aksi halde üçgenin içi ya tek bir UV’ye sabitlenir ya da “hangi piksel hangi köşeye yakın?” gibi kaba bir seçimle blok blok görünürdü.

Kritik ama çoğu zaman görünmez detay: bu ara değer üretimi, perspektifli sahnelerde “doğru” görünmesi için genellikle perspective-correct interpolation mantığıyla yapılır. Yani UV, ekrandaki 2D mesafeye göre körlemesine değil; projeksiyonun getirdiği derinlik etkisi hesaba katılarak yayılır. Bu yüzden eğik yüzeylerde doku akışının doğal görünmesi, yalnızca doğru UV’den değil, doğru interpolasyon sözleşmesinden de gelir.

Mini teşhis sezgisi: doku, kameraya yaklaşınca “yüzey üzerinde yüzüyor” gibi kayıyor veya eğik yüzeylerde beklenmedik gerilme yapıyorsa, sorun bazen sampling ayarından önce UV’nin nasıl taşındığı (varying türü, normalize/scale hatası, yanlış koordinat uzayı) olabilir. Bu sayfada hatırlanacak şey basit: fragment’teki UV, vertex’teki UV’nin GPU tarafından yayılmış hâlidir; aradaki sözleşme bozulursa okunan adres de bozulur.

Sezgi: UV’yi “taşıma” işi vertex’te başlar, “okuma” işi fragment’te biter; ortada ise GPU’nun otomatik yaptığı bir dağıtım/ara değer üretimi vardır.

Texture sampling (doku örnekleme)

Fragment shader aşamasında GPU, elindeki UV adresini kullanarak texture’dan renk (veya başka bir kanal verisi) çeker. Bu işlem “resim okumak”tan çok, koordinatlara göre yapılan bir lookup gibi düşünülmelidir: “Bana \((u,v)\) adresindeki veriyi ver.”

  • Lookup: Shader, sampler üzerinden “şu UV’deki değeri” ister.
  • Veri kaynağı: Texture, büyük bir 2D veri alanıdır; her sorgu size bir texel (veya filtreleme sonucu birkaç texel’in birleşimi) döndürür.

Bu noktada “sampler” kelimesini iki parçalı düşünmek faydalı olur: (1) hangi texture? (veri kaynağı), (2) nasıl okuyayım? (okuma kuralı). Çünkü sampling sadece “şu adresteki tek hücreyi ver” demek değildir; aynı UV için farklı filtreleme, mip seçimi veya wrapping davranışıyla farklı sonuçlar üretebilirsiniz. Bu yüzden filtering ve mipmapping bölümü, sampling’in “kalite + maliyet” tarafını tamamlar.

İkinci kritik bakış: texture’dan okuduğunuz şey her zaman “renk” olmak zorunda değildir. Shader açısından texture, RGBA kanallarına paketlenmiş genel bir veridir: maske, roughness, metalness, AO, emissive, hatta bazı akışlarda yükseklik/derinlik gibi sayısal alanlar bu mekanizma üzerinden okunur. Yani sampling, “görsel boya”dan çok bir veri çekme protokolüdür; hangi kanalın ne anlama geldiği ise materyal sözleşmesidir.

Mini sezgi: UV adresi “hangi veriye bakıyorum?” sorusudur; sampling sonucu ise “o veriyi hangi çözünürlükte ve hangi yumuşatma kuralıyla okuyorum?” sorusunun cevabıdır. Bu yüzden bir doku uzakta titriyorsa veya yakında blok blok görünüyorsa, sorun çoğu zaman “texture bozuk” değil, sampling sözleşmesinin (filter + mip + wrap) beklentiyle uyuşmamasıdır.

Bu yüzden texture mapping’in en güçlü kavramsal cümlesi şudur: texture, bir “fotoğraf” gibi görünse bile GPU tarafında bir adreslenebilir veri kaynağıdır.

1.00
0.00
Inspect Sampling Data
UV Sampling Explorer: Plane üzerinde imleci gezdir ve fragment’in interpolasyonla oluşan UV adresiyle texture’dan nasıl veri çektiğini izle: wrapping, filtering ve mip seviyesi “okunan texel”i değiştirir.

Filtering ve mipmapping (kalite + performans)

Ekran pikselleri ile texture pikselleri (texel) her zaman birebir eşleşmez. Kamera yakında veya uzakta olabilir; yüzey eğik olabilir; aynı texel ekranda birden fazla piksele yayılabilir ya da çok sayıda texel tek piksele sıkışabilir. Bu uyuşmazlık kalite sorunları (bulanıklık, tırtıklanma, titreşim) ve performans maliyeti doğurur.

Buradaki temel ayrım iki uç senaryodur: büyütme (magnification) ve küçültme (minification). Kamera çok yaklaşınca bir texel ekranda büyük bir alana yayılır; kamera uzaklaşınca ise çok sayıda texel tek bir pikselin içine “sıkışır”. Bu iki uç, farklı kalite problemleri üretir: büyütmede “blok blok pikseller”, küçültmede ise “moiré / titreşim / aliasing” baskın hâle gelir.

  • Filtering (Nearest vs Linear): Örneklenen değer tek texel’den mi gelsin (keskin ama bloklu), yoksa komşu texel’ler karıştırılsın mı (yumuşak ama bazen “çamur” hissi)?
  • Mipmapping: Nesne uzaklaştıkça, texture’ın daha düşük çözünürlüklü sürümleri seçilerek hem bellek bant genişliği hem de aliasing baskılanır; uzak yüzeylerdeki titreşim çoğu zaman mip seviyeleriyle dramatik biçimde azalır.

Mipmapping sezgisi basittir: uzak bir yüzey, ekranda çok az piksel kaplıyorsa, “tam çözünürlüklü” dokudan okumak gereksiz ayrıntıyı tek piksele yığar ve zaman içinde titreşir. Mip zinciri, texture’ın küçük sürümlerini hazır tutarak GPU’nun “bu mesafede şu detay yeterli” demesini sağlar. Böylece hem görsel stabilite artar hem de minification tarafında okuma maliyeti düşer.

Bedeli de vardır: mip seviyeleri ek bellek kullanır. Kaba sezgi olarak, tam mip zinciri çoğu durumda texture belleğine yaklaşık \(\sim 1/3\) ek yük getirir; ama karşılığında özellikle uzak yüzeylerdeki shimmer/aliasing dramatik biçimde azalır.

Son küçük not: yüzey kameraya çok eğikse, bir pikselin “gördüğü” doku alanı tek eksende değil, elips gibi uzar. Bu durumda standart filtreleme yetersiz kalabilir; daha gelişmiş örnekleme teknikleri (örn. anisotropic yaklaşım) bu eğik yüzeylerdeki bulanıklık/titreşim dengesini iyileştirmek için vardır. Bu sayfanın bağlamında hatırlanacak şey, problemin kaynağının “UV” değil, texel → piksel ölçek uyuşmazlığı olduğudur.

Wrapping mantığı (sınırların ötesi)

UV koordinatları her zaman \([0,1]\) aralığında kalmayabilir. Örneğin \(u = 1.5\) gibi bir değer geldiğinde, “doku sınırın ötesinde nasıl davranacak?” sorusuna bir örnekleme kuralı gerekir. Bu kural, texture’ın nasıl “adreslendiğinin” parçasıdır.

  • Repeat: Doku kendini tekrar eder; duvar kağıdı gibi düşünün.
  • Clamp to Edge: En kenardaki texel, sınırın ötesine doğru uzatılır; doku taşmasında kenar rengi hâkim olur.

Wrapping’i “UV’yi düzeltme” işlemi gibi değil, adres taşması olduğunda hangi adrese düşeceğim? sorusunun cevabı gibi düşünün. Yani UV \(1.5\) olduğunda GPU paniklemez; sadece “bu adresi tekrar mı edeyim, kenara mı yapıştırayım?” kuralını uygular ve sampling normal şekilde devam eder.

Bu yüzden wrapping kararı, görselin karakterini doğrudan değiştirir: Repeat seçtiğinizde desen periyodik olur ve UV’yi bilinçli biçimde \([0,1]\) dışına taşırmak bir “tiling” tekniğine dönüşür. Clamp tarafında ise doku kenarına yaklaşınca yeni içerik gelmez; kenar texel’i uzar ve bu, özellikle atlas veya tek parça görsellerde “sınırdan dışarı sızma”yı kontrol etmek için kullanılır.

Mini teşhis sezgisi: bir atlas kullanırken veya doku adaları (seam) yakınında “başka bölgeden renk taşması” görüyorsanız, bu çoğu zaman UV’nin kendisinden çok örnekleme sınır davranışı ve filtrelemenin komşu texel’leri karıştırmasıyla ilişkilidir. Kenara yakın okumalar, lineer filtrelemede sınırın ötesinden veri “karıştırabilir”; wrapping burada devreye giren sözleşmedir.

Wrapping, “UV’yi nasıl üretiyorum?” sorusundan bağımsız değildir: tekrar eden desenler, atlas kullanımı veya bilinçli taşırma efektleri çoğu zaman bu sözleşmeye dayanır.

Güçlü anlatım: “resim” değil, “veri”

Texture’ı bir resim olarak düşünmek başlangıçta yardım eder; ama shader yazmaya veya hataları teşhis etmeye geldiğinizde daha doğru çerçeve şudur:

Texture aslında bir resim değil, GPU’nun koordinatlara göre okuduğu bir veri kaynağıdır.

Bu bakış açısı, “texture space vs world space” ayrımını da netleştirir. Doku kendi 2D uzayında sabittir; nesne sahnede hareket etse veya dönse bile, vertex’lerin taşıdığı UV koordinatları değişmediği sürece doku yüzey üzerinde “kaymaz” — çünkü okunan adresler değişmemiştir. Kayma görmek genelde UV’nin, modelin veya shader’daki koordinat dönüşümünün beklenmedik şekilde değişmesinden doğar.

Bu cümle, “neden bazı materyallerde doku özellikle kaydırılır?” sorusunu da anlaşılır kılar. Çünkü kaydırma aslında görüntüyü değil, adresi değiştirir: UV’ye bir offset eklemek, farklı bir satır/sütundan veri okumaktır. Bu yüzden su akışı, hologram taraması, enerji kalkanı gibi efektler çoğu zaman geometriyi hareket ettirmeden, yalnızca UV adresini zamanla değiştirerek üretilir — shader açısından hâlâ yapılan şey koordinata göre veri okumaktır.

Diğer tarafta “world space” ifadesi, texture’ın kendisinin dünyada durması anlamına gelmez; adres üretiminde hangi uzayı kullandığınız anlamına gelir. UV ile (mesh’e gömülü yerel adres) örneklemek, dokuyu nesneyle birlikte “taşır”. Dünya uzayındaki konumdan türetilen bir koordinatla örneklemek ise dokuyu sanki dünyaya sabitlemiş gibi hissettirebilir: nesne hareket ederken desen yerinde kalır ve yüzey üzerinde kayıyormuş gibi görünür. Bu, hata da olabilir, bilinçli bir tasarım kararı da — kritik olan, “hangi uzaydan adres üretiyorum?” sorusunu bilerek cevaplamaktır.

Son bağlayıcı sezgi: texture’ı veri kaynağı olarak gördüğünüzde, pek çok problem kategorize olur. “Bozuk ışık” ile “bozuk doku” farklı görünse de, ikisi de çoğu zaman aynı temel soruya iner: ben bu hesabı hangi uzayda ve hangi adresle yapıyorum? UV, interpolation, sampling, filtering ve wrapping bölümleri; bu adresin nasıl doğduğunu, nasıl taşındığını ve nasıl okunduğunu tek zincirde toplar.

Bağlantı haritası (kimin görevi ne?)

Texture mapping’i tek cümleyle “hat akışı” olarak yazarsak: UV buffer’da doğar → vertex’te taşınır → rasterizasyonda yayılır → fragment’te texture’dan veri okunur. Aşağıdaki rol listesi, bu zincirde hangi parçanın neyi “sahiplendiğini” hızlıca hatırlatır.

  • Buffer: Ham UV verisi (ve pozisyon/normal gibi diğer attribute’lar) buradan GPU’ya akar.
  • Vertex shader: UV’yi alır ve rasterizasyona taşınacak şekilde iletir.
  • Fragment shader: Elindeki UV adresiyle texture’dan veri okur (sampling).

Debug sırasında bu harita, sorunu hızlı sınıflandırmaya yarar:

  • UV yok / yanlışsa: doku tamamen “rastgele” görünür, tek renge saplanır veya beklenmedik bir bölgede toplanır — problem çoğu zaman buffer’daki UV seti veya attribute bağlamasındadır.
  • UV var ama yüzeyde akış bozuksa: eğik yüzeylerde gerilme, kameraya yaklaşınca kayma hissi veya seam yakınında kopma görürsünüz — problem çoğu zaman “taşıma + yayma” sözleşmesindedir (varying/interpolation).
  • UV doğru ama görüntü kalitesi bozuksa: blok blok büyüme, uzakta titreşim, atlas kenarından renk sızması gibi durumlarda problem çoğu zaman sampling kuralındadır (filter/mip/wrap).

Bu sayfanın geri kalanı aslında aynı zincirin farklı halkalarını “yakın plan” inceler: 2. bölüm adresi (UV), 3. bölüm adresin yayılmasını, 4. bölüm okumayı (sampling), 5–6 ise okuma kalitesini belirleyen kuralları anlatır.

Three.js bağlantısı (üst seviye soyutlama)

Üst seviye projelerde bu süreci çoğu zaman “elle yönetmezsiniz”. Three.js’te kullandığınız Texture, map, repeat ve benzeri ayarlar; arka plandaki UV adresleme, sampling, filtering ve wrapping sözleşmelerinin kullanıcı dostu bir arayüzüdür.

WebGL tarafındaki kazanım, bu ayarların “görsel tercih” olmasının yanında hangi veri okuma kuralını değiştirdiğini bilmekte yatar: “neden bulanık?”, “neden titriyor?”, “neden tekrar ediyor?” gibi sorular bu sayfadaki temel mekanizmaya iner.