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).
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.
| 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).
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.
| 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) · random →
setColorAt ile HSL s=0.72 l=0.68
|
↔ HTML |
| Yoğunluk | data-inst-density-mode |
clustered → yarıçap 6 · spread →
18 · ışı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).