WebGL на TypeScript

В этой статье объясняется использование WebGL на TypeScript.

Мы последовательно познакомимся с основами WebGL, минимальной конфигурацией, рендерингом, расширениями и проектированием классов с помощью примеров кода.

YouTube Video

WebGL на TypeScript

WebGL — это низкоуровневый API, который позволяет напрямую управлять графическим процессором (GPU) в браузере.

Используя TypeScript, вы можете значительно сократить количество 'ошибок реализации' в WebGL благодаря безопасности типов, автодополнению кода и структурированному программированию.

TypeScript особенно эффективен в следующих аспектах:.

  • Типы объектов WebGL становятся очевидны.
  • Ошибки при работе с переменными шейдеров можно сократить.
  • Структура становится более наглядной и понятной.

Минимальная настройка WebGL

Вот минимальные требования для рендеринга с помощью WebGL:.

  • Элемент <canvas>
  • WebGL контекст
  • Вершинный шейдер
  • Фрагментный шейдер

Сначала создадим состояние, в котором можно инициализировать экран.

Получение Canvas и контекста WebGL

Сначала получите canvas и WebGLRenderingContext.

Здесь мы используем TypeScript с акцентом на безопасную работу с null.

 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}
  • В этот момент gl становится точкой входа для всех операций WebGL.

Очистка экрана для проверки работы WebGL

Перед рендерингом проверьте, заполняется ли фоновый цвет.

Это первичная проверка работоспособности WebGL.

1gl.clearColor(0.1, 0.1, 0.1, 1.0);
2gl.clear(gl.COLOR_BUFFER_BIT);
  • До этого момента холст будет заполнен темно-серым цветом.

Что такое шейдер?

В WebGL процессы отрисовки описываются на специальном языке GLSL. В GLSL вы в основном готовите два типа шейдеров: вершинные шейдеры и фрагментные шейдеры.

Сначала создадим «минимальный рабочий шейдер» с использованием этих двух типов шейдеров.

Написание вершинного шейдера

Вершинные шейдеры определяют положение точек или фигур для отрисовки.

Ниже приведён простой код, размещающий одну точку по центру экрана.

1const vertexShaderSource = `
2attribute vec2 a_position;
3
4void main() {
5	gl_Position = vec4(a_position, 0.0, 1.0);
6}
7`;
  • a_position — это координата, передаваемая со стороны JavaScript.

Написание фрагментного шейдера

Фрагментные шейдеры определяют цвета, отображаемые на экране.

В этот раз он всегда выводит красный цвет.

1const fragmentShaderSource = `
2precision mediump float;
3
4void main() {
5	gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
6}
7`;
  • В WebGL всегда необходимо указывать precision.

Создание функции для компиляции шейдеров

Поскольку процесс создания шейдера всегда одинаковый, оформим его как функцию.

 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}
  • Эта функция создаёт шейдер указанного типа, компилирует исходный код, выбрасывает ошибку при неудаче и возвращает скомпилированный шейдер при успехе.

Создание программы (набора шейдеров)

Объедините вершинный и фрагментный шейдеры в одну программу.

Это называется WebGLProgram.

 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}
  • Этот код связывает вершинный и фрагментный шейдеры в одну программу и проверяет их готовность к рендерингу.

Подготовка вершинных данных

Здесь мы подготовим вершинные данные для отрисовки треугольника.

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 варьируется от -1.0 до 1.0.

Создание буфера и передача его на GPU

Далее, чтобы массив вершинных данных стал пригоден для использования на GPU, мы передаем данные с помощью буфера.

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);
  • Этот код создаёт буфер для хранения вершинных данных и передаёт его содержимое на GPU.

Связывание атрибута с буфером

Свяжите a_position в шейдере с ранее созданным буфером.

 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);

Рендеринг

Наконец, используйте программу для выполнения команды рендеринга.

1gl.useProgram(program);
2gl.drawArrays(gl.TRIANGLES, 0, 3);
  • После запуска этой программы на экране появится красный треугольник.

Проектирование классов с WebGL

WebGL — это императивный API, поэтому код быстро разрастается, если писать его без структуры. Благодаря проектированию классов вы можете чётко разделить инициализацию, рендеринг и управление ресурсами.

Здесь мы воспользуемся преимуществами TypeScript и шаг за шагом будем строить архитектуру с акцентом на разделение ответственности, повторное использование и удобство поддержки.

Принципы проектирования (минимально, но практично)

Класс разделён на следующие роли.

  • GLApp управляет общей инициализацией и рендерингом.
  • ShaderProgram управляет шейдерами.
  • TriangleMesh управляет вершинными данными.

Сначала создадим класс, который управляет всем процессом.

Главный класс для WebGL-приложения

GLApp управляет холстом и контекстом WebGL и служит точкой входа для рендеринга.

 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}
  • Этот класс управляет процессами инициализации и рендеринга в WebGL и отвечает за отрисовку на экране с помощью шейдеров и сеток.
  • Разделяя методы initialize и render, проще в будущем поддерживать переинициализацию и анимацию.

Класс для управления шейдерами

Затем создайте класс для управления шейдерами, объединяя создание, связывание и использование шейдеров в одном месте. Это позволяет рендерингу фокусироваться только на их «использовании».

 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}
  • Этот класс объединяет создание, связывание и использование вершинных и фрагментных шейдеров, позволяя безопасно использовать их при рендеринге.
  • Благодаря наличию метода getAttribLocation, его можно безопасно использовать со стороны сетки.

Класс для управления сетками (вершинными данными)

Мы также создадим класс для управления вершинными данными. Разделяя классы сеток по объекту для отрисовки, легко расширять функционал.

 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}
  • Этот класс управляет вершинными данными треугольника и, связывая их с шейдером, выполняет сам рендеринг.
  • Вся необходимая информация для рендеринга содержится внутри TriangleMesh.

Точка входа (пример использования)

В конце объедините классы для запуска приложения.

1const canvas = document.getElementById('gl') as HTMLCanvasElement;
2
3const app = new GLApp(canvas);
4app.initialize();
5app.render();
  • С такой архитектурой добавление анимаций или нескольких сеток становится проще.

Практические советы по написанию WebGL на TypeScript

Использование TypeScript с WebGL даёт следующие преимущества:.

  • Переведя процедуры WebGL в классы, их можно организовать по ролям, что облегчает поддержку и расширение.
  • Разделяя ответственность (например, рендеринг и управление шейдерами), повышается читаемость кода.
  • Используя автодополнение типов TypeScript, можно сократить ошибки при вызове WebGL API и указании параметров.

Резюме

Благодаря TypeScript даже низкоуровневые процессы WebGL можно надёжно реализовать с типобезопасностью и структурой. Понимая процесс от минимальной настройки до рендеринга и применяя классы для разделения ролей (инициализация, рендеринг, управление ресурсами), вы улучшаете читаемость и удобство поддержки. Реализуя пошагово, вы сможете изучить WebGL как практическое знание, а не «чёрный ящик», и применять его в реальных проектах.

Вы можете следовать этой статье, используя Visual Studio Code на нашем YouTube-канале. Пожалуйста, также посмотрите наш YouTube-канал.

YouTube Video