目录
- WebGL介绍 1.1. WebGL基本原理 1.2. WebGL工作原理 1.3. WebGL 着色器和 GLSL
- 图像处理 2.1. WebGL 图像处理
- 2D 转换、旋转、伸缩、矩阵 2.1. WebGL 2D 图像转换 2.2. WebGL 2D 图像旋转 2.3. WebGL 2D 图像伸缩 2.4. WebGL 2D 矩阵
- 3D 4.1. WebGL 3D 正交 4.1. WebGL 3D 透视 4.1. WebGL 3D 摄像机
WebGL介绍
WebGL 是一种 3D 这种绘图技术标准允许绘图标准 JavaScript 和 OpenGL ES 2.0 结合在一起,通过增加 OpenGL ES 2.0 的一个 JavaScript 绑定,WebGL 可以为 HTML5 Canvas 提供硬件 3D 加速渲染,这样 Web 借助系统显卡,开发人员可以在浏览器中更流畅地显示3D场景和模型也可以创建复杂的导航和数据视觉。
WebGL基础
WebGL基本原理
WebGL 浏览器上显示的出现 3D 图像成为可能,WebGL 本质上是基于光栅化的 API ,而不是基于 3D 的 API。WebGL 只关注两个方面,即投影矩阵的坐标和投影矩阵的颜色。使用 WebGL 程序的任务是实现投影矩阵坐标和颜色 WebGL 对象即可。上述任务可以用着色器来完成。顶点着色器可以提供投影矩阵的坐标,片段着色器可以提供投影矩阵的颜色。
无论图形尺寸如何,投影矩阵的坐标范围始终从 -1 到 1。例如:
// Get A WebGL context var canvas = document.getElementById("canvas"); var gl = canvas.getContext("experimental-webgl"); // setup a GLSL program var program = createProgramFromScripts(gl, ["2d-vertex-shader", "2d-fragment-shader"]); gl.useProgram(program); // look up where the vertex data needs to go. var positionLocation = gl.getAttribLocation(program, "a_position"); // Create a buffer and put a single clipspace rectangle in // it (2 triangles) var buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([ -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]),
gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
// draw
gl.drawArrays(gl.TRIANGLES, 0, 6);
下面是两个着色器。
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0, 1);
}
</script>
<script id="2d-fragment-shader" type="x-shader/x-fragment">
void main() {
gl_FragColor = vec4(0, 1, 0, 1); // green
}
</script>
如果想实现 3D 的效果,那么可以使用着色器来将 3D 转换为投影矩阵,这是因为 WebGL 是基于光栅的 API。对于 2D 的图像,也许会使用像素而不是投影矩阵来表述尺寸,那么这里我们就更改这里的着色器,使得我们实现的矩形可以以像素的方式来度量,下面是新的顶点着色器。
attribute vec2 a_position;
uniform vec2 u_resolution;
void main() {
// convert the rectangle from pixels to 0.0 to 1.0
vec2 zeroToOne = a_position / u_resolution;
// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace, 0, 1);
}
WebGL工作原理
WebGL 和 GPU 是如何运作的。GPU 有两个基础任务,第一个就是将点处理为投影矩阵。第二部分就是基于第一部分将相应的像素点描绘出来。当用户调用
gl.drawArrays(gl.TRIANGLE, 0, 9);
这里的 9 就意味着“处理 9 个顶点”,所以就有 9 个顶点需要被处理。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kEtRQ8hd-1631442055895)(en-resource://database/4815:1)]
上图左侧的是用户自己提供的数据。顶点着色器就是用户在 GLSL 中写的函数。处理每个顶点时,均会被调用一次。用户可以将投影矩阵的值存储在特定的变量 gl_Position
中。
GPU 会处理这些值,并将他们存储在其内部。假设用户希望绘制三角形 TRIANGLES, 那么每次绘制时,上述的第一部分就会产生三个顶点,然后 GPU 会使用他们来绘制三角形。
首先 GPU 会将三个顶点对应的像素绘制出来,然后将三角形光栅化,或者说是使用像素点绘制出来。对每一个像素点,GPU 都会调用用户定义的片段着色器来确定该像素点该涂成什么颜色。当然,用户定义的片段着色器必须在 gl_FragColor 变量中设置对应的值。
我们例子中的片段着色器中并没有存储每一个像素的信息。我们可以在其中存储更丰富的信息。我们可以为每一个值定义不同的意义从顶点着色器传递到片段着色器。
作为一个简单的例子,我们将直接计算出来的投影矩阵坐标从顶点着色器传递给片段着色器。我们将绘制一个简单的三角形。我们在上个例子的基础上更改一下。
function setGeometry(gl) {
gl.bufferData(
gl.ARRAY_BUFFER, new Float32Array([ 0, -100, 150, 125, -175, 100]), gl.STATIC_DRAW
);
}
然后,我们绘制三个顶点。
// Draw the scene.
function drawScene() {
... // Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
然后,我们可以在顶点着色器中定义变量来将数据传递给片段着色器。
varying vec4 v_color;
... void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
// Convert from clipspace to colorspace.
// Clipspace goes -1.0 to +1.0
// Colorspace goes from 0.0 to 1.0
v_color = gl_Position * 0.5 + 0.5;
}
然后,我们在片段着色器中声明相同的变量。
precision mediump float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
WebGL 将会连接顶点着色器中的变量和片段着色器中的相同名字和类型的变量。下面是可以交互的版本。
移动、缩放或旋转这个三角形。注意由于颜色是从投影矩阵计算而来,所以,颜色并不会随着三角形的移动而一直一样。他们完全是根据背景色设定的。
现在我们考虑下面的内容。我们仅仅计算三个顶点。我们的顶点着色器被调用了三次,因此,仅仅计算了三个颜色。而我们的三角形可以有好多颜色,这就是为何被称为 varying。
WebGL 使用了我们为每个顶点计算的三个值,然后将三角形光栅化。对于每一个像素,都会使用被修改过的值来调用片段着色器。
基于上述例子,我们以三个顶点开始 .[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0dZv4qpK-1631442055897)(en-resource://database/4817:1)]
我们的顶点着色器会引用矩阵来转换、旋转、缩放和转化为投影矩阵。转换、旋转和缩放的默认值是转换为200,150,旋转为 0,缩放为 1,1,所以实际上只进行转换。我们的后台缓存是 400x300。我们的顶点矩阵应用矩阵然后计算下面的三个投影矩阵顶点。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ao53q6pQ-1631442055898)(en-resource://database/4819:1)]
同样也会将这些转换到颜色空间上,然后将他们写到我们声明的多变变量 v_color。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NewWzvrC-1631442055900)(en-resource://database/4821:1)] 这三个值会写回到 v_color,然后它会被传递到片段着色器用于每一个像素进行着色。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VR0FGgng-1631442055902)(en-resource://database/4823:1)]
v_color 被修改为 v0,v1 和 v2 三个值中的一个。我们也可以在顶点着色器中存储更多的数据以便往片段着色器中传递。
所以,对于以两种颜色绘制包含两个三角色的矩形的例子。为了实现这个例子,我们需要往顶点着色器中附加更多的属性,以便传递更多的数据,这些数据会直接传递到片段着色器中。
attribute vec2 a_position;
attribute vec4 a_color;
...
varying vec4 v_color;
void main() {
...
// Copy the color from the attribute to the varying.
v_color = a_color;
}
我们现在需要使用 WebGL 颜色相关的功能。
var positionLocation = gl.getAttribLocation (program, "a_position");
var colorLocation = gl.getAttribLocation(program, "a_color");
...
// Create a buffer for the colors.
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(colorLocation);
gl.vertexAttribPointer(colorLocation, 4, gl.FLOAT, false, 0, 0);
// Set the colors.
setColors(gl);
// Fill the buffer with colors for the 2 triangles
// that make the rectangle.
function setColors(gl) {
// Pick 2 random colors.
var r1 = Math.random();
var b1 = Math.random();
var g1 = Math.random();
var r2 = Math.random();
var b2 = Math.random();
var g2 = Math.random();
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array( [ r1, b1, g1, 1, r1, b1, g1, 1, r1, b1, g1, 1, r2, b2, g2, 1, r2, b2, g2, 1, r2, b2, g2, 1]),
gl.STATIC_DRAW);
}
下面是结果。注意,在上面的例子中,有两个苦点颜色的三角形。我们仍将要传递的值存储在多变变量中,所以,该变量会相关三角形区域内改变。我们只是对于每个三角形的三个顶点使用了相同的颜色。如果我们使用了不同的颜色,我们可以看到整个渲染过程。
// Fill the buffer with colors for the 2 triangles
// that make the rectangle.
function setColors(gl) {
// Make every vertex a different color.
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array( [
Math.random(),
Math.random(),
Math.random(), 1,
Math.random(),
Math.random(),
Math.random(), 1,
Math.random(),
Math.random(),
Math.random(), 1,
Math.random(),
Math.random(),
Math.random(), 1,
Math.random(),
Math.random(),
Math.random(), 1,
Math.random(),
Math.random(),
Math.random(), 1]), gl.STATIC_DRAW);
}
从顶点着色器往片段着色器可以传递更多更丰富的数据。
缓存和属性指令
缓存是获取顶点和顶点相关数据到 GPU 中的方法。gl.createBuffer
用于创建缓存。 gl.bindBuffer
方法用于将缓存激活来处于准备工作的状态。 gl.bufferData
方法可以将数据拷贝到缓存中。一旦,数据到了缓存中,就需要告诉 WebGL 如何从里面除去数据,并将它提供给顶点着色器以给相应的属性赋值。 为了实现这个功能,首先我们需要求出 WebGL 提供一个属性存储位置。
// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");
var colorLocation = gl.getAttribLocation(program, "a_color");
我们可以触发两个指令
gl.enableVertexAttribArray(location);
这个指令会告诉 WebGL 我们希望将缓存中的数据赋值给一个变量。
gl.vertexAttribPointer(
location,
numComponents,
typeOfData,
normalizeFlag,
strideToNextPieceOfData,
offsetIntoBuffer,
);
这个指令会告诉 WebGL 会从缓存中获取数据,这个缓存会与 gl.bindBuffer 绑定。每个顶点可以有 1 到 4 个部件,数据的类型可以是 BYTE,FLOAT,INT,UNSIGNED_SHORT 等。跳跃意味着从数据的这片到那片会跨越多少个字节。跨越多远会以偏移量的方式存储在缓存中。部件的数目一般会是 1 到 4。如果每个数据类型仅使用一个缓存,那么跨越和偏移量都会是 0。跨越为 0 意味着“使用一个跨越连匹配类型和尺寸”。偏移量为 0 意味着是在缓存的开头部分。将这个值赋值为除 O 之外其他的值会实现更为灵活的功能。虽然在性能方面它有些优势,但是并不值得搞得很复杂,除非程序员希望将 WebGL 运用到极致。
vertexAttribPointer 的规范化标志 normalizeFlag
规范化标志应用于非浮点指针类型。如果该值置为 false, 就意味着该值就会被翻译为类型。BYTE 的标示范围是-128 到 127。UNSIGNED_BYTE 范围是 0 到 255,SHORT 是从-32768 到 32767。
如果将规范化标志置为 true,那么BYTE的标示范围将为变为-1.0 到 +1.0,UNSIGNED_BYTE 将会变为 0.0 到 +1.0,规范化后的 SHORT 将会变为 -1.0 到 +1.0,它将有比 BYTE 更高的精确度.标准化数据最通用的地方就是用于颜色。
大部分时候,颜色范围为 0.0 到 1.0 红色、绿色和蓝色需要个浮点型的值来表示,alpha 需要 16 字节来表示顶点的每个颜色。如果要实现更为复杂的图形,可以增加更多的字节。
相反的,程序可以将颜色转为 UNSIGNED_BYTE 类型,这个类型使用 0 表示 0.0,使用 255 表示 1.0。那么仅需要 4 个字节来表示顶点的每个颜色,这将节省 75% 的存储空间。我们按照下面的方式来更改我们的代码。当我们告诉 WebGL 如何获取颜色。
gl.vertexAttribPointer(colorLocation, 4, gl.UNSIGNED_BYTE, true, 0, 0);
使用下面的代码来填充我们的缓冲区
function setColors(gl) {
// Pick 2 random colors.
var r1 = Math.random() * 256;
// 0 to 255.99999
var b1 = Math.random() * 256;
// these values
var g1 = Math.random() * 256;
// will be truncated
var r2 = Math.random() * 256;
// when stored in the
var b2 = Math.random() * 256;
// Uint8Array
var g2 = Math.random() * 256;
gl.bufferData( gl.ARRAY_BUFFER, new Uint8Array(
[ r1, b1, g1, 255, r1, b1, g1, 255, r1, b1, g1, 255, r2, b2, g2, 255, r2, b2, g2, 255, r2, b2, g2, 255]), gl.STATIC_DRAW);
}
WebGL着色器和GLSL
WebGL每次绘制,都需要两个着色器,分别是顶点着色器和片段着色器。每个着色器都是一个函数。顶点着色器和片段着色器都是链接在程序中的。一个典型的 WebGL 程序都会包含很多这样的着色器程序。
顶点着色器
顶点着色器的任务就是产生投影矩阵的坐标。其形式如下:
void main() {
gl_Position = doMathToMakeClipspaceCoordinates
}
每一个顶点都会调用你的着色器。每次调用程序都需要设置特定的全局变量 gl_Position 来表示投影矩阵的坐标。顶点着色器需要数据,它以下面三种方式来获取这些数据。
- 属性(从缓冲区中获取数据)
- 一致变量(每次绘画调用时都保持一致的值)
- 纹理(从像素中得到的数据)
最常用的方式就是使用缓存区和属性。程序可以以下面的方式创建缓存区。
var buf = gl.createBuffer();
在这些缓存中存储数据。
gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, someData, gl.STATIC_DRAW);
于是,给定一个着色器程序,程序可以去查找属性的位置。
var positionLoc = gl.getAttribLocation(someShaderProgram, "a_position");
下面告诉 WebGL 如何从缓存区中获取数据并存储到属性中。
// turn on getting data out of a buffer for this attribute
gl.enableVertexAttribArray(positionLoc);
var numComponents = 3; // (x, y, z)
var type = gl.FLOAT;
var normalize = false; // leave the values as they are
var offset = 0; // start at the beginning of the buffer
var stride = 0; // how many bytes to move to the next vertex // 0 = use the correct stride for type and numComponents
gl.vertexAttribPointer(positionLoc, numComponents, type, false, stride, offset);
如果我们可以将投影矩阵放入我们的缓存区中,它就会开始运作。属性可以使用 float,vec2,vec3,vec4,mat2,mat3 和 mat4 作为类型。
对于顶点着色器,一致性变量就是在绘画每次调用时,在着色器中一直保持不变的值。下面是一个往顶点中添加偏移量着色器的例子。
attribute vec4 a_position;
uniform vec4 u_offset;
void main() {
gl_Position = a_position + u_offset;
}
下面,我们需要对每一个顶点都偏移一定量。首先,我们需要先找到一致变量的位置。
var offsetLoc = gl.getUniformLocation(someProgram, "u_offset");
然后,我们在绘制前需要设置一致性变量
gl.uniform4fv(offsetLoc, [1, 0, 0, 0]); // offset it to the right half the screen
一致性变量可以有很多种类型。对每一种类型都可以调用相应的函数来设置。
gl.uniform1f (floatUniformLoc, v); // for float
gl.uniform1fv(floatUniformLoc, [v]); // for float or float array
gl.uniform2f (vec2UniformLoc, v0, v1);// for vec2
gl.uniform2fv(vec2UniformLoc, [v0, v1]); // for vec2 or vec2 array
gl.uniform3f (vec3UniformLoc, v0, v1, v2);// for vec3
gl.uniform3fv(vec3UniformLoc, [v0, v1, v2]); // for vec3 or vec3 array
gl.uniform4f (vec4UniformLoc, v0, v1, v2, v4);// for vec4
gl.uniform4fv(vec4UniformLoc, [v0, v1, v2, v4]); // for vec4 or vec4 array
gl.uniformMatrix2fv(mat2UniformLoc, false, [ 4x element array ]) // for mat2 or mat2 array
gl.uniformMatrix3fv(mat3UniformLoc, false, [ 9x element array ]) // for mat3 or mat3 array
gl.uniformMatrix4fv(mat4UniformLoc, false, [ 17x element array ]) // for mat4 or mat4 array
gl.uniform1i (intUniformLoc, v); // for int
gl.uniform1iv(intUniformLoc, [v]); // for int or int array
gl.uniform2i (ivec2UniformLoc, v0, v1);// for ivec2
gl.uniform2iv(ivec2UniformLoc, [v0, v1]); // for ivec2 or ivec2 array
gl.uniform3i (ivec3UniformLoc, v0, v1, v2);// for ivec3
gl.uniform3iv(ivec3UniformLoc, [v0, v1, v2]); // for ivec3 or ivec3 array
gl.uniform4i (ivec4UniformLoc, v0, v1, v2, v4);// for ivec4
gl.uniform4iv(ivec4UniformLoc, [v0, v1, v2, v4]); // for ivec4 or ivec4 array
gl.uniform1i (sampler2DUniformLoc, v); // for sampler2D (textures)
gl.uniform1iv(sampler2DUniformLoc, [v]); // for sampler2D or sampler2D array
gl.uniform1i (samplerCubeUniformLoc, v); // for samplerCube (textures)
gl.uniform1iv(samplerCubeUniformLoc, [v]); // for samplerCube or samplerCube array
一般类型都有 bool,bvec2,bvec3 和 bvec4。他们相应的调用函数形式为 gl.uniform?f?
或 gl.uniform?i?
。可以一次性设置数组中的所有一致性变量。比如:
uniform vec2 u_someVec2[3]; gl.getUniformLocation(someProgram, "u_someVec2"); gl.uniform2fv(someVec2Loc, [1, 2, 3,