holodepth

Three.js · Geometri

Instancing (örnekleme ile performans)

1000 draw call yerine 1 draw call

Bilgisayar grafiklerinde en büyük performans düşmanı, GPU’nun kendisinden ziyade CPU ile GPU arasındaki iletişim trafiğidir (draw call). Sahnede 1000 adet ayrı küp varsa, CPU ekran kartına 1000 kez “Şimdi bunu çiz!” emri gönderir. Instancing, bu 1000 emri tek bir devasa emre dönüştürerek işlemci yükünü ciddi biçimde düşürür.

Draw call problemi ve çözümü

Standart Mesh yaklaşımında sahnedeki her nesne genelde ayrı “çiz” komutu doğurur. Bu komutların maliyeti çoğu zaman shader’dan değil, CPU → GPU tarafındaki state değişimlerinden gelir.

InstancedMesh ise tek bir geometri ve tek bir materyali bellekte bir kez saklar; sonra her kopyanın “nerede duracağı” bilgisini (matrix) değiştirerek binlerce kopyayı tek draw call ile çizer.

InstancedMesh anatomisi

Bir InstancedMesh oluştururken Three.js’e bu nesneden en fazla kaç adet olacağını söylersiniz. Her instance için şu verileri yönetebilirsiniz:

  • Matrix: Konum, rotasyon, ölçek (instance başına).
  • Color: Her kopyanın kendine has rengi (opsiyonel).

Matrix burada “tek paket” demek: position + rotation + scale bilgisinin, GPU’nun tek seferde okuyacağı şekilde bir araya getirilmiş halidir (Matrix4).

Pratik sezgi: instancing “çok obje” problemini çözer; ama her instance’ın geometri/materyal bakımından aynı sınıfta kalmasını ister.

GPU tarafında (WebGL2) instancing vertex shader içinde çoğu zaman gl_InstanceID ile temsil edilir; hangi instance’ın işlendiği bu indeksle ayrılır. Three.js hazır materyallerinde bu soyutlanır; ShaderMaterial veya özel vertex kodu yazarken bu kapı özellikle işe yarar.

Demo: 1000 Mesh vs 1 InstancedMesh (draw call farkı)

Aynı sahne, iki farklı yaklaşım: solda 1000 ayrı Mesh, sağda tek bir InstancedMesh. Üstteki sayaçlar gerçek zamanlı draw call ve FPS okur. Aşağıdaki demo sabitleri, doc-instancing-demos.js içindeki initDrawCallSplitDemo ile aynı rakamları özetler.

İsteğe bağlı ses: demo kutusundan ayrı sütun genişliğinde şerit — dosya adı İnstancing-Demo-1 (uzantı sırasıyla denenir).

Draw Call Karşılaştırma · split
Draw Calls: — FPS: — Draw Calls: — FPS: —
1000 Mesh
InstancedMesh
Buradaki “kazanç”, GPU’nun daha hızlı çizmesinden değil; CPU’nun daha az komut vermesinden gelir. Draw call sayısı düşer, CPU nefes alır.

Demo sabitleri tablosu (draw call split)

Bu blok doc-instancing-demos.js içindeki initDrawCallSplitDemo ile eşlenir. Dar görünümde (max-width: 980px) instance yerleşimi ızgara moduna geçer; masaüstünde küp dağılımı populateTransforms(count, 10) ile rastgeledir.

