WebGPU ile Navigasyon Eksen Üçlüsü Render Etme

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:

  1. İki pipeline, tek shader. WGSL modülünde tek bir vs_main/ fs_main çifti var. Çizgi pipeline’ı üç eksen gövdesini line-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 de depthCompare: 'always' ve depthWriteEnabled: false kullanır; böylece çizim sırasından bağımsız olarak üçlü her zaman görünür.
  2. 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.
  3. 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.
  4. 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].
  5. 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:

example.html
<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

navigation-axis-webgpu.js
// 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.


Check out similar posts by category: JavaScript WebGPU Web