TypeScript'te WebGL

TypeScript'te WebGL

Bu makale, WebGL'in TypeScript ile nasıl kullanılacağını açıklar.

WebGL kavramını, en basit yapılandırmasını, render işlemini, uzantılarını ve sınıf tasarımını adım adım örnek kodlarla tanıtacağız.

YouTube Video

TypeScript'te WebGL

WebGL, GPU'yu tarayıcıda doğrudan kontrol etmenizi sağlayan düşük seviyeli bir API'dir.

TypeScript kullanarak, tip güvenliği, kod tamamlama ve yapılandırılmış kodlama sayesinde WebGL'deki 'uygulama hatalarını' büyük ölçüde azaltabilirsiniz.

TypeScript özellikle aşağıdaki noktalarda etkilidir:.

  • WebGL nesnelerinin türleri netleşir.
  • Shader değişkenlerinin kullanımındaki hatalar azaltılabilir.
  • Yapıyı takip etmek daha kolay hale gelir.

WebGL'in minimum yapılandırması

WebGL ile render almak için gereken en az koşullar şunlardır:.

  • <canvas> elementi
  • WebGL bağlamı
  • Verteks shader
  • Fragment shader

Öncelikle, ekranın ilklendirilebileceği bir durum oluşturalım.

Canvas ve WebGL Bağlamını Edinmek

İlk olarak canvas ve WebGLRenderingContext elde edin.

Burada, null güvenliğine vurgu yaparak TypeScript kodlaması kullanıyoruz.

 1const canvas = document.getElementById('gl') as HTMLCanvasElement | null;
 2
 3if (!canvas) {
 4	throw new Error('Canvas not found');
 5}
 6
 7const gl = canvas.getContext('webgl');
 8
 9if (!gl) {
10	throw new Error('WebGL not supported');
11}
  • Bu noktada, gl tüm WebGL işlemlerinin giriş noktası olur.

WebGL'in çalıştığını doğrulamak için ekranı temizleme

Render öncesi arka plan renginin doldurulup doldurulamayacağını kontrol edin.

Bu, WebGL'in doğru çalıştığını doğrulamak için yapılan ilk denetlemedir.

1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);
  • Bu noktaya kadar, canvas koyu gri bir renkle doldurulacaktır.

Shader (gölgelendirici) nedir?

WebGL'de çizim işlemleri GLSL adlı özel bir dil ile tanımlanır. GLSL'de, temel olarak iki tür shader hazırlarsınız: tepe noktası (vertex) shaderları ve parça (fragment) shaderları.

Öncelikle bu iki shader'ı kullanarak 'en temel çalışan bir shader' oluşturacağız.

Verteks shader yazmak

Verteks shader'ları, çizilecek noktaların veya şekillerin konumlarını belirler.

Aşağıda, ekranın ortasına bir nokta yerleştiren basit bir kod örneği var.

1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5	gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;
  • a_position, JavaScript tarafında geçirilen koordinattır.

Fragment shader yazmak

Fragment shader'ları, ekranda görüntülenecek renkleri belirler.

Bu kez, her seferinde kırmızı renk çıktısı verir.

1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5	gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;
  • WebGL'de precision her zaman belirtilmelidir.

Shader'ları derlemek için fonksiyon oluşturmak

Shader oluşturma işlemi her zaman aynı olduğundan, bunu bir fonksiyon haline getiriyoruz.

 1function createShader(
 2	gl: WebGLRenderingContext,
 3	type: number,
 4	source: string
 5): WebGLShader {
 6	const shader = gl.createShader(type);
 7	if (!shader) {
 8		throw new Error('Failed to create shader');
 9	}
10
11	gl.shaderSource(shader, source);
12	gl.compileShader(shader);
13
14	if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
15		throw new Error(gl.getShaderInfoLog(shader) ?? 'Shader compile error');
16	}
17
18	return shader;
19}
  • Bu fonksiyon, belirtilen türde bir shader oluşturur, kaynak kodunu derler, hata oluşursa hata fırlatır ve başarıyla derlenirse shader'ı döndürür.

Program oluşturmak (shader seti)

Verteks ve fragment shader'larını birleştirin.

Buna WebGLProgram denir.

 1const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
 2const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
 3
 4const program = gl.createProgram();
 5if (!program) {
 6	throw new Error('Failed to create program');
 7}
 8
 9gl.attachShader(program, vertexShader);
10gl.attachShader(program, fragmentShader);
11gl.linkProgram(program);
12
13if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
14	throw new Error(gl.getProgramInfoLog(program) ?? 'Program link error');
15}
  • Bu kod, verteks ve fragment shader'larını tek bir programda birleştirir ve render için kullanılabilirliğini kontrol eder.