initDrawCallSplitDemo · Mesh / InstancedMesh
Sahne / rol Parametre Değer Tür
Örneklem sayısı data-inst-count 1000 (öznitelik yoksa JS varsayılanı da 1000) ↔ HTML
Yerleşim isInstancingCompactLayout() (max-width: 980px)populateNeatGrid(count, 0.34) · aksi → populateTransforms(count, 10) 🔄 Kırılım
Paylaşılan küp BoxGeometry 0.18 × 0.18 × 0.18 🔒 Sabit
Paylaşılan materyal MeshStandardMaterial color: 0xffffff · roughness: 0.4 · metalness: 0.2 🔒 Sabit
Sol / sağ sahne makeSharedScene ambientIntensity: 0.55 · directionalIntensity: 2.9 · yönlü konum (5, 8, 5) · sis kapalı 🔒 Sabit
Ortam ışık HemisphereLight 0xa7d8ff / 0x1b1d2a · yoğunluk 1.35 🔒 Sabit
Zemin PlaneGeometry + MeshStandardMaterial yüzey 50 × 50 · renk 0x0b0f16 · roughness: 0.95 · y = -2.2 · rotateX(-π/2) 🔒 Sabit
Kamera (taban) makeCamera PerspectiveCamera(45, 1, 0.1, 80) · başlangıç (12, 8, 14)lookAt(0,0,0) 🔒 Sabit
Kamera (orbit) her kare dar görünüm: FOV 50 · yarıçap 27 · y 10.5 · masaüstü: FOV 45 · yarıçap 18 · y 9 · açı t = performance.now() × 0.00025 🔒 Sabit
Renderer makeRenderer antialias: true · temizlik şeffaf · ACESFilmicToneMapping · toneMappingExposure: 1.5 · SRGBColorSpace · setPixelRatio(min(dpr, 2)) 🔒 Sabit
FPS örnekleyici makeFpsSampler 500 ms pencerede ortalama FPS 🔒 Sabit
Mesh tarafı createManyMeshes ızgara: düz dönüş / ölçek 1 · dağınık: rastgele dönüş (0…π) · ölçek 0.85 + rand×0.6 🔒 Mantık
Instanced tarafı createInstanced randomColor: false · aynı transform kuralları · instanceMatrix.needsUpdate = true 🔒 Sabit

Önemli kod kesiti

Sol ve sağ aynı positions dizisini kullanır; tek fark çoklu Mesh yerine tek InstancedMesh.

const count = Number(root.getAttribute("data-inst-count") || 1000) || 1000;
const neatLayout = isInstancingCompactLayout();
const positions = neatLayout
  ? populateNeatGrid(count, 0.34)
  : populateTransforms(count, 10);

const geo = new THREE.BoxGeometry(0.18, 0.18, 0.18);
const mat = new THREE.MeshStandardMaterial({
  color: 0xffffff,
  roughness: 0.4,
  metalness: 0.2,
});

createManyMeshes(leftScene, geo, mat, positions, neatLayout);
createInstanced(rightScene, geo, mat, positions, false, neatLayout);

Teknik tablo: Mesh vs InstancedMesh

Özellik Standart Mesh InstancedMesh
Draw call sayısı Obje sayısı kadar (n) 1 (genelde)
GPU bellek kullanımı Yüksek (veri tekrarlanır) Çok düşük (geometri bir kez saklanır)
Bireysel kontrol Kolay (her biri ayrı nesne) Orta (matrix / attribute üzerinden)
Kullanım alanı Eşsiz, az sayıda objeler Yapraklar, mermiler, kalabalıklar, yıldızlar

Draw call azalması, GPU’nun çizdiği toplam vertex yükünü sihirli biçimde sıfırlamaz: aynı geometri tekrar kullanılsa da, instance sayısı arttıkça vertex shader hâlâ her instance için geometriyi işler. Özet: draw call ↓ ama geometry / vertex maliyeti sahnede durmaya devam eder — ağır bir mesh’i binlerce instance ile çoğaltmak hâlâ pahalıdır.

Uygulama: 1000 nesneyi tek hamlede çizmek

Instancing kullanırken her kopyanın konumunu bir Matrix4 ile tanımlarız. En yaygın pratik: bir Object3D (“dummy”) ile transform kurup, matrix’i setMatrixAt ile instance indeksine yazmak.

const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const material = new THREE.MeshStandardMaterial({ color: 0xffffff });
const count = 1000;

// 1) InstancedMesh oluştur (Geometry, Material, Count)
const mesh = new THREE.InstancedMesh(geometry, material, count);
scene.add(mesh);

const dummy = new THREE.Object3D();

for (let i = 0; i < count; i++) {
    // 2) Her kopya için rastgele konum ayarla
    dummy.position.set(
        (Math.random() - 0.5) * 10,
        (Math.random() - 0.5) * 10,
        (Math.random() - 0.5) * 10
    );
    dummy.updateMatrix();

    // 3) Matrix’i ilgili index’e ata
    mesh.setMatrixAt(i, dummy.matrix);
}

// 4) Değişikliği GPU’ya bildir
mesh.instanceMatrix.needsUpdate = true;

Demo: instance sayısı ve renk (setColorAt)

Bu demo tek bir InstancedMesh ile sahneyi doldurur. Sayıyı artırınca aynı draw call içinde daha fazla instance çizilir. “Random color” açıkken setColorAt ile instance başına renk verilir. Kendi kodunuzda setColorAt kullanırken materyalde material.vertexColors = true olmalı; aksi hâlde renk buffer’ı hesaplansa bile shader tarafında görünmez — bu yüzden “renk niye yok?” takılmaları çok sık görülür. Ölçüler demo sabitleri tablosunda initInstanceControlDemo ile özetlenir.

İsteğe bağlı ses: demo kutusundan ayrı sütun genişliğinde şerit — dosya adı İnstancing-Demo-2 (uzantı sırasıyla denenir).

Instance dağılımı · kontrol
Draw Calls: — FPS: —
Count:
Renk modu
Density mode
Instancing CPU tarafını rahatlatır; ama instance’lar çok geniş alana yayılıysa tek bir bounding hacim yüzünden culling verimsizleşebilir (aşağıdaki notu oku).

Demo sabitleri tablosu (instance kontrolleri)

Bu blok doc-instancing-demos.js içindeki initInstanceControlDemo ve rebuild döngüsüyle eşlenir. Aynı küp/materyal paylaşılır; yoğunluk modu ışık yoğunluklarını ve kamera yörüngesini kaydırır.

initInstanceControlDemo · tek InstancedMesh
Sahne / rol Parametre Değer Tür
Count aralığı data-inst-count-range min 10 · max 10000 · adım 10 · varsayılan 1000 ↔ HTML
Renk modu data-inst-color-mode solid (varsayılan) · randomsetColorAt ile HSL s=0.72 l=0.68 ↔ HTML
Yoğunluk data-inst-density-mode clustered → yarıçap 6 · spread18 · ışıklar spread’de ortam 0.8 / yönlü 3.0 · clustered’da 0.55 / 2.9 ↔ HTML
Materyal MeshStandardMaterial paylaşılan küp ile aynı (0.18³ · beyaz · roughness 0.4 · metalness 0.2) · vertexColors random modda true 🔒 / ↔ JS
Yerleşim isInstancingCompactLayout() dar görünüm → populateNeatGrid(count, 0.34 | 0.52) · geniş → populateTransforms(count, radius) 🔄 Kırılım
Kamera yörüngesi rebuild içi taban yarıçap: count < 500 → 14 · < 2500 → 18 · < 7000 → 22 · aksi 26 · spread’de +5 · dar görünüm çarpanı 1.32 · y: clustered 9.5 / spread 11 · dar çarpan 1.12 🔒 Mantık
Orbit animasyonu renderFrame t = performance.now() × 0.00022 · position.set(cos(t)×orbitR, orbitY, sin(t)×orbitR) 🔒 Sabit
Kamera FOV dar / geniş 50 / 45 (split demosu ile aynı eşik 980px) 🔄 Kırılım
Sahne tabanı makeSharedScene draw call split ile aynı (hemi + zemin + başlangıç ışıkları); yoğunluk seçimine göre ortam/yönlü güncellenir 🔒 + ↔ UI
FPS makeFpsSampler 500 ms pencere 🔒 Sabit

Önemli kod kesiti

Renk modu materyalde vertexColors bayrağını değiştirir; konumlar yoğunluk ve dar görünüme göre ya ızgara ya da küre içi rastgele üretilir.

function createInstanced(
  scene,
  geometry,
  material,
  positions,
  randomColor = false,
  neat = false,
) {
  const count = positions.length;
  const mesh = new THREE.InstancedMesh(geometry, material, count);
  const dummy = new THREE.Object3D();

  for (let i = 0; i < count; i++) {
    dummy.position.copy(positions[i]);
    if (neat) {
      dummy.rotation.set(0, 0, 0);
      dummy.scale.setScalar(1);
    } else {
      dummy.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);
      dummy.scale.setScalar(0.85 + Math.random() * 0.6);
    }
    dummy.updateMatrix();
    mesh.setMatrixAt(i, dummy.matrix);
    if (randomColor) {
      mesh.setColorAt(i, new THREE.Color().setHSL(Math.random(), 0.72, 0.68));
    }
  }

  mesh.instanceMatrix.needsUpdate = true;
  if (randomColor) mesh.instanceColor.needsUpdate = true;
  scene.add(mesh);
  return mesh;
}

// rebuild() özeti — UI’dan count / renk / yoğunluk; ardından yukarıdaki fonksiyon
const count = Number(countRange.value || 1000) || 1000;
const colorMode =
  root.querySelector("[data-inst-color-mode]:checked")?.value || "solid";
const densityMode =
  root.querySelector("[data-inst-density-mode]:checked")?.value || "clustered";
const randomColor = colorMode === "random";
const radius = densityMode === "spread" ? 18 : 6;
const nextVertexColors = randomColor;
if (mat.vertexColors !== nextVertexColors) {
  mat.vertexColors = nextVertexColors;
  mat.needsUpdate = true;
}
const positions = compact
  ? populateNeatGrid(count, densityMode === "spread" ? 0.52 : 0.34)
  : populateTransforms(count, radius);
mesh = createInstanced(scene, geo, mat, positions, randomColor, compact);

Gelişmiş teknik: InstancedBufferAttribute

Her instance’ın sadece konumu değil, shader içerisindeki bir efekti (parlama hızı, bükülme miktarı vb.) farklı olsun istiyorsanız InstancedBufferAttribute kullanırsınız. Bu, her bir instance için özel bir veri paketini GPU’ya iletmenizi sağlar.

Mantık, “vertex attribute” ile aynıdır; tek fark, verinin instance başına bir kez ilerlemesidir (per-instance).

attribute float aOffset;
uniform float time;

void main() {
  vec3 p = position;
  p.y += sin(time + aOffset);
  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}

Ne zaman instancing kullanmamalıyım?

Instancing güçlüdür ama her senaryoya uymaz. Şunlarda genelde kullanmayın:

  • Her obje tamamen farklıysa (farklı geometri / topoloji).
  • Farklı materyal gerekiyorsa (her instance başka shader/texture).
  • Tek tek etkileşim çok yoğun ve sık ise (seçim, highlight, fizik vb.).

Özet: “çok kopya + aynı geometri/materyal” = instancing; “her şey farklı” = normal Mesh.

Holodepth notu: frustum culling dikkat

Instancing “tek parça” olduğu için culling davranışı değişir

Standart bir Mesh, kamera görüş alanının dışına çıktığında Three.js onu çizmez (culling). Ancak bir InstancedMesh içinde 10.000 obje varsa ve sadece 1 tanesi bile kameranın görüş alanındaysa, GPU 10.000 objenin tamamını işlemeye devam edebilir.

Bu yüzden instance’ları devasa alanlara yaymak yerine mantıklı gruplara bölmek (birkaç InstancedMesh) çoğu sahnede daha iyi sonuç verir.

Instancing ≠ LOD: Uzak nesneleri otomatik olarak basitleştirmez; çözüm hâlâ geometri seviyeleri, mesafe kuralları veya bilinçli tasarımdır. Instancing ≠ culling panzehiri: Görüş dışı instance’ları tek tek budamaz; frustum / portal mantığı ayrı tasarlanmalıdır (yukarıdaki paragraf).