min.

WebGL 入門

2020-07-17# creative code

透過上面範例大致可以粗略的知道從 OpenGL 一路到 WebGL 怎麼運作的了,接下來要開始進入 WebGL 的重頭戲,GLSL 到底怎麼寫。

GLSL

從前面的敘述得知,Shader 就是一個大量資料進來時在每一個像素點都會幫你執行一次的程式,所以以一個像素點都要等於特定顏色的 Shader 來說,基本上會是上述架構:

void main() {
	gl_FragColor = vec4(1.0,0.0,1.0,1.0);
}
  • 將 shader 要執行的部分放在 main() 之中,就像大部分的 C/C++ 一樣。
  • 也有一些 GLSL 內建的輸入跟輸出值、型別跟函數 e.g. gl_FragColorvec4
  • 在 GLSL 裡面的數值需要 normalize,轉換為 0 - 1 之間。

接著我們可能會希望 Shader 有更多的變化,我們可以透過四種資料型態來增加 Shader 的變化:

  1. Uniform: Context 中的共同變數 e.g. uniform float u_time 指的就是現在的系統時間。
uniform float u_time;
	void main() {
		gl_FragColor = vec4(abs(sin(u_time)),0.0,0.0,1.0);
}
  1. Buffer and Attribute:Buffer 通常用來儲存一系列的陣列資料,像是:座標點、座標對應顏色等等,而 Attributes 怎麼讀取 Buffer Array 的設定,從 WebGL 裡面匯入,舉例來說:如果今天是一次要讀 Array 裡面幾個值出來、什麼資料型態、從第幾個位置等等。
attribute vec4 a_position;
void main() {
	gl_Position = a_position;
}
  1. Varyings:從 Vertex Shader 到 Fragment Shader 中間傳遞的資料,也可以理解成:在每個 Pixel 上的轉換資料。

Vertex Shader

attribute vec4 a_position;
uniform vec4 u_offset;
varying vec4 v_positionWithOffset;

void main() {
	gl_Position = a_position + u_offset;
	v_positionWithOffset = a_position + u_offset;
}
  • **Fragment Shader **
precision mediump float;
varying vec4 v_positionWithOffset;
void main() {
	// convert from clip space (-1 <-> +1) to color space (0 -> 1).
	vec4 color = v_positionWithOffset * 0.5 + 0.5
	gl_FragColor = color;
}
  1. Textures:關於材質的點陣資料。

我們需要先從 OpenGL 這邊匯入 Texture 的點陣資料:

var tex = gl.createTexture();gl.bindTexture(gl.TEXTURE_2D, tex);

var level = 0;var width = 2;var height = 1;var data = new Uint8Array([
	255,
	0,
	0,
	255, // a red pixel
	0,
	255,
	0,
	255, // a green pixel
]);
gl.texImage2D(
	gl.TEXTURE_2D,
	level,
	gl.RGBA,
	width,
	height,
	0,
	gl.RGBA,
	gl.UNSIGNED_BYTE,
	data,
);

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
var someSamplerLoc = gl.getUniformLocation(someProgram, 'u_texture');
var unit = 5; // Pick some texture unit
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.uniform1i(someSamplerLoc, unit);

接著在 Fragment Shader 這邊引用。

precision mediump float;
uniform sampler2D u_texture;

void main() {
	vec2 texcoord = vec2(0.5, 0.5);
	gl_FragColor = texture2D(u_texture, texcoord);
}

以上簡述了 GLSL 跟 WebGL 的架構,以及 GLSL 可以接受的資料類型,接下來我們就要初步的從零開始做一個有一點點不一樣的效果:

<body>
<canvas id="container"></div>
<script>
	const canvas = document.querySelector('canvas');
	const gl = canvas.getContext('webgl');
	const vsGLSL = `
		precision mediump float;
		attribute vec4 position;
		void main() {
			gl_Position = position;
		}
	`;

	const fsGLSL = `
		precision mediump float;
		uniform vec2 u_resolution;
		uniform float u_time;
		vec3 colorA = vec3(0.149,0.141,0.912);
		vec3 colorB = vec3(1.000,0.833,0.224);
		void main() {
			vec3 color = vec3(0.0);
			float pct = abs(sin(u_time));
			color = mix(colorA, colorB, pct);
			gl_FragColor = vec4(color,1.0);
		}
	`;

	const vertexShader = gl.createShader(gl.VERTEX_SHADER);
	gl.shaderSource(vertexShader, vsGLSL);
	gl.compileShader(vertexShader);
	if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
		throw new Error(gl.getShaderInfoLog(vertexShader))
	};

	const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
	gl.shaderSource(fragmentShader, fsGLSL);
	gl.compileShader(fragmentShader);

	if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
		throw new Error(gl.getShaderInfoLog(fragmentShader))
	};

	const prg = gl.createProgram();
	gl.attachShader(prg, vertexShader);
	gl.attachShader(prg, fragmentShader);
	gl.linkProgram(prg);

	if (!gl.getProgramParameter(prg, gl.LINK_STATUS)) {
		throw new Error(gl.getProgramInfoLog(prg))
	};

	const positionLoc = gl.getAttribLocation(prg, 'position');
	const timeLocation = gl.getUniformLocation(prg, "u_time");
	const vertexPositions = new Float32Array([
		0, 0.7,
		0.5, -0.7,
		-0.5, -0.7,
	]);

	const positionBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, vertexPositions, gl.STATIC_DRAW);
	gl.enableVertexAttribArray(positionLoc);
	gl.vertexAttribPointer(
		positionLoc,
		2,
		gl.FLOAT,
		false,
		0,
		0,
	);

	gl.useProgram(prg);
	function renderLoop(timeStamp) {
		gl.uniform1f(timeLocation, timeStamp/1000);
		gl.drawArrays(gl.TRIANGLES, 0, 3);
		window.requestAnimationFrame(renderLoop);
	}

	renderLoop();
</script>
</body>

WebGL 3D

好不容易用 2D 的角度介紹完 WebGL、GLSL 的部分,那 3D 怎麼做?我們需要做一個矩陣的轉換,然而矩陣轉換在程式裡面會讓 3D 的操作變得非常不直覺。

Three.js

以上簡述了 WebGL 3D 的部分,也深刻感受到 WebGL 有多麽不直覺,所以為了更符合大家操作 3D 的邏輯,Three.js 誕生了。Three.js 的邏輯基本上如我們一般操作 3D 軟體的邏輯相同,有下列內容需要被設定:

  • Render 渲染
  • Scene 場景

  • Mesh 物體

  • Geometry 幾何

  • Material / Texture 材質

  • Light 光線

  • Camera 攝影機

Three.js 只是將這些底層 WebGL 的部分幫我們算好打包

import './style.css';
import * as THREE from 'three';
import {
    OrbitControls
} from 'three/examples/jsm/controls/OrbitControls';
// Canvas
const canvas = document.querySelector('canvas.webgl');
// Sizes
const sizes = {
    width: 800,
    height: 600,
};
const cursor = {
    x: 0,
    y: 0,
};
window.addEventListener('mousemove', evt => {
    cursor.x = evt.clientX / sizes.width - 0.5;
    cursor.y = -(evt.clientY / sizes.height - 0.5);
});
// 建立場景
const scene = new THREE.Scene();
// 建立物件,並設定他的形狀跟材質
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1, 5, 5, 5), new THREE.MeshBasicMaterial({
    color: 0xcccccc
}), );
// 加入場景
scene.add(mesh);
// 建立攝影機
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height);
camera.position.z = 2;
camera.lookAt(mesh.position);
scene.add(camera);
// 控制器
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
// 渲染器
const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
});
renderer.setSize(sizes.width, sizes.height);
const tick = () => {
    controls.update();
    renderer.render(scene, camera);
    window.requestAnimationFrame(tick);
};
tick();

完成,在經過千山萬水之後,讓我們看一段 three.js 的程式碼感受簡化了多少事情:

一個 3D 的正方形就誕生了。

Canvas Element vs Canvas API vs WebGL API

事實上 Canvas 跟 WebGL 是不同的 JavaScript API7,雖然都要透過 HTML canvas 來顯示。Canvas API 也可以繪製 3D 而且使用上比較直覺接近腳本語言但彈性比較低,以一個 2D 的 MDN 範例為例:

Reference

# creative code

© 2020 minw Powered by Gatsby