Navigasyon eksen üçlüsü, 3D görüntüleyicilerin (Blender, FreeCAD, CAD araçları) köşesinde gördüğün küçük kırmızı/yeşil/mavi ok göstergesidir. X/Y/Z eksenlerinin yönünü gösterir ve kamerayla birlikte döner, böylece her zaman yukarının nereye olduğunu bilirsin. direction-cubes araç çubuğunun aksine — o tıklanabilir küp düğmelerinden bir ızgara — üçlü, koni ok başlıklı üç renkli çizgiden oluşan tek bir settir.
Bu yazı, üçlüyü render eden ve sürükleyerek döndürmene izin veren
minimal, bağımlılıksız bir WebGPU uygulaması gösterir. Tam kaynak en
altta; aşağıdaki canlı demo /scripts/navigation-axis-webgpu.js’ten
birebir aynı script’i yükler.
Döndürmek için sürükle. X=kırmızı, Y=yeşil, Z=mavi. WebGPU destekli tarayıcı gerekir (Chrome / Edge 113+).
Nasıl çalışır
Üçlü, harici kütüphanesi olmayan tek bir kendi kendine yeten script’tir.
Vertex shader standart bir perspektif MVP matrisi uygular
(proj * translate(0,0,-2.2) * rotateY(yaw) * rotateX(pitch)) ve
oklara derinlik hissi verir — kameraya dönük oklar, uzağa bakanlardan
biraz daha büyük görünür.
Parçalar:
- İki pipeline, tek shader. WGSL modülünde tek bir
vs_main/fs_mainçifti var. Çizgi pipeline’ı üç eksen gövdesiniline-list(6 vertex, 3 segment) olarak render eder. Üçgen pipeline’ı üç koni ok başlığınıtriangle-list(39 vertex, 36 indeks) olarak render eder. İkisi dedepthCompare: 'always'vedepthWriteEnabled: falsekullanır; böylece çizim sırasından bağımsız olarak üçlü her zaman görünür. - Koni ok başlıkları. Her ok başlığı 12 segmentli bir konidir:
1 uç vertex + 12 halka vertex’i = 13 vertex, 12 üçgen. Halka yarıçapı
AXIS_HEAD_SIZE * 0.25‘tir — budala şapkası değil ok gibi görünecek kadar dar. Halka, eksen yönüne dik iki vektör bulup koninin tabanı etrafında bir çember taramayla inşa edilir. - Uniform buffer. Tek bir
mat4x4<f32>(64 bayt) model-görünüm- projeksiyon matrisini tutar. Render döngüsü, işaretçi sürükleme olaylarıyla yürütülen yaw/pitch çiftinden her karede onu yeniden kurar. - Ayrı konum ve renk vertex buffer’ları. Her pipeline iki vertex
buffer’ı bağlar: konumlar için
@location(0), renkler için@location(1). Renk buffer’ı vertex başına olduğundan her eksen (ve ok başlığı) kendi RGB rengini alır: X=kırmızı[0.9, 0.2, 0.2], Y=yeşil[0.2, 0.8, 0.2], Z=mavi[0.2, 0.4, 0.9]. - Etkileşim. İşaretçi olayları yaw/pitch’i sürer; pitch ±90°‘nin hemen altına sınırlanır, böylece üçlü asla ters dönmez.
Kendi sayfanda kullanma
/scripts/navigation-axis-webgpu.js‘i
statik klasörüne koy ve nav-axis-canvas sınıflı bir canvas ekle —
script sayfadaki eşleşen her canvas’ı otomatik başlatır:
<canvas class="nav-axis-canvas"
style="width:240px;height:240px;"></canvas>
<script src="/scripts/navigation-axis-webgpu.js" defer></script>Canvas’ı CSS ile boyutlandır (width/height); script devicePixelRatio
ölçeklemesini ve derinlik texture’ı yeniden boyutlandırmayı dahili olarak
halleder.
Tam kaynak
// SPDX-FileCopyrightText: 2026 Uli Köhler <gitlab@techoverflow.net>
// SPDX-License-Identifier: CC0-1.0
//
// Minimal WebGPU navigasyon eksen üçlüsü.
// Üç renkli eksen okunu (X=kırmızı, Y=yeşil, Z=mavi) çizgi + koni ok
// başlıkları olarak render eder; sürüklemeyle kontrol edilen yaw/pitch
// çiftiyle döndürülür.
// Sayfadaki her <canvas class="nav-axis-canvas">'ı otomatik başlatır.
//
// Neden IIFE? Yardımcıların, sabitlerin veya WGSL kaynağının global
// kapsama sızmasını önlemek için her şeyi anında çağrılan fonksiyon
// ifadesiyle sarıyoruz. Script'in <script src=... defer> ile olduğu
// gibi, muhtemelen aynı sayfadaki diğer script'lerle birlikte dahil
// edilmesi amaçlanır, bu yüzden izolasyon önemli.
(function () {
'use strict';
// ─────────────────────────────────────────────────────────────────────
// Geometri sabitleri
// ─────────────────────────────────────────────────────────────────────
// Tüm koordinatlar orijin etrafındaki küçük bir birim küptedir.
// Perspektif kamera (daha sonra kurulur) üçlüyü z = -2.2'ye yerleştirir
// ve 45° görüş alanı kullanır, bu yüzden AXIS_LENGTH = 0.7 ok
// başlıkları kenarlara değmeden canvas'ın iyi bir bölümünü doldurur.
// Her eksen gövdesinin uzunluğu, orijinden ok başlığının tabanına
// kadar. Ok başlığı bu uzunluğun üstüne oturur, bu yüzden toplam eksen
// uzunluğu AXIS_LENGTH + AXIS_HEAD_SIZE'dır.
const AXIS_LENGTH = 0.7;
// Eksen yönü boyunca koni ok başlığının uzunluğu. Koninin taban
// yarıçapı bundan türetilir (aşağıdaki headRad'a bak).
const AXIS_HEAD_SIZE = 0.15;
// Doğrusal RGB (0-1) eksen renkleri. Bunlar gama düzeltmesi olmadan
// doğrudan fragment shader çıktısına yazılır — WebGPU'nun swapchain'i
// tarayıcı tarafından sRGB olarak ele alındığından, burada seçtiğin
// değerler ekranda gördüğün değerlerdir.
// X=kırmızı / Y=yeşil / Z=mavi kuralı Blender, FreeCAD ve çoğu CAD
// aracıyla aynıdır, bu da üçlüyü anında tanınabilir kılar.
const COLORS = {
X: [0.9, 0.2, 0.2], // kırmızı
Y: [0.2, 0.8, 0.2], // yeşil
Z: [0.2, 0.4, 0.9], // mavi
};
// ─────────────────────────────────────────────────────────────────────
// Minimal mat4 yardımcıları (sütun-öncelikli)
// ─────────────────────────────────────────────────────────────────────
// WebGPU / WGSL matrisleri sütun-öncelikli, yani eleman [col*4+row]
// olarak saklar. Float32Array'i transpoz adımı olmadan doğrudan uniform
// buffer'a yükleyebilmek için burada aynı düzeni yansıtıyoruz.
// Yalnızca ihtiyacımız olan birkaç matrisi uyguluyoruz: birim, çarpma,
// X ve Y etrafında döndürme, perspektif ve öteleme. gl-matrix veya
// benzerine bağımlılık yok — hedef tek bir kendi kendine yeten dosya.
function mat4Identity() {
const m = new Float32Array(16);
m[0] = m[5] = m[10] = m[15] = 1;
return m;
}
// (a * b) * v'nin önce b'yi, sonra a'yı uyguladığı anlamında a * b
// döndürür. Bu dosyada her yerde kullanılan kural budur: MVP,
// proj * view * model olarak kurulur, yani nokta model→view→clip
// olarak dönüştürülür.
function mat4Multiply(a, b) {
const r = new Float32Array(16);
for (let col = 0; col < 4; col++) {
for (let row = 0; row < 4; row++) {
let s = 0;
for (let k = 0; k < 4; k++) s += a[k * 4 + row] * b[col * 4 + k];
r[col * 4 + row] = s;
}
}
return r;
}
// X ekseni etrafında `a` radyan döndürme (sağ elli).
function mat4RotationX(a) {
const c = Math.cos(a), s = Math.sin(a);
const m = mat4Identity();
m[5] = c; m[6] = s;
m[9] = -s; m[10] = c;
return m;
}
// Y ekseni etrafında `a` radyan döndürme (sağ elli).
function mat4RotationY(a) {
const c = Math.cos(a), s = Math.sin(a);
const m = mat4Identity();
m[0] = c; m[2] = -s;
m[8] = s; m[10] = c;
return m;
}
// Sağ elli perspektif projeksiyon matrisi.
// Kamera -Z'ye bakar, bu yüzden görünür noktalarda z < 0.
// Bu WebGPU kuralıdır: clip space z [0, 1] içindedir (OpenGL'deki
// gibi [-1, 1] değil), bu yüzden m[10] = far/(near-far) ve m[14]'te
// ek near*far terimi var. Bunu yanlış yapmak, WebGL kodunu WebGPU'ya
// taşırken "geometrim kayboldu" hatalarının bir numaralı kaynağı.
function mat4Perspective(fovy, aspect, near, far) {
const f = 1 / Math.tan(fovy / 2);
const m = new Float32Array(16);
m[0] = f / aspect;
m[5] = f;
m[10] = far / (near - far);
m[11] = -1;
m[14] = (far * near) / (near - far);
return m;
}
// Saf öteleme matrisi (döndürme veya ölçek yok).
function mat4Translation(x, y, z) {
const m = mat4Identity();
m[12] = x; m[13] = y; m[14] = z;
return m;
}
// ─────────────────────────────────────────────────────────────────────
// Geometri: eksen çizgileri + koni ok başlıkları
// ─────────────────────────────────────────────────────────────────────
// Üçlü iki geçişte çizilir:
// 1. Çizgiler — orijinden her eksen ucuna üç çizgi segmenti.
// 2. Üçgenler — her eksen ucunda bir tane, üç koni.
// Konumlar ve renkler için ayrı vertex buffer'ları kullanıyoruz;
// böylece aynı renk buffer'ı kavramsal olarak paylaşılabiliyor ve
// pipeline'daki vertex layout basit kalıyor (iki bağlama, her biri
// float32x3).
// 3 çizgi segmenti (orijin → uç), 6 vertex, [x0,y0,z0, x1,y1,z1, ...]
// olarak düzenlendi. Her eksen (0,0,0)'da başlar ve (AXIS_LENGTH, 0, 0)
// vb.'de biter. Ok başlığı konisi uçtan *sonra* yerleştirilir, bu yüzden
// görünür ok AXIS_LENGTH'in biraz ötesine uzanır.
const lineVertices = new Float32Array([
0, 0, 0, AXIS_LENGTH, 0, 0, // X ekseni: orijin → (L, 0, 0)
0, 0, 0, 0, AXIS_LENGTH, 0, // Y ekseni: orijin → (0, L, 0)
0, 0, 0, 0, 0, AXIS_LENGTH, // Z ekseni: orijin → (0, 0, L)
]);
// Vertex başına renkler: her segmentin iki uç noktası eksen rengini
// paylaşır, bu yüzden tüm çizgi düz renktir.
const lineColors = new Float32Array([
...COLORS.X, ...COLORS.X,
...COLORS.Y, ...COLORS.Y,
...COLORS.Z, ...COLORS.Z,
]);
// line-list indeks buffer'ı: segment oluşturan vertex indeks çiftleri.
// (0,1) = X segmenti, (2,3) = Y segmenti, (4,5) = Z segmenti.
const lineIndices = new Uint32Array([0, 1, 2, 3, 4, 5]);
// Tek bir koni ok başlığı kur ve verilen dizilere ekle.
//
// Koninin tanımı:
// - `tip`'te 1 uç vertex
// - `base` etrafında çember halinde 12 halka vertex'i,
// base = tip - dir * headLen
// - 12 üçgen, her biri ucu komşu iki halka vertex'ine bağlar
//
// `dir` eksen boyunca bakan birim vektör olmalıdır (örn. [1,0,0]).
// `vertexOffset`, global vertex dizisindeki uç vertex'in indeksidir;
// böylece çağıran, indeks çakışması olmadan birden çok ok başlığını
// tek buffer'a paketleyebilir.
//
// Neden basit piramit değil de koni? 12 segmentli koni ucuzdur
// (36 indeks) ama burada kullanılan küçük boyutlarda yeterince yuvarlak
// görünür ve 4 yüzlü piramidin köşeli görünümünü önler.
function addArrowHead(positions, colors, indices, vertexOffset, tip, dir, color) {
const segments = 12;
const headLen = AXIS_HEAD_SIZE;
// Taban yarıçapı başlık uzunluğunun %25'i. Oku budala şapkası yerine
// ok gibi gösteren bu orandır: çok genişse koni gibi okunur, çok
// darsa kaybolur.
const headRad = AXIS_HEAD_SIZE * 0.25;
// Uç vertex (okun sivri ucu).
positions.push(tip[0], tip[1], tip[2]);
colors.push(...color);
// Taban merkezi = uç eksen boyunca headLen geriye taşınmış hali.
// Vertex halkası buraya merkezlenir ve `dir`'e dik düzlemde yatar.
const baseX = tip[0] - dir[0] * headLen;
const baseY = tip[1] - dir[1] * headLen;
const baseZ = tip[2] - dir[2] * headLen;
// `dir`'e dik düzlemde çember kurmak için hem `dir`'e hem birbirine
// dik iki birim vektör perp1 ve perp2 gerekir. Halkadaki herhangi
// bir nokta şöyledir:
// base + (perp1 * cos(θ) + perp2 * sin(θ)) * headRad
//
// perp1, `dir`'i paralel olmayan bir eksenle çarparak kurulur.
// `dir` ona neredeyse paralel (yani Z ekseni) değilse yedek eksen
// olarak (0,0,1) seçeriz; paralelse onun yerine (0,1,0) kullanırız.
// Bu, dejenere çapraz çarpımı önler.
let perp1;
if (Math.abs(dir[2]) < 0.9) {
// dir Z eksenine (yakın) paralel değil → (0,0,1) ile çapraz çarpım güvenli.
// cross(dir, (0,0,1)) = (dir.y*1 - dir.z*0, dir.z*0 - dir.x*1, ...) = (-dir.y, dir.x, 0)
perp1 = [-dir[1], dir[0], 0];
} else {
// dir (yaklaşık) (0,0,±1) → onun yerine (0,1,0) ile çapraz çarp.
// cross(dir, (0,1,0)) = (dir.z*0 - 0*1, 0*0 - dir.x*0, dir.x*1 - dir.y*0) = (0, -dir.z, dir.y)...
// dir≈(0,0,1) için sadeleştirilmiş: (0, -1, 0), yeterli.
perp1 = [0, -dir[2], dir[1]];
}
const p1len = Math.hypot(perp1[0], perp1[1], perp1[2]);
perp1 = [perp1[0] / p1len, perp1[1] / p1len, perp1[2] / p1len];
// perp2 = dir × perp1 — dir ve perp1 dik birim vektörler olduğundan
// zaten birim uzunlukta.
const perp2 = [
dir[1] * perp1[2] - dir[2] * perp1[1],
dir[2] * perp1[0] - dir[0] * perp1[2],
dir[0] * perp1[1] - dir[1] * perp1[0],
];
// θ'yı çember etrafında tarayarak halka vertex'lerini üret.
for (let i = 0; i < segments; i++) {
const angle = (i / segments) * Math.PI * 2;
const c = Math.cos(angle);
const s = Math.sin(angle);
positions.push(
baseX + (perp1[0] * c + perp2[0] * s) * headRad,
baseY + (perp1[1] * c + perp2[1] * s) * headRad,
baseZ + (perp1[2] * c + perp2[2] * s) * headRad,
);
colors.push(...color);
}
// Koninin 12 yan üçgenini kur. Her üçgen ucu (vertexOffset) komşu iki
// halka vertex'ine bağlar. Sarmalama sırası (uç → ring[i] → ring[i+1])
// tutarlıdır ama cullMode: 'none' ile render ettiğimizden burada
// önemsiz.
for (let i = 0; i < segments; i++) {
indices.push(
vertexOffset, // uç
vertexOffset + 1 + i, // ring[i]
vertexOffset + 1 + ((i + 1) % segments), // ring[i+1] (başa sarar)
);
}
}
// Üç ok başlığını (X, Y, Z) tek buffer setine kur.
// Her ok başlığı 13 vertex katkıda bulunur (1 uç + 12 halka), bu yüzden
// her birinden sonra offset 13 ilerletilir.
function buildArrowHeads() {
const positions = [];
const colors = [];
const indices = [];
let offset = 0;
// X ekseni: uç (AXIS_LENGTH, 0, 0), +X yönüne bakar.
addArrowHead(positions, colors, indices, offset,
[AXIS_LENGTH, 0, 0], [1, 0, 0], COLORS.X);
offset += 13;
// Y ekseni: uç (0, AXIS_LENGTH, 0), +Y yönüne bakar.
addArrowHead(positions, colors, indices, offset,
[0, AXIS_LENGTH, 0], [0, 1, 0], COLORS.Y);
offset += 13;
// Z ekseni: uç (0, 0, AXIS_LENGTH), +Z yönüne bakar.
addArrowHead(positions, colors, indices, offset,
[0, 0, AXIS_LENGTH], [0, 0, 1], COLORS.Z);
return {
positions: new Float32Array(positions),
colors: new Float32Array(colors),
indices: new Uint32Array(indices),
};
}
// ─────────────────────────────────────────────────────────────────────
// WGSL shader
// ─────────────────────────────────────────────────────────────────────
// Tek shader modülü, iki pipeline (çizgiler + üçgenler). Vertex aşaması
// konumları MVP matrisiyle dönüştürür ve vertex başına rengi fragment
// aşamasına olduğu gibi geçirir. Fragment aşaması rengi tam opaklıkta
// (alpha = 1.0) çıktılar — aydınlatma yok, renkler düz.
//
// Uniform buffer tek bir mat4x4<f32> (64 bayt) içerir. İki pipeline da
// aynı buffer'ı bağladığından, kare başına yalnızca bir kez güncelleriz.
const SHADER = /* wgsl */ `
struct Uniforms {
mvp: mat4x4<f32>,
};
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) color: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clipPosition: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
// Standart MVP dönüşümü: model-uzayı konumu → clip-uzayı konumu.
// 'position' yön değil nokta olduğundan w bileşeni 1.0'dır.
output.clipPosition = uniforms.mvp * vec4<f32>(input.position, 1.0);
// Vertex başına rengi değişmeden geçir; fragment shader doğrudan
// çıktılayacak.
output.color = input.color;
return output;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
// Düz renk, tam opak. Aydınlatma hesabı yok — üçlü gölgeli bir nesne
// değil, parlak, yüksek kontrastlı bir gösterge olmalı.
return vec4<f32>(input.color, 1.0);
}
`;
// ─────────────────────────────────────────────────────────────────────
// Canvas başına başlatma
// ─────────────────────────────────────────────────────────────────────
// Sayfadaki her <canvas class="nav-axis-canvas"> için bir kez çağrılır.
// WebGPU cihazını, pipeline'ları, geometri buffer'larını kurar ve render
// döngüsünü başlatır. Tüm GPU kaynakları closure'da yakalanır, böylece
// render döngüsü global durum olmadan erişebilir.
async function initCanvas(canvas) {
// --- WebGPU başlatma ---
// navigator.gpu giriş noktasıdır. Yoksa tarayıcı WebGPU'yu hiç
// desteklemiyor demektir (flag'siz Firefox veya eski tarayıcı).
// Sayfada bozuk boş bir dikdörtgen kalmaması için canvas'ı metin
// düğümüyle değiştiriyoruz.
if (!navigator.gpu) {
canvas.replaceWith(document.createTextNode('WebGPU is not supported in this browser.'));
return;
}
// requestAdapter() fiziksel GPU seçer. null ise kullanılabilir GPU
// yok (örn. GPU sürücüsüz headless ortam).
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
canvas.replaceWith(document.createTextNode('No WebGPU adapter available.'));
return;
}
// requestDevice() mantıksal cihaz oluşturur — sonraki tüm GPU
// işlemlerinin yapıldığı handle.
const device = await adapter.requestDevice();
// Canvas'ın WebGPU context'ini yapılandır. getPreferredCanvasFormat()
// platform için en uygun swapchain texture formatını döndürür
// (tipik olarak Windows'ta 'bgra8unorm', başka yerlerde 'rgba8unorm').
// alphaMode: 'premultiplied', canvas'ın sayfa arka planıyla
// harmanlanmasını sağlar — şeffafa temizlediğimiz ve beyaz canvas
// arka planının görünmesini istediğimiz için burada önemli.
const ctx = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
ctx.configure({ device, format, alphaMode: 'premultiplied' });
// WGSL shader'ı shader modülüne derle.
const shader = device.createShaderModule({ code: SHADER });
// --- Pipeline'lar ---
// Aynı shader modülünü paylaşan ama primitif topolojisi farklı iki
// render pipeline oluşturuyoruz:
// - triPipeline koni ok başlıklarını üçgenler olarak render eder
// - linePipeline eksen gövdelerini çizgiler olarak render eder
//
// İkisi de layout: 'auto' kullanır; bu, WebGPU'nun bind group
// layout'unu shader'dan türetmesini sağlar. Kullanışlı ama bind
// group oluştururken layout'u pipeline'dan (getBindGroupLayout(0)
// ile) almamız gerektiği anlamına gelir.
//
// İkisi de depthCompare: 'always' ve depthWriteEnabled: false
// kullanır. Bu kasıtlıdır: üçlü, her zaman tamamen görünür olması
// gereken, kendisi tarafından asla örtülmeyen bir kaplama
// göstergesidir. Derinlik testi açık olsaydı, dönüşe bağlı olarak
// koni üçgenleri arkalarındaki çizgi gövdelerini bazen gizlerdi ve
// bozuk görünürdü.
// Ok başlıkları için üçgen pipeline (derinlik testi yok — her zaman görünür)
const triPipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shader,
entryPoint: 'vs_main',
// İki vertex buffer, her biri arrayStride 12 (bir vec3<f32>):
// binding 0 → @location(0) konum
// binding 1 → @location(1) renk
buffers: [
{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] },
{ arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: 'float32x3' }] },
],
},
fragment: { module: shader, entryPoint: 'fs_main', targets: [{ format }] },
primitive: { topology: 'triangle-list' },
depthStencil: { format: 'depth24plus', depthCompare: 'always', depthWriteEnabled: false },
});
// Eksen gövdeleri için çizgi pipeline (derinlik testi yok — her
// zaman görünür). Topoloji dışında üçgen pipeline ile aynı.
const linePipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shader,
entryPoint: 'vs_main',
buffers: [
{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] },
{ arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: 'float32x3' }] },
],
},
fragment: { module: shader, entryPoint: 'fs_main', targets: [{ format }] },
primitive: { topology: 'line-list' },
depthStencil: { format: 'depth24plus', depthCompare: 'always', depthWriteEnabled: false },
});
// --- Geometriyi GPU buffer'larına yükle ---
// Her buffer, queue.writeBuffer ile yazabilmemiz için VERTEX (veya
// INDEX) kullanımı artı COPY_DST ile oluşturulur. Geometri statik
// olduğundan bir kez yükler ve bir daha dokunmayız.
// Çizgi geometrisi: 3 eksen gövdesi için konumlar, renkler ve indeksler.
const lineBuf = device.createBuffer({
size: lineVertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(lineBuf, 0, lineVertices);
const lineColBuf = device.createBuffer({
size: lineColors.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(lineColBuf, 0, lineColors);
const lineIdxBuf = device.createBuffer({
size: lineIndices.byteLength,
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(lineIdxBuf, 0, lineIndices);
// Ok başlığı geometrisi: 3 koni için konumlar, renkler ve indeksler.
const arrows = buildArrowHeads();
const arrowBuf = device.createBuffer({
size: arrows.positions.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(arrowBuf, 0, arrows.positions);
const arrowColBuf = device.createBuffer({
size: arrows.colors.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(arrowColBuf, 0, arrows.colors);
const arrowIdxBuf = device.createBuffer({
size: arrows.indices.byteLength,
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(arrowIdxBuf, 0, arrows.indices);
// --- Uniform buffer ---
// Tek bir mat4x4<f32> (MVP matrisi) = 64 bayt tutar.
// queue.writeBuffer ile her karede güncellenir. Bu buffer'ı referans
// alan pipeline başına bir bind group oluşturuyoruz; ikisi de aynı
// 64 bayta işaret ettiğinden, buffer'ı bir kez güncellemek her iki
// pipeline'ı da günceller.
const ubuf = device.createBuffer({
size: 64,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const triBindGroup = device.createBindGroup({
layout: triPipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: ubuf } }],
});
const lineBindGroup = device.createBindGroup({
layout: linePipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: ubuf } }],
});
// --- Derinlik texture'ı ---
// depthCompare: 'always' kullandığımız halde (derinlik asla
// fragment reddetmez), pipeline depthStencil durumu bildiriyorsa
// WebGPU yine de depth-stencil eki ister. Canvas boyutu değiştiğinde
// yeni boyutlara uyması için texture'ı yeniden oluşturuyoruz.
// 'depth24plus' formatı yaygın desteklenir.
let depthTex = null;
function ensureDepth(w, h) {
if (depthTex && depthTex.width === w && depthTex.height === h) return;
if (depthTex) depthTex.destroy();
depthTex = device.createTexture({
size: [w, h, 1], format: 'depth24plus', usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
}
// --- Etkileşim: döndürmek için sürükle ---
// Bir yaw/pitch çifti izliyoruz ve işaretçi hareketinden güncelliyoruz.
// Yaw, Y ekseni etrafında (yatay sürükleme), pitch X ekseni etrafında
// (dikey sürükleme) döner. Pitch ±90°'nin hemen altına sınırlanır,
// böylece üçlü asla baş aşağı dönmez — bu kafa karıştırıcı olurdu.
//
// setPointerCapture, sürükleme sırasında işaretçi canvas'tan çıksa
// bile pointermove olaylarını almaya devam etmemizi sağlar.
let yaw = 0.6, pitch = 0.4;
let dragging = false, lastX = 0, lastY = 0;
canvas.style.cursor = 'grab';
canvas.style.touchAction = 'none'; // dokunmatik cihazlarda kaydırmayı önle
canvas.addEventListener('pointerdown', (e) => {
dragging = true; lastX = e.clientX; lastY = e.clientY;
canvas.setPointerCapture(e.pointerId);
canvas.style.cursor = 'grabbing';
});
canvas.addEventListener('pointerup', () => {
dragging = false; canvas.style.cursor = 'grab';
});
canvas.addEventListener('pointermove', (e) => {
if (!dragging) return;
const dx = e.clientX - lastX, dy = e.clientY - lastY;
lastX = e.clientX; lastY = e.clientY;
// Piksel başına 0.01 rad — tam ekran genişliğinde sürükleme ≈ 6.3 rad ≈ 360°.
yaw += dx * 0.01;
pitch += dy * 0.01;
const lim = Math.PI / 2 - 0.05;
pitch = Math.max(-lim, Math.min(lim, pitch));
});
// --- Render döngüsü ---
// requestAnimationFrame ile animasyon karesi başına bir kez çalışır.
// Her karede:
// 1. CSS boyutu değiştiyse canvas + derinlik texture'ını yeniden
// boyutlandır.
// 2. Mevcut yaw/pitch'ten MVP matrisini yeniden kur.
// 3. MVP'yi uniform buffer'a yükle.
// 4. Render pass kaydet: temizle, çizgileri çiz, üçgenleri çiz.
// 5. Gönder ve sonraki kareyi zamanla.
function frame() {
// HiDPI'yı işle: devicePixelResolution'da render et ama yüksek
// DPI'lı telefonlarda aşırı dolgu oranını önlemek için 2×'te sınırla.
// clientWidth CSS piksel boyutudur; canvas.width çizim buffer'ı
// boyutudur.
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const cw = Math.max(1, Math.floor(canvas.clientWidth * dpr));
const ch = Math.max(1, Math.floor(canvas.clientHeight * dpr));
if (canvas.width !== cw || canvas.height !== ch) {
canvas.width = cw; canvas.height = ch;
}
ensureDepth(cw, ch);
// MVP matrisini kur:
// mvp = proj * view * rot
// burada:
// proj = perspective(45°, aspect, 0.1, 100)
// view = translate(0, 0, -2.2) — üçlüyü kameradan uzağa it
// rot = rotateY(yaw) * rotateX(pitch)
//
// Kamera orijinde ve -Z'ye bakıyor. translate(0,0,-2.2) üçlüyü
// z = -2.2'ye taşır; bu, [0.1, 100] near/far aralığının çok
// içindedir. 45° FOV ile 2.2 mesafenin birleşimi, 0.7 uzunluklu
// eksenlerin görünümün rahat bir bölümünü doldurmasını sağlar.
const aspect = cw / ch;
const proj = mat4Perspective(Math.PI / 4, aspect, 0.1, 100);
const view = mat4Translation(0, 0, -2.2);
const rot = mat4Multiply(mat4RotationY(yaw), mat4RotationX(pitch));
const mvp = mat4Multiply(proj, mat4Multiply(view, rot));
device.queue.writeBuffer(ubuf, 0, mvp);
// Render pass'i kaydet. Renk ekini tamamen şeffafa (alpha = 0)
// temizliyoruz, böylece canvas'ın CSS arka planı görünür. Derinlik
// eki 1.0'a (en uzağa) temizlenir.
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: ctx.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0, b: 0, a: 0 },
loadOp: 'clear', storeOp: 'store',
}],
depthStencilAttachment: {
view: depthTex.createView(),
depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store',
},
});
// Pass 1: 3 eksen çizgi segmentini çiz (6 indeks).
pass.setPipeline(linePipeline);
pass.setBindGroup(0, lineBindGroup);
pass.setVertexBuffer(0, lineBuf); // @location(0) konumlar
pass.setVertexBuffer(1, lineColBuf); // @location(1) renkler
pass.setIndexBuffer(lineIdxBuf, 'uint32');
pass.drawIndexed(6);
// Pass 2: 3 koni ok başlığını çiz (3 × 12 üçgen = 108 indeks).
// Aynı bind group (aynı MVP), farklı vertex buffer'ları.
pass.setPipeline(triPipeline);
pass.setBindGroup(0, triBindGroup);
pass.setVertexBuffer(0, arrowBuf);
pass.setVertexBuffer(1, arrowColBuf);
pass.setIndexBuffer(arrowIdxBuf, 'uint32');
pass.drawIndexed(arrows.indices.length);
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
// ─────────────────────────────────────────────────────────────────────
// Otomatik başlatma
// ─────────────────────────────────────────────────────────────────────
// Sayfadaki her <canvas class="nav-axis-canvas">'ı bul ve başlat.
// Belge hâlâ yükleniyorsa (script <head>'den veya defer ile çalıştı),
// canvas'ların var olması için DOMContentLoaded'ı bekle. Belge zaten
// hazırsa (script geç enjekte edildiyse) hemen başlat.
function initAll() {
document.querySelectorAll('canvas.nav-axis-canvas').forEach(initCanvas);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAll);
} else {
initAll();
}
})();Script ve yukarıdaki satır içi liste aynıdır ve ikisi de CC0-1.0
altında yayınlanmıştır. Buradaki sürüm, oklara derinlik veren standart
bir perspektif MVP kullanan ve görüş açısından bağımsız olarak üçlünün
her zaman görünür kalması için depthCompare: 'always' koruyan,
sadeleştirilmiş bağımlılıksız bir porttur.