Verteks verilerini hazırlamak

Burada, bir üçgen çizmek için tepe noktası verilerini hazırlayacağız.

1// Vertex positions for a triangle in clip space (x, y)
2// Coordinates range from -1.0 to 1.0
3const vertices = new Float32Array([
4	0.0,  0.5,   // Top vertex
5	-0.5, -0.5,  // Bottom-left vertex
6	0.5, -0.5,   // Bottom-right vertex
7]);
  • WebGL'deki koordinat sistemi -1.0 ile 1.0 arasındadır.

Bir tampon (buffer) oluşturup GPU'ya aktarmak

Sonraki adımda, tepe noktası verisi dizisini GPU tarafından kullanılabilir hale getirmek için veriyi bir tampon (buffer) kullanarak aktarırız.

1const buffer = gl.createBuffer();
2if (!buffer) {
3	throw new Error('Failed to create buffer');
4}
5
6gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
7gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
  • Bu kod, verteks verilerini saklamak için bir tampon oluşturur ve içeriğini GPU'ya aktarır.

Attribute ile buffer'ı ilişkilendirme

Shader'daki a_position ile önceden oluşturulmuş buffer'ı ilişkilendirin.

 1const positionLocation = gl.getAttribLocation(program, 'a_position');
 2
 3gl.enableVertexAttribArray(positionLocation);
 4gl.vertexAttribPointer(
 5	positionLocation,
 6	2,
 7	gl.FLOAT,
 8	false,
 9	0,
10	0
11);

Render alma (Çizim)

Son olarak, programı kullanarak render komutunu gönderin.

1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);
  • Bu programı çalıştırdığınızda ekranda kırmızı bir üçgen gözükecek.

WebGL ile sınıf tasarımı

WebGL, buyurgan (imperative) bir API'dir, bu yüzden kod doğrudan yazılırsa hızla karmaşıklaşabilir. Sınıf tasarımıyla, başlatma, render ve kaynak yönetimi süreçlerini net şekilde ayırabilirsiniz.

Burada TypeScript'in avantajlarını kullanarak, tasarımı sorumluluk ayrımı, tekrar kullanılabilirlik ve sürdürülebilirlik odaklı olarak aşamalı şekilde geliştireceğiz.

Tasarım Politikası (Minimal ama Pratik)

Sınıf aşağıdaki rollere ayrılmıştır.

  • GLApp genel başlatma ve render işlemini kontrol eder.
  • ShaderProgram shader'ları yönetir.
  • TriangleMesh verteks verilerini yönetir.

İlk olarak, her şeyi kontrol eden sınıfı oluşturalım.

Bir WebGL uygulaması için giriş sınıfı

GLApp, canvas ve WebGL bağlamını yönetir ve render işlemi için başlangıç noktasıdır.

 1export class GLApp {
 2	private gl: WebGLRenderingContext;
 3	private shader!: ShaderProgram;
 4	private mesh!: TriangleMesh;
 5
 6	constructor(private canvas: HTMLCanvasElement) {
 7		const gl = canvas.getContext('webgl');
 8		if (!gl) {
 9			throw new Error('WebGL not supported');
10		}
11		this.gl = gl;
12	}
13
14	initialize() {
15		this.gl.clearColor(0.1, 0.1, 0.1, 1.0);
16
17		this.shader = new ShaderProgram(this.gl);
18		this.mesh = new TriangleMesh(this.gl);
19	}
20
21	render() {
22		this.gl.clear(this.gl.COLOR_BUFFER_BIT);
23
24		this.shader.use();
25		this.mesh.draw(this.shader);
26	}
27}
  • Bu sınıf, WebGL'in başlatma ve render süreçlerini yönetir ve shader'lar ile mesh'ler kullanarak ekrana çizimden sorumludur.
  • initialize ve render işlemlerini ayırarak, gelecekte yeniden başlatma ve animasyonu desteklemek daha kolay olur.

Shader'ları yöneten sınıf

Ardından, shader oluşturma, birleştirme ve kullanımını tek bir yerde toplayacak şekilde shader'ları yönetecek bir sınıf oluşturun. Bu sayede, render tarafından sadece bunları 'kullanmak' yeterlidir.

 1export class ShaderProgram {
 2	private program: WebGLProgram;
 3
 4	constructor(private gl: WebGLRenderingContext) {
 5		const vertexSource = `
 6		attribute vec2 a_position;
 7		void main() {
 8			gl_Position = vec4(a_position, 0.0, 1.0);
 9		}
10		`;
11
12		const fragmentSource = `
13		precision mediump float;
14		void main() {
15			gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
16		}
17		`;
18
19		const vs = this.createShader(this.gl.VERTEX_SHADER, vertexSource);
20		const fs = this.createShader(this.gl.FRAGMENT_SHADER, fragmentSource);
21
22		const program = this.gl.createProgram();
23		if (!program) throw new Error('Program create failed');
24
25		this.gl.attachShader(program, vs);
26		this.gl.attachShader(program, fs);
27		this.gl.linkProgram(program);
28
29		if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
30			throw new Error(this.gl.getProgramInfoLog(program) ?? 'Link error');
31		}
32
33		this.program = program;
34	}
35
36	use() {
37		this.gl.useProgram(this.program);
38	}
39
40	getAttribLocation(name: string): number {
41		return this.gl.getAttribLocation(this.program, name);
42	}
43
44	private createShader(type: number, source: string): WebGLShader {
45		const shader = this.gl.createShader(type);
46		if (!shader) throw new Error('Shader create failed');
47
48		this.gl.shaderSource(shader, source);
49		this.gl.compileShader(shader);
50
51		if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
52			throw new Error(this.gl.getShaderInfoLog(shader) ?? 'Compile error');
53		}
54
55		return shader;
56	}
57}
  • Bu sınıf, verteks ve fragment shader'ın oluşturulması, bağlanması ve kullanılmasını birleştirir; böylece render eden taraf, shader'ları güvenle kullanabilir.
  • getAttribLocation fonksiyonunu açığa çıkararak, mesh tarafı buradan güvenle referans alabilir.

Mesh'leri (verteks veri) yöneten sınıf

Ayrıca tepe noktası verilerini yönetmek için bir sınıf oluşturacağız. Mesh sınıflarını çizilecek objeye göre bölerek genişletmek kolaylaşır.

 1export class TriangleMesh {
 2	private buffer: WebGLBuffer;
 3	private vertexCount = 3;
 4
 5	constructor(private gl: WebGLRenderingContext) {
 6		const vertices = new Float32Array([
 7			0.0,  0.5,
 8			-0.5, -0.5,
 9			0.5, -0.5,
10		]);
11
12		const buffer = gl.createBuffer();
13		if (!buffer) throw new Error('Buffer create failed');
14
15		this.buffer = buffer;
16
17		gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
18		gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
19	}
20
21	draw(shader: ShaderProgram) {
22		const gl = this.gl;
23		const positionLoc = shader.getAttribLocation('a_position');
24
25		gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
26		gl.enableVertexAttribArray(positionLoc);
27		gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
28
29		gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
30	}
31}
  • Bu sınıf, üçgenin verteks verisini yönetir ve bunu shader'a bağlayarak asıl render işlemini üstlenir.
  • Çizim için gereken tüm bilgiler TriangleMesh içinde bulunur.

Başlangıç noktası (Kullanım örneği)

Son olarak, uygulamayı başlatmak için sınıfları birleştirin.

1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();
  • Bu yapı ile animasyon veya birden çok mesh eklemek kolaylaşır.

TypeScript ile WebGL yazmak için pratik ipuçları

TypeScript ile WebGL kullanmanın şu avantajları vardır:.

  • WebGL işlemlerini sınıflara dönüştürerek, rollere göre düzenleyebilir, bakımı ve genişletmeyi kolaylaştırabilirsiniz.
  • Render ve shader yönetimi gibi sorumlulukları ayırarak kodun okunabilirliğini artırabilirsiniz.
  • TypeScript'in tip tamamlama özelliğini kullanarak, WebGL API çağrılarında veya parametre belirtmede hata riskini azaltabilirsiniz.

Özet

TypeScript kullanarak, düşük seviyeli WebGL işlemleri bile tip güvenliği ve yapı ile rahatça kontrol edilebilir. Minimum yapılandırmadan render'a kadar olan akışı anlayarak ve başlatma, render ve kaynak yönetimi gibi rolleri sınıf tasarımıyla ayırarak, okunabilirlik ve sürdürülebilirliği iyileştirebilirsiniz. Aşamalar halinde uygulayarak, WebGL'i bir kara kutu olmaktan çıkaran ve gerçek işlerinizde uygulayabileceğiniz pratik bilgiler olarak öğrenebilirsiniz.

Yukarıdaki makaleyi, YouTube kanalımızda Visual Studio Code'u kullanarak takip edebilirsiniz. Lütfen YouTube kanalını da kontrol edin.

YouTube Video