目录
- 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, 4, 5, 6]);
但是,如果程序希望单独设置数组中的成员,那么必须单个的查询每个成员的位置。
var someVec2Element0Loc = gl.getUniformLocation(someProgram, "u_someVec2[0]"); var someVec2Element1Loc = gl.getUniformLocation(someProgram, "u_someVec2[1]"); var someVec2Element2Loc = gl.getUniformLocation(someProgram, "u_someVec2[2]");
// at render time
gl.uniform2fv(someVec2Element0Loc, [1, 2]); // set element 0
gl.uniform2fv(someVec2Element1Loc, [3, 4]); // set element 1
gl.uniform2fv(someVec2Element2Loc, [5, 6]); // set element 2
类似的,可以创建一个结构体
struct SomeStruct {
bool active; vec2 someVec2;
};
uniform SomeStruct u_someThing;
程序可以单独的查询每一个成员。
var someThingActiveLoc = gl.getUniformLocation(someProgram, "u_someThing.active"); var someThingSomeVec2Loc = gl.getUniformLocation(someProgram, "u_someThing.someVec2");
片段着色器
片段着色器的任务就是为当前被栅格化的像素提供颜色。它通常以下面的方式呈现出来。
precision mediump float;
void main() {
gl_FragColor = doMathToMakeAColor;
}
片段着色器对每一个像素都会调用一次。每次调用都会设置全局变量 gl_FragColor 来设置一些颜色。片段着色器需要存储获取数据,通常有下面这三种方式。
- 一致变量(每次绘制像素点时都会调用且一直保持一致)
- 纹理(从像素中获取数据)
- 多变变量(从定点着色器中传递出来且被栅格化的值)
片段着色器中的纹理
我们可以从纹理中获取值来创建 sampler2D 一致变量, 然后使用 GLSL 函数 texture2D 来从中获取值。
precision mediump float;
uniform sampler2D u_texture;
void main() {
vec2 texcoord = vec2(0.5, 0.5)
gl_FragColor = texture2D(u_texture, texcoord);
}
从纹理中提取的值是要依据很多设置的。最基本的,我们需要创建并在文理中存储值。比如:
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, 0, 255, 0, 255]);
gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
然后,在着色器程序中查询一致变量的位置。
var someSamplerLoc = gl.getUniformLocation(someProgram, "u_texture");
WebGL 需要将它绑定到纹理单元中。
var unit = 5;
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(gl.TEXTURE_2D, tex);
然后告知着色器哪个单元会被绑定到纹理中
gl.uniform1i(someSamplerLoc, unit);
多变变量
多变变量是从顶点着色器往片段着色器中传递的值,这些在WebGL 如何工作的已经涵盖了。为了使用多变变量,需要在顶点着色器和片段着色器中均设置匹配的多变变量。我们在顶点着色器中设置多变变量。当 WebGL 绘制像素时,它会栅格化该值,然后传递到片段着色器中相对应的片段着色器。
顶点着色器
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;
}
片段着色器
precision mediump float;
varying vec4 v_positionWithOffset;
void main() {
// convert from clipsapce (-1 <-> +1) to color space (0 -> 1).
vec4 color = v_positionWithOffset * 0.5 + 0.5
gl_FragColor = color;
}
GLSL
GLSL是图像库着色器语言的简称。语言着色器就是被写在这里。它具有一些 JavaScript 中不存在的独特的特性。 它用于实现一些逻辑来渲染图像。比如,它可以创建类似于 vec2,vec3 和 vec4 分别表示2、3、4个值。类似的,mat2,mat3 和 mat4 来表示 2x2,3x3,4x4 的矩阵。可以实现 vec 来乘以一个标量。
vec4 a = vec4(1, 2, 3, 4); vec4 b = a * 2.0;
实现矩阵的乘法和矩阵的向量乘法:
mat4 a = ???
mat4 b = ???
mat4 c = a * b;
vec4 v = ???
vec4 y = c * v;
也可以选择 vec 的部分,比如,vec4
vec4 v;
- v.x 等价于 v.s,v.r,v[0]
- v.y 等价于 v.t,v.g,v[1]
- v.z 等价于 v.p,v.b,v[2]
- v.w 等价于 v.q,v.a,v[3]
可以调整 vec 组件意味着可以交换或重复组件。
v.yyyy
这等价于
vec4(v.y, v.y, v.y, v.y)
类似的
v.bgra
等价于
vec4(v.b, v.g, v.r, v.a)
当创建一个 vec 或 一个 mat时,程序可以一次操作多个部分:
vec4(v.rgb, 1)
这等价于
vec4(v.r, v.g, v.b, 1)
你可能意识到 GLSL 是一种很严格类型的语言
float f = 1;
正确的方式如下:
float f = 1.0; // use float
float f = float(1) // cast the integer to a float
上面例子的 vec4(v.rgb, 1) 并不会对 1 进行混淆,这是因为 vec4 是类似于 float(1)。GLSL 是内置函数的分支.可以同时操作多个组件。比如,
T sin(T angle)
这意味着 T 可以是 float,vec2,vec3 或 vec4。如果用户在 vec4 中传递数据。也就是说 v 是 vec4,
vec4 s = sin(v);
折等价于
vec4 s = vec4(sin(v.x), sin(v.y), sin(v.z), sin(v.w));
有时候,一个参数是 float,另一个是 T。这意味着 float 将应用到所有的部件。比如,如果 v1,v2 是 vec4,f 是 flat,然后
vec4 m = mix(v1, v2, f);
这等价于
vec4 m = vec4(
mix(v1.x, v2.x, f),
mix(v1.y, v2.y, f),
mix(v1.z, v2.z, f),
mix(v1.w, v2.w, f),
);
混合起来看
WebGL 是关于创建各种着色器的,将数据存储在这些着色器上,然后调用 gl.drawArrays 或 gl.drawElements 来使 WebGL 处理顶点通过为每个顶点调用当前的顶点着色器,然后为每一个像素调用当前的片段着色器。
实际上,着色器的创建需要写几行代码。因为,这些代码在大部分 WebGL 程序中的是一样的,又因为一旦写了,可以几乎可以忽略他们如何编译 GLSL 着色器和链接成一个着色器程序。
WebGL图像处理
WebGL 会需要操作投影矩阵的坐标,WebGL 读取纹理时需要获取纹理坐标。纹理坐标范围是从 0.0 到 1.0。
因为我们仅需要绘制由两个三角形组成的矩形,我们需要告诉 WebGL 在矩阵中纹理对应的那个点。我们可以使用特殊的被称为多变变量,会将这些信息从顶点着色器传递到片段着色器。
WebGL 将会插入这些值,这些值会在顶点着色器中,当对每个像素绘制时均会调用片段着色器。我们需要在纹理坐标传递过程中添加更多的信息,然后将他们传递到片段着色器中。
attribute vec2 a_texCoord;
...
varying vec2 v_texCoord;
void main() {
...
// pass the texCoord to the fragment shader
// The GPU will interpolate this value between points
v_texCoord = a_texCoord;
}
片段着色器来查找颜色纹理
precision mediump float;
// our texture
uniform sampler2D u_image;
// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;
void main() {
// Look up a color from the texture.
gl_FragColor = texture2D(u_image, v_texCoord);
}
加载一个图片,然后创建一个问题,将该图片传递到纹理里面
function main() {
var image = new Image();
image.src = "http://someimage/on/our/server";
image.onload = function() {
render(image);
}
}
function render(image) {
...
// all the code we had before.
...
// look up where the texture coordinates need to go.
var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");
// provide texture coordinates for the rectangle.
var texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]),
gl.STATIC_DRAW);
gl.enableVertexAttribArray(texCoordLocation);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
// Create a texture.
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S,
gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D,
gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// Upload the image into the texture.
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
...
}
如下是 WebGL 渲染出来的图像
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gJ8YRx1n-1631442055903)(en-resource://database/4827:1)]
下面对这个图片进行一些操作,来交换图片中的红色和蓝色
...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...
现在红色和蓝色已经被交换了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HR5D0XEE-1631442055904)(en-resource://database/4829:1)]
自从 WebGL 引用纹理的纹理坐标从 0.0 到 1.0 。然后,我们可以计算移动的多少个像素 onePixel = 1.0 / textureSize
。这里有个片段着色器来平均纹理中每个像素的左侧和右侧的像素。
precision mediump float;
// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;
// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;
void main() {
// compute 1 pixel in texture coordinates.
vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
// average the left, middle, and right pixels.
gl_FragColor = (
texture2D(u_image, v_texCoord) +
texture2D(u_image, v_texCoord + vec2(onePixel.x, 0.0)) +
texture2D(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
}
通过 JavaScript 传递出纹理的大小
...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// set the size of the image
gl.uniform2f(textureSizeLocation, image.width, image.height); ...
比较上述两个图片
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B10V21RS-1631442055905)(en-resource://database/4831:1)]
使用 3x3 的内核。卷积内核就是一个 3x3 的矩阵,矩阵中的每个条目代表有多少像素渲染。然后,我们将这个结果除以内核的权重或 1.0。
在着色器中做这样工作,这里是一个新的片段着色器
precision mediump float;
// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;
uniform float u_kernel[9];
uniform float u_kernelWeight;
// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;
void main() {
vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
vec4 colorSum =
texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +
texture2D(u_image, v_texCoord + onePixel * vec2(-1, 0)) * u_kernel[3] +
texture2D(u_image, v_texCoord + onePixel * vec2( 0, 0)) * u_kernel[4] +
texture2D(u_image, v_texCoord + onePixel * vec2( 1, 0)) * u_kernel[5] +
texture2D(u_image, v_texCoord + onePixel * vec2(-1, 1)) * u_kernel[6] +
texture2D(u_image, v_texCoord + onePixel * vec2( 0, 1)) * u_kernel[7] +
texture2D(u_image, v_texCoord + onePixel * vec2( 1, 1)) * u_kernel[8] ;
// Divide the sum by the weight but just use rgb
// we'll set alpha to 1.0
gl_FragColor = vec4((colorSum / u_kernelWeight).rgb, 1.0); }
提供一个卷积内核和它的权重:
function computeKernelWeight(kernel) {
var weight = kernel.reduce(function(prev, curr) {
return prev + curr;
});
return weight <= 0 ? 1 : weight;
}
...
var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
var kernelWeightLocation = gl.getUniformLocation(program, "u_kernelWeight");
...
var edgeDetectKernel = [ -1, -1, -1, -1, 8, -1, -1, -1, -1 ];
gl.uniform1fv(kernelLocation, edgeDetectKernel);
gl.uniform1f(kernelWeightLocation, computeKernelWeight(edgeDetectKernel));
...
实时的渲染
使用两种或更多的纹理和渲染效果来交替渲染,每次应用一个效果,然后反复应用.
Original Image -> [Blur]-> Texture 1
Texture 1 -> [Sharpen] -> Texture 2
Texture 2 -> [Edge Detect] -> Texture 1
Texture 1 -> [Blur]-> Texture 2
Texture 2 -> [Normal] -> Canvas
创建帧缓存区。在 WebGL 和 OpenGL 中,帧缓存区实际上是一个非常不正式的名称。 WebGL/OpenGL 中的帧缓存实际上仅仅是一些状态的集合,而不是真正的缓存。但是,每当一种纹理到达帧缓存,我们就会渲染出这种纹理。
把旧的纹理创建代码写成一个函数:
function createAndSetupTexture(gl) {
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set up texture so we can render any size image and so we are
// working with pixels.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
return texture;
}
// Create a texture and put the image in it.
var originalImageTexture = createAndSetupTexture(gl);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
用这两个函数来生成两种问题,并且附在两个帧缓存中:
// create 2 textures and attach them to framebuffers.
var textures = [];
var framebuffers = [];
for (var ii = 0; ii < 2; ++ii) {
var texture = createAndSetupTexture(gl);
textures.push(texture);
// make the texture the same size as the image
gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
// Create a framebuffer
var fbo = gl.createFramebuffer();
framebuffers.push(fbo);
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
// Attach a texture to it.
gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
}
生成一些核的集合,然后存储到列表里来应用:
var kernels = {
normal: [ 0, 0, 0, 0, 1, 0, 0, 0, 0 ],
gaussianBlur: [ 0.045, 0.122, 0.045, 0.122, 0.332, 0.122, 0.045, 0.122, 0.045 ],
unsharpen: [ -1, -1, -1, -1, 9, -1, -1, -1, -1 ],
emboss: [ -2, -1, 0, -1, 1, 1, 0, 1, 2 ]
};
// List of effects to apply.
var effectsToApply = [ "gaussianBlur", "emboss", "gaussianBlur", "unsharpen" ];
最后,应用每一个,然后交替渲染:
// start with the original image
gl.bindTexture(gl.TEXTURE_2D, originalImageTexture);
// don't y flip images while drawing to the textures
gl.uniform1f(flipYLocation, 1);
// loop through each effect we want to apply.
for (var ii = 0; ii < effectsToApply.length; ++ii) {
// Setup to draw into one of the framebuffers.
setFramebuffer(framebuffers[ii % 2], image.width, image.height);
drawWithKernel(effectsToApply[ii]);
// for the next draw, use the texture we just rendered to.
gl.bindTexture(gl.TEXTURE_2D, textures[ii % 2]); }
// finally draw the result to the canvas.
gl.uniform1f(flipYLocation, -1);
// need to y flip for canvas
setFramebuffer(null, canvas.width, canvas.height);
drawWithKernel("normal");
function setFramebuffer(fbo, width, height) {
// make this the framebuffer we are rendering to.
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
// Tell the shader the resolution of the framebuffer.
gl.uniform2f(resolutionLocation, width, height);
// Tell webgl the viewport setting needed for framebuffer.
gl.viewport(0, 0, width, height);
}
function drawWithKernel(name) {
// set the kernel
gl.uniform1fv(kernelLocation, kernels[name]);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
以空值调用 gl.bindFramebuffer 即可告诉 WebGL 程序希望渲染到画板而不是帧缓存中的纹理.WebGL 不得不将投影矩阵转换为像素。这是基于 gl.viewport 的设置。当我们初始化 WebGL 的时候, gl.viewport 的设置默认为画板的尺寸。
因为,我们会将帧缓存渲染为不同的尺寸,所以画板需要设置合适的视图。最后,在原始例子中,当需要渲染的时候,我们会翻转 Y 坐标。这是因为 WebGL 会以 0 来显示面板。 0 表示是左侧底部的坐标,这不同于 2D 图像的顶部左侧的坐标。当渲染为帧缓存时就不需要了。
这是因为帧缓存并不会显示出来。其部分是顶部还是底部是无关紧要的。所有重要的就是像素 0,0 在帧缓存里就对应着 0。为了解决这一问题,我们可以通过是否在着色器中添加更多输入信息的方法来设置是否快读交替。
...
uniform float u_flipY;
...
void main() {
...
gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1);
...
}
当我们渲染的时候,就可以设置它
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// don't flip
gl.uniform1f(flipYLocation, 1);
...
// flip
gl.uniform1f(flipYLocation, -1);
如果想做完整的图像处理你可能需要许多 GLSL 程序。一个程序实现色相、饱和度和亮度调整。另一个实现亮度和对比度。一个实现反相,另一个用于调整水平。可能需要更改代码以更新 GLSL 程序和更新特定程序的参数。
WebGL 2D 转换、旋转、伸缩、矩阵
WebGL 2D图像转换
基于Translation 2D图像转换基本示例:
// First lets make some variables
// to hold the translation of the rectangle
var translation = [0, 0];
// then let's make a function to
// re-draw everything. We can call this
// function after we update the translation.
// Draw a the scene.
function drawScene() {
// Clear the canvas.
gl.clear(gl.COLOR_BUFFER_BIT);
// Setup a rectangle
setRectangle(gl, translation[0], translation[1], width, height);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
可以通过滑动按钮来修改 translation[0] 和 translation[1] 的值,而且在这个两个值发生修改时调用 drawScene 函数对界面进行更新。拖动滑动条对矩阵进行移动。
使用的改变 setRectangle 值的代码:
function setGeometry(gl, x, y) {
var width = 100;
var height = 150;
var thickness = 30;
gl.bufferData( gl.ARRAY_BUFFER,
new Float32Array([
// left column
x, y,
x + thickness, y,
x, y + height,
x, y + height,
x + thickness, y,
x + thickness, y + height,
// top rung
x + thickness, y,
x + width, y,
x + thickness, y + thickness,
x + thickness, y + thickness,
x + width, y,
x + width, y + thickness,
// middle rung
x + thickness, y + thickness * 2,
x + width * 2 / 3, y + thickness * 2,
x + thickness, y + thickness * 3,
x + thickness, y + thickness * 3,
x + width * 2 / 3, y + thickness * 2,
x + width * 2 / 3, y + thickness * 3]),
gl.STATIC_DRAW);
}
渲染器部分:
attribute vec2 a_position;
uniform vec2 u_resolution;
uniform vec2 u_translation;
void main() {
// Add in the translation.
vec2 position = a_position + u_translation;
// convert the rectangle from pixels to 0.0 to 1.0
vec2 zeroToOne = position / u_resolution; ...
设置几何图形一次实现效果:
function setGeometry(gl) {
gl.bufferData( gl.ARRAY_BUFFER,
new Float32Array([
// left column
0, 0, 30, 0, 0, 150, 0, 150, 30, 0, 30, 150,
// top rung
30, 0, 100, 0, 30, 30, 30, 30, 100, 0, 100, 30,
// middle rung
30, 60, 67, 60, 30, 90, 30, 90, 67, 60, 67, 90]),
gl.STATIC_DRAW);
}
更新下u_translation 变量的值:
...
var translationLocation = gl.getUniformLocation( program, "u_translation");
...
// Set Geometry.
setGeometry(gl);
..
// Draw scene.
function drawScene() {
// Clear the canvas.
gl.clear(gl.COLOR_BUFFER_BIT);
// Set the translation.
gl.uniform2fv(translationLocation, translation);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 18);
}
WebGL 2D图像旋转
在单位圆上的最高点处,Y 为 1 和 X 为 0。在最右的位置时 X 为 1 和 Y 为 0。
从单位圆上得到任何点的 X 和 Y 值,接着将他们乘以上一节示例中的几何图形。如下是更新渲染器:
attribute vec2 a_position;
uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;
void main() {
// Rotate the position
vec2 rotatedPosition = vec2( a_position.x * u_rotation.y + a_position.y * u_rotation.x, a_position.y * u_rotation.y - a_position.x * u_rotation.x);
// Add in the translation.
vec2 position = rotatedPosition + u_translation;
传参:
var rotationLocation = gl.getUniformLocation(program, "u_rotation");
... var rotation = [0, 1];
..
// Draw the scene.
function drawScene() {
// Clear the canvas.
gl.clear(gl.COLOR_BUFFER_BIT);
// Set the translation.
gl.uniform2fv(translationLocation, translation);
// Set the rotation.
gl.uniform2fv(rotationLocation, rotation);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 18);
}
首先,让我们看下数学公式:
rotatedX = a_position.x * u_rotation.y + a_position.y * u_rotation.x;
rotatedY = a_position.y * u_rotation.y - a_position.x * u_rotation.x;
假设你有一个矩形,并且你想旋转它。在你把它旋转到右上角 (3.0,9.0) 这个位置之前。我们先在单位圆中选择一个从 12 点钟的位置顺时针偏移 30 度的点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tR7I8RWG-1631442055906)(en-resource://database/4833:1)]
在圆上那个位置的点的坐标为 0.50 和 0.87:
3.0 * 0.87 + 9.0 * 0.50 = 7.1
9.0 * 0.87 - 3.0 * 0.50 = 6.3
那刚刚好是我们需要的位置:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yz6uPW0n-1631442055907)(en-resource://database/4835:1)]
旋转 60 度和上面的操作一样:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SB8E70Tm-1631442055908)(en-resource://database/4837:1)]
圆上面的位置的坐标是 0.87 和 0.50:
3.0 * 0.50 + 9.0 * 0.87 = 9.3
9.0 * 0.50 - 3.0 * 0.87 = 1.9
当我们顺时针向右旋转那个点时,X 的值变得更大而 Y 的值在变小。如果接着旋转超过 90 度,X 的值将再次变小而 Y 的值将变得更大。这个形式就能够达到旋转的目的。
圆环上的那些点还有另外一个名称。他们被称作为 sine 和 cosine。因此,对任意给定的角度,我们就只需查询它所对应的 sine 和 cosine 值:
function printSineAndCosineForAnyAngle(angleInDegrees) {
var angleInRadians = angleInDegrees * Math.PI / 180;
var s = Math.sin(angleInRadians);
var c = Math.cos(angleInRadians); console.log("s = " + s + " c = " + c);
}
如果你把上面的代码复制粘贴到 JavaScript 控制台中,接着输入printSineAndCosineForAnyAngle(30),接着你会看到输出 s = 0.49 c = 0.87(注意:这个数字是近似值。)
如果把上面的代码整合在一起的话,你就可以将你的几何体按照你想要的任何角度进行旋转。仅仅只需要将你需要旋转的角度值传给 sine 和 cosine 就可以了。
var angleInRadians = angleInDegrees * Math.PI / 180;
rotation[0] = Math.sin(angleInRadians);
rotation[1] = Math.cos(angleInRadians);
WebGL 2D图像伸缩
伸缩比例控制:
var scaleLocation = gl.getUniformLocation(program, "u_scale");
var scale = [1, 1];
// Draw the scene.
function drawScene() {
// Clear the canvas.
gl.clear(gl.COLOR_BUFFER_BIT);
// Set the translation.
gl.uniform2fv(translationLocation, translation);
// Set the rotation.
gl.uniform2fv(rotationLocation, rotation);
// Set the scale.
gl.uniform2fv(scaleLocation, scale);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 18);
}
WebGL 2D矩阵
伸缩变换图形。平移,旋转和伸缩都是一种变化类型。每一种变化都需要改变渲染器,而且他们依赖于操作的顺序。在前面的例子中我们进行了伸缩,旋转和平移操作。
如果他们执行操作的顺序改变将会得到不同的结果。例如 XY 伸缩变换为 2,1,旋转 30%,接着平移变换 100,0。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xw90vdCv-1631442055909)(en-resource://database/4839:1)]
如下是平移 100,0,旋转 30%,接着伸缩变换 2,1。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i4UDf5hA-1631442055910)(en-resource://database/4841:1)]
结果是完全不同的。更糟糕的是,如果我们需要得到的是第二个示例的效果,就必须编写一个不同的渲染器,按照我们想要的执行顺序进行平移,旋转和伸缩变换。然而,有些比我聪明的人利用数学中的矩阵能够解决上面这个问题。对于 2d 图形,使用一个 3X3 的矩阵。3X3 的矩阵类似了 9 宫格。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DUKvww2P-1631442055910)(en-resource://database/4843:1)]
数学中的操作是与列相乘然后把结果加在一起。一个位置有两个值,用 x 和 y 表示。但是为了实现这个需要三个值,因为我们对第三个值设为 1。在上面的例子中就变成了:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TkiWfl1z-1631442055911)(en-resource://database/4845:1)]
对于上面的处理你也许会想“这样处理的原因在哪里”。假设要执行平移变换。我们将想要执行的平移的总量为 tx 和 ty。构造如下的矩阵:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C1Q1xLL4-1631442055912)(en-resource://database/4847:1)]
接着进行计算:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y7ZRFIqk-1631442055912)(en-resource://database/4849:1)]
如果你还记得代数学,就可以那些乘积结果为零的位置。乘以 1 的效果相当于什么都没做,那么将计算简化看看发生了什么:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhSgoSb7-1631442055913)(en-resource://database/4851:1)]
或者更简洁的方式:
newX = x + tx;
newY = y + ty;
extra 变量我们并不用在意。这个处理和我们在平移中编写的代码惊奇的相似。同样地,让我们看看旋转。正如在旋转那篇中指出当我们想要进行旋转的时候,我们只需要角度的 sine 和 cosine 值。
s = Math.sin(angleToRotateInRadians);
c = Math.cos(angleToRotateInRadians);
构造如下的矩阵:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4QHoUyyW-1631442055914)(en-resource://database/4853:1)]
执行上面的矩形操作:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9WgqwoFQ-1631442055915)(en-resource://database/4855:1)]
将得到 0 和 1 结果部分用黑色块表示了。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qfjy0J6V-1631442055916)(en-resource://database/4857:1)]
同样可以简化计算:
newX = x * c + y * s;
newY = x * -s + y * c;
上面处理的结果刚好和旋转例子效果一样。最后是伸缩变换。称两个伸缩变换因子为 sx 和 sy。构造如下的矩阵:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9BDdffxr-1631442055917)(en-resource://database/4859:1)]
进行矩阵操作会得到如下:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wedyykTX-1631442055918)(en-resource://database/4861:1)]
实际需要计算:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pySUkfn6-1631442055918)(en-resource://database/4863:1)]
简化为:
newX = x * sx;
newY = y * sy;
和我们以前讲解的伸缩示例是一样的。到了这里,我坚信你仍然在思考,这样处理之后了?有什么意义。看起来好象它只是做了和我们以前一样的事。接下来就是魔幻的地方。已经被证明了我们可以将多个矩阵乘在一起,接着一次执行完所有的变换。
假设有函数 matrixMultiply
,它带两个矩阵做参数,将他们俩相乘,返回乘积结果。为了让上面的做法更清楚,于是编写如下的函数构建一个用来平移,旋转和伸缩的矩阵:
function makeTranslation(tx, ty) {
return [ 1, 0, 0, 0, 1, 0, tx, ty, 1 ];
}
function makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [ c,-s, 0, s, c, 0, 0, 0, 1 ];
}
function makeScale(sx, sy) {
return [ sx, 0, 0, 0, sy, 0, 0, 0, 1 ];
}
修改渲染器:
attribute vec2 a_position;
uniform vec2 u_resolution;
uniform mat3 u_matrix; void main() {
// Multiply the position by the matrix.
vec2 position = (u_matrix * vec3(a_position, 1)).xy;
使用渲染器:
function drawScene() {
// Clear the canvas.
gl.clear(gl.COLOR_BUFFER_BIT);
// Compute the matrices
var translationMatrix = makeTranslation(translation[0], translation[1]);
var rotationMatrix = makeRotation(angleInRadians);
var scaleMatrix = makeScale(scale[0], scale[1]);
// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
// Set the matrix.
gl.uniformMatrix3fv(matrixLocation, false, matrix);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 18);
}
进一步简化渲染器。如下是完整的顶点渲染器。
attribute vec2 a_position;
uniform mat3 u_matrix; void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); }
// Draw the scene.
function drawScene() {
...
// Compute the matrices
var projectionMatrix = make2DProjection(
canvas.clientWidth, canvas.clientHeight);
...
// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, projectionMatrix);
...
}
WebGL 3D
WebGL 正交3D
改变顶点着色来处理三维:
attribute vec4 a_position;
uniform mat4 u_matrix; void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
}
渲染三维数据:
function makeTranslation(tx, ty, tz) {
return [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, tx, ty, tz, 1 ];
}
function makeXRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [ 1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1 ];
};
function makeYRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [ c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1 ];
};
function makeZRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [ c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, ];
}
function makeScale(sx, sy, sz) {
return [ sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, 0, 0, 0, 1, ];
}
有3个旋转函数。在二维中只需要一个旋转函数,因为只需要绕 Z 轴旋转。现在虽然做三维也希望能够绕 X 轴和 Y 轴旋转。可以从中看出,它们都非常相似。如果让它们工作,会看到它们像以前一样简化:
Z 旋转newX = x * c + y * s;
WebGL 3D 透视
什么是透视?它基本上是一种事物越远显得更小的特征。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0cTsVZNf-1631442055919)(en-resource://database/4881:1)]
看上面的例子,我们看到越远的东西被画得越小。鉴于我们目前样品的一个让较远的物体显得更小简单的方法就是将 clipspace X 和 Y 除以 Z。可以这样想:如果你有一条从(10,15)到 (20,15) 的线段,10个单位长。在我们目前的样本中,它将绘制 10 像素长。但是如果我们除以 Z,例如例子中如果是 Z 是 1
10 / 1 = 10
20 / 1 = 20
abs(10-20) = 10
这将是 10 像素,如果 Z 是 2,则有
10 / 2 = 5
20 / 2 = 10
abs(5 - 10) = 5
5 像素长。如果 Z = 3,则有
10 / 3 = 3.333
20 / 3 = 6.666
abs(3.333 - 6.666) = 3.333
你可以看到,随着 Z 的增加,随着它变得越来越远,我们最终会把它画得更小。如果我们在 clipspace 中除,我们可能得到更好的结果,因为 Z 将是一个较小的数字(-1 到 +1)。如果在除之前我们加一个 fudgeFactor 乘以 Z,对于一个给定的距离我们可以调整事物多小。让我们尝试一下。首先让我们在乘以我们的 “fudgefactor” 后改变顶点着色器除以 Z 。
...
uniform float u_fudgeFactor;
...
void main() {
// Multiply the position by the matrix.
vec4 position = u_matrix * a_position;
// Adjust the z to divide by
float zToDivideBy = 1.0 + position.z * u_fudgeFactor;
// Divide x and y by z.
gl_Position = vec4(position.xy / zToDivideBy, position.zw);
}
因为在 clipspace 中 Z 从 -1 到 +1,我加 1 得到 zToDivideBy 从 0 到 +2 * fudgeFactor我们也需要更新代码,让我们设置 fudgeFactor。
...
var fudgeLocation = gl.getUniformLocation(program, "u_fudgeFactor")
...
var fudgeFactor = 1;
...
function drawScene() {
...
// Set the fudgeFactor
gl.uniform1f(fudgeLocation, fudgeFactor);
// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);
}
下面是结果。如果没有明确的把 “fudgefactor” 从 1 变化到 0 来看事物看起来像什么样子在我们除以 Z 之前。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F6y6H2S8-1631442055920)(en-resource://database/4883:1)]
WebGL 在我们的顶点着色器中把 X,Y,Z,W 值分配给 gl_Position 并且自动除以 W。我们可以证明通过改变着色这很容易实现,而不是自己做除法,在 gl_Position.w 中加 zToDivideBy 。
...
uniform float u_fudgeFactor;
... void main() {
// Multiply the position by the matrix.
vec4 position = u_matrix * a_position;
// Adjust the z to divide by
float zToDivideBy = 1.0 + position.z * u_fudgeFactor;
// Divide x, y and z by zToDivideBy
gl_Position = vec4(position.xyz, zToDivideBy);
}
看看这是完全相同的。为什么有这样一个事实: WebGL 自动除以 W ?因为现在,使用更多维的矩阵,我们可以使用另一个矩阵复制 z 到 w。矩阵如下
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 1,
0, 0, 0, 0,
将复制 z 到 w.你可以看看这些列如下
x_out = x_in * 1 +
y_in * 0 +
z_in * 0 +
w_in * 0 ;
y_out = x_in * 0 +
y_in * 1 +
z_in * 0 +
w_in * 0 ;
z_out = x_in * 0 +
y_in * 0 +
z_in * 1 +
w_in * 0 ;
w_out = x_in * 0 +
y_in * 0 +
z_in * 1 +
w_in * 0 ;
简化后如下
x_out = x_in;
y_out = y_in;
z_out = z_in;
w_out = z_in;
我们可以加 1 我们之前用的这个矩阵,因为我们知道 w_in 总是 1.0。
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 1,
0, 0, 0, 1,
这将改变 W 计算如下
w_out = x_in * 0 +
y_in * 0 +
z_in * 1 +
w_in * 1 ;
因为我们知道 w_in = 1.0 所以就有
w_out = z_in + 1;
最后我们可以将 fudgeFactor 加到矩阵,矩阵如下
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, fudgeFactor,
0, 0, 0, 1,
这意味着
w_out = x_in * 0 +
y_in * 0 +
z_in * fudgeFactor
+ w_in * 1 ;
简化后如下
w_out = z_in * fudgeFactor + 1;
让我们再次修改程序只使用矩阵。
首先让我们放回顶点着色器。这很简单
uniform mat4 u_matrix;
void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
...
}
接下来让我们做一个函数使 Z - > W 矩阵。
function makeZToWMatrix(fudgeFactor) {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, fudgeFactor,
0, 0, 0, 1,
];
}
我们将更改代码,以使用它
...
// Compute the matrices
var zToWMatrix = makeZToWMatrix(fudgeFactor);
...
// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationZMatrix);
matrix = matrixMultiply(matrix, rotationYMatrix);
matrix = matrixMultiply(matrix, rotationXMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, projectionMatrix);
matrix = matrixMultiply(matrix, zToWMatrix);
...
注意,这一次也是完全相同的。以上基本上是向你们展示,除以 Z 给了我们透视图,WebGL 方便地为我们除以 Z。
但是仍然有一些问题。例如如果你设置 Z 到 -100 左右,你会看到类似下面的动画。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BgFAtBPv-1631442055921)(en-resource://database/4885:1)]
发生了什么事?为什么 F 消失得很早?
就像 WebGL 剪辑 X 和 Y 或+ 1 到 - 1 它也剪辑 Z。这里看到的就是 Z<-1 的地方。我可以详细了解如何解决它,但你可以以我们做二维投影相同的方式来得到它。
我们需要利用 Z,添加一些数量和测量一定量,我们可以做任何我们想要得到的 -1 到 1 的映射范围。真正酷的事情是所有这些步骤可以在 1 个矩阵内完成。
甚至更好的,我们来决定一个 fieldOfView 而不是一个 fudgeFactor,并且计算正确的值来做这件事。这里有一个函数来生成矩阵。
function makePerspective(fieldOfViewInRadians, aspect, near, far) {
var f = Math.tan(Math.PI * 0.5 - 0.5 * fieldOfViewInRadians);
var rangeInv = 1.0 / (near - far);
return [
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (near + far) * rangeInv, -1,
0, 0, near * far * rangeInv * 2, 0
];
};
这个矩阵将为我们做所有的转换。它将调整单位,所以他们在clipspace 中它会做数学运算,因此我们可以通过角度选择视野,它会让我们选择我们的 z-clipping 空间。
它假定有一个 eye 或 camera 在原点(0,0,0)并且给定一个 zNear 和 fieldOfView 计算它需要什么,因此在 zNear 的物体在 z = - 1 结束以及在 zNear 的物体它们在中心以上或以下半个 fieldOfView,分别在 y = - 1 和 y = 1 结束。
计算 X 所使用的只是乘以传入的 aspect。我们通常将此设置为显示区域的 width / height。最后,它计算出在 Z 区域物体的规模,因此在 zFar 的物体在 z = 1 处结束。下面是动作矩阵的图。形状像四面锥的立方体旋转称为“截锥”。
矩阵在截锥内占空间并且转换到 clipspace。zNear 定义夹在前面的物体,zfar定义夹在后面的物体。设置 zNear 为23你会看到旋转的立方体的前面得到裁剪。设置 zFar 为24你会看到立方体的后面得到剪辑。只剩下一个问题。这个矩阵假定有一个视角在 0,0,0 并假定它在Z轴负方向,Y的正方向。
我们的矩阵到目前为止已经以不同的方式解决问题。为了使它工作,我们需要我们的对象在前面的视图。我们可以通过移动我们的 F 做到。 我们在(45,150,0)绘图。让我们将它移到(0,150,- 360)现在,要想使用它,我们只需要用对 makePerspective 的调用取代对make2DProjection 旧的调用
var aspect = canvas.clientWidth / canvas.clientHeight;
var projectionMatrix = makePerspective(fieldOfViewRadians, aspect, 1, 2000);
var translationMatrix = makeTranslation(translation[0], translation[1], translation[2]);
var rotationXMatrix = makeXRotation(rotation[0]);
var rotationYMatrix = makeYRotation(rotation[1]);
var rotationZMatrix = makeZRotation(rotation[2]);
var scaleMatrix = makeScale(scale[0], scale[1], scale[2]);
我们回到了一个矩阵乘法,我们得到两个领域的视图,我们可以选择我们的 z 空间。
WebGL 3D 摄像机
我们需要有效地将现实中的物体移动到摄像机的前面。能达到这个目的的最简单的方法是使用“逆”矩阵。一般情况下的逆矩阵的计算是复杂的,但从概念上讲,它是容易的。逆是你用来作为其他数值的对立的值。例如,123 的是相反数是 -123。缩放比例为5的规模矩阵的逆是 1/5 或 0.2。在 X 域旋转 30° 的矩阵的逆是一个在 X 域旋转 -30° 的矩阵。直到现在我们已经使用了平移,旋转和缩放来影响我们的 ‘F’ 的位置和方向。
把所有的矩阵相乘后,我们有一个单一的矩阵,表示如何将 “F” 以我们希望的大小和方向从原点移动到相应位置。使用摄像机我们可以做相同的事情。一旦我们的矩阵告诉我们如何从原点到我们想要的位置移动和旋转摄像机,我们就可以计算它的逆,它将给我们一个矩阵来告诉我们如何移动和旋转其它一切物体的相对数量,这将有效地使摄像机在点(0,0,0),并且我们已经将一切物体移动到它的前面。让我们做一个有一圈 ‘F’ 的三维场景,就像上面的图表那样。下面是实现代码。
var numFs = 5;
var radius = 200;
var aspect = canvas.clientWidth / canvas.clientHeight;
var projectionMatrix = makePerspective(fieldOfViewRadians, aspect, 1, 2000); // Draw 'F's in a circle
for (var ii = 0; ii < numFs; ++ii) {
var angle = ii * Math.PI * 2 / numFs;
var x = Math.cos(angle) * radius;
var z = Math.sin(angle) * radius;
var translationMatrix = makeTranslation(x, 0, z);
var matrix = translationMatrix;
matrix = matrixMultiply(matrix, projectionMatrix);
// Set the matrix.
gl.uniformMatrix4fv(matrixLocation, false, matrix);
// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);
}
就在我们计算出我们的投影矩阵之后,我们就可以计算出一个就像上面的图表中显示的那样围绕 ‘F’ 旋转的摄像机。
var cameraMatrix = makeTranslation(0, 0, radius * 1.5);
cameraMatrix = matrixMultiply( cameraMatrix, makeYRotation(cameraAngleRadians));
然后,我们根据相机矩阵计算“视图矩阵”。“视图矩阵”是将一切物体移动到摄像机相反的位置,这有效地使摄像机相对于一切物体就像在原点(0,0,0)。
var viewMatrix = makeInverse(cameraMatrix);
最后我们需要应用视图矩阵来计算每个 ‘F’ 的矩阵
var matrix = translationMatrix;
matrix = matrixMultiply(matrix, viewMatrix);
// <=-- added
matrix = matrixMultiply(matrix, projectionMatrix);
一个摄像机可以绕着一圈 “F”。拖动 cameraAngle 滑块来移动摄像机。这一切都很好,但使用旋转和平移来移动一个摄像头到你想要的地方,并且指向你想看到的地方并不总是很容易。
例如如果我们想要摄像机总是指向特定的 ‘F’ 就要进行一些非常复杂的数学计算来决定当摄像机绕 ‘F’ 圈旋转的时候如何旋转摄像机来指向那个 ‘F’。幸运的是,有一个更容易的方式。我们可以决定摄像机在我们想要的地方并且可以决定它指向什么,然后计算矩阵,这个矩阵可以将把摄像机放到那里。
基于矩阵的工作原理这非常容易实现。首先,我们需要知道我们想要摄像机在什么位置。我们将称之为 CameraPosition。然后我们需要了解我们看过去或瞄准的物体的位置。我们将把它称为 target。
如果我们将 CameraPosition 减去 target 我们将得到一个向量,它指向从摄像头获取目标的方向。让我们称它为 zAxis。因为我们知道摄像机指向 -Z 方向,我们可以从另一方向做减法 cameraPosition - target。我们将结果规范化,并直接复制到 z 区域矩阵。
这部分矩阵表示的是 Z 轴。在这种情况下,是摄像机的 Z 轴。一个向量的标准化意味着它代表了 1.0。如果你回到二维旋转的文章,在哪里我们谈到了如何与单位圆以及二维旋转,在三维中我们需要单位球面和一个归一化的向量来代表在单位球面上一点。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5gRsyymh-1631442055922)(en-resource://database/4887:1)]
虽然没有足够的信息。只是一个单一的向量给我们一个点的单位范围内,但从这一点到东方的东西?我们需要把矩阵的其他部分填好。特别的 X 轴和 Y 轴类零件。我们知道这 3 个部分是相互垂直的。我们也知道,“一般”我们不把相机指向。
因为,如果我们知道哪个方向是向上的,在这种情况下(0,1,0),我们可以使用一种叫做“跨产品和“计算 X 轴和 Y 轴的矩阵。我不知道一个跨产品意味着在数学方面。我所知道的是,如果你有 2 个单位向量和你计算的交叉产品,你会得到一个向量,是垂直于这 2 个向量。
换句话说,如果你有一个向量指向东南方,和一个向量指向上,和你计算交叉产品,你会得到一个向量指向北西或北东自这2个向量,purpendicular 到东南亚和。根据你计算交叉产品的顺序,你会得到相反的答案。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XH18mwDm-1631442055923)(en-resource://database/4889:1)]
现在,我们有 xAxis,我们可以通过 zAxis 和 xAxis 得到摄像机的 yAxis[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8BkM1BYp-1631442055924)(en-resource://database/4891:1)]
现在我们所要做的就是将 3 个轴插入一个矩阵。这使得矩阵可以指向物体,从 cameraPosition 指向 target。我们只需要添加 position
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZcmRLpjD-1631442055925)(en-resource://database/4893:1)]
下面是用来计算 2 个向量的交叉乘积的代码。
function cross(a, b) {
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
}
这是减去两个向量的代码。
function subtractVectors(a, b) {
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
}
这里是规范化一个向量(使其成为一个单位向量)的代码。
function normalize(v) {
var length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
// make sure we don't divide by 0.
if (length > 0.00001) {
return [v[0] / length, v[1] / length, v[2] / length];
} else {
return [0, 0, 0];
}
}
下面是计算一个 “lookAt” 矩阵的代码。
function makeLookAt(cameraPosition, target, up) {
var zAxis = normalize( subtractVectors(cameraPosition, target));
var xAxis = cross(up, zAxis);
var yAxis = cross(zAxis, xAxis);
return [ xAxis[0], xAxis[1], xAxis[2], 0, yAxis[0], yAxis[1], yAxis[2], 0, zAxis[0], zAxis[1], zAxis[2], 0, cameraPosition[0], cameraPosition[1], cameraPosition[2], 1];
}
这是我们如何使用它来使相机随着我们移动它指向在一个特定的 ‘F’ 的。
...
// Compute the position of the first F
var fPosition = [radius, 0, 0];
// Use matrix math to compute a position on the circle.
var cameraMatrix = makeTranslation(0, 50, radius * 1.5);
cameraMatrix = matrixMultiply( cameraMatrix, makeYRotation(cameraAngleRadians));
// Get the camera's postion from the matrix we computed
cameraPosition = [ cameraMatrix[12], cameraMatrix[13], cameraMatrix[14]]; var up = [0, 1, 0];
// Compute the camera's matrix using look at.
var cameraMatrix = makeLookAt(cameraPosition, fPosition, up);
// Make a view matrix from the camera matrix.
var viewMatrix = makeInverse(cameraMatrix);
...
拖动滑块,注意到相机追踪一个 ‘F’。请注意,您可以不只对摄像机使用 “lookAt” 函数。共同的用途是使一个人物的头跟着某人。使小塔瞄准一个目标。使对象遵循一个路径。你计算目标的路径。然后你计算出目标在未来几分钟在路径的什么地方。把这两个值放进你的 lookAt 函数,你会得到一个矩阵,使你的对象跟着路径并且朝向路径。