我们生活在三维的由各种色彩构成的大千世界里,包含了复杂的细节纹理,而纹理采样作为重要的图形学技术,能够在渲染物体表面时,使用一张图片来提供颜色信息,从而增强物体的细节和真实感。本章将结合少量数学知识,介绍如何使用MVP矩阵将三维场景投射到二维屏幕,最终执行纹理采样的过程。
在顶点着色器中,会使用 MVP 矩阵进行坐标转换,P · V · M · 原始矩阵 = 最终坐标
,MVP矩阵分别为:
Model Matrix:模型矩阵,处理模型自身的平移、旋转、缩放;
View Matrix:视图矩阵,处理摄像机视角转换;
Projection Matrix:投影矩阵,处理到裁剪空间的转换,分为正交投影 Orthography 和透视投影 perspective。
在图形学中会大量应用三类变换:平移(translate)、旋转(rotate)、缩放(scale)。
根据数学基础知识,平移和缩放可以通过简单的向量运算实现 [x1, y1, z1] => [x2, y2, z2]
:
[x1, y1, z1] + [tx, ty, tz] = [x2, y2, z2] // 平移[tx, ty, tz]
k[x1, y1, z1] = [x2, y2, z2] // 缩放k倍
旋转相对复杂些,可以推导出:
[x1*cosB - y1*sinB, x1*sinB + y1*cosB, z1] = [x2, y2, z2] // 旋转B度
Shader 中的处理如下:
...
const vertexShader = `
attribute vec2 a_position;
uniform float cosB;
uniform float sinB;
void main() {
// 变换前
float x1 = a_position.x;
float y1 = a_position.y;
float z1 = 0.0;
// 变换后
float x2 = x1*cosB - y1*sinB;
float y2 = x1*sinB + y1*cosB;
float z2 = z1;
gl_Position = vec4(x2, y2, z2, 1.0); // vec4
}`;
...
deg = 30; // 旋转角度
const sinB = gl.getUniformLocation(gl.program, 'sinB');
const cosB = gl.getUniformLocation(gl.program, 'cosB');
gl.uniform1f(sinB, Math.sin(deg / 180 * Math.PI));
gl.uniform1f(cosB, Math.cos(deg / 180 * Math.PI));
...
在图形学中,会使用矩阵来高效地进行变换运算,平移、旋转、缩放矩阵分别如下所示:
...
const vertexShader = `
attribute vec2 a_position;
uniform mat4 u_tMatrix;
uniform mat4 u_rMatrix;
uniform mat4 u_sMatrix;
void main() {
// 变换矩阵 x 向量
// OpenGL中的乘法顺序为从左向右: P * V * M平移 * M旋转 * M缩放 * 3DPoint
// 实际执行顺序为从右向左: 3DPoint * M缩放 * M旋转 * M平移 * V * P
gl_Position = u_tMatrix * u_rMatrix * u_sMatrix * vec4(a_position, 0.0, 1.0);
}`;
// 缩放
const Sx = 1, Sy = 1, Sz = 1;
const scale_matrix = [
Sx, 0, 0, 0,
0, Sy, 0, 0,
0, 0, Sz, 0,
0, 0, 0, 1,
];
// 平移
const Tx = 0, Ty = 0, Tz = 0;
const translate_matrix = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
Tx, Ty, Tz, 1, // 注意:js在写矩阵时,行列需要互换转置(AT)
];
// 旋转
const deg = 30;
const cos = Math.cos((deg / 180) * Math.PI), sin = Math.sin((deg / 180) * Math.PI);
const rotate_matrix = [
cos, sin, 0, 0,
-sin, cos, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
];
// 传入变换矩阵
const u_tMatrix = gl.getUniformLocation(gl.program, "u_tMatrix");
gl.uniformMatrix4fv(u_tMatrix, false, new Float32Array(translate_matrix));
const u_rMatrix = gl.getUniformLocation(gl.program, "u_rMatrix");
gl.uniformMatrix4fv(u_rMatrix, false, new Float32Array(rotate_matrix)); // 传入旋转矩阵,旋转30度
const u_sMatrix = gl.getUniformLocation(gl.program, "u_sMatrix");
gl.uniformMatrix4fv(u_sMatrix, false, new Float32Array(scale_matrix));
...
注意:矩阵相乘没有交换律,必须按照指定顺序编写矩阵乘法!
在 OpenGL 中指定的乘法顺序为 P * V * M * 3DPoint
,因为三大变换发生在 M 中,因此乘法顺序为 P * V * M平移 * M旋转 * M缩放 * 3DPoint
,实际的矩阵执行顺序为 3DPoint * M缩放 * M旋转 * M平移 * V * P
。
MVP矩阵在后面会介绍
glMatrix 是一款高性能 JavaScript 矩阵运算库:https://github.com/toji/gl-matrix,有两种使用方式:
【1】直接修改原矩阵 matrix
mat4.fromScaling(matrix, [Sx, Sy, Sz]);
mat4.fromTranslation(matrix, [Tx, Ty, Tz]);
mat4.fromRotation(matrix, deg, [X, Y, Z]);
【2】传入原矩阵 matrix1 修改矩阵 matrix2
mat4.scale(matrix2, matrix1, [Sx, Sy, Sz]);
mat4.rotate(matrix2, matrix1, matrix, deg, [X, Y, Z]);
mat4.translate(matrix2, matrix1, [Tx, Ty, Tz]);
使用 glMatrix 能够简化矩阵的代码编写
...
const vertexShader = `
attribute vec2 a_position;
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * vec4(a_position, 0.0, 1.0);
}`;
...
// glMatrix.toRadian 角度转弧度,10° => 10 / 180 * Math.PI
mat4.fromRotation(rotate_matrix, glMatrix.toRadian(10), [0, 0, 1]);
接着继续通过代码来理解视图矩阵和投影矩阵:
import initShaders from "./initShaders.js";
import { mat4 } from "./gl_matrix/esm/index.js";
const canvas = document.getElementById("webgl");
const gl = canvas.getContext("webgl");
/**
* y
* ↑
* o → x
* ↙z
* v5----- v8
* /| /|
* v1------v4|
* | | | |
* | |v6---|-|v7
* |/ |/
* v2------v3
*/
const v1 = [-0.5, 0.5, 0.5];
const v2 = [-0.5, -0.5, 0.5];
const v3 = [0.5, -0.5, 0.5];
const v4 = [0.5, 0.5, 0.5];
const v5 = [-0.5, 0.5, -0.5];
const v6 = [-0.5, -0.5, -0.5];
const v7 = [0.5, -0.5, -0.5];
const v8 = [0.5, 0.5, -0.5];
const positions = [
...v1, ...v2, ...v3, ...v4, // 前面
...v5, ...v6, ...v7, ...v8, // 后面
...v5, ...v6, ...v2, ...v1, // 左面
...v4, ...v3, ...v7, ...v8, // 右面
...v5, ...v1, ...v4, ...v8, // 上面
...v6, ...v2, ...v3, ...v7, // 下面
];
const c1 = [1.0, 0.0, 0.0];
const c2 = [0.0, 1.0, 0.0];
const c3 = [0.0, 0.0, 1.0];
const c4 = [0.0, 1.0, 1.0];
const c5 = [1.0, 0.0, 1.0];
const c6 = [1.0, 1.0, 0.0];
const colors = [
...c1, ...c1, ...c1, ...c1, //前面
...c2, ...c2, ...c2, ...c2, //后面
...c3, ...c3, ...c3, ...c3, //左面
...c4, ...c4, ...c4, ...c4,//右面
...c5, ...c5, ...c5, ...c5, //上面
...c6, ...c6, ...c6, ...c6, //下面
];
const vertexShader = /*glsl*/`
attribute vec3 a_position;
attribute vec3 a_color;
varying vec3 v_color;
uniform mat4 u_modelMatrix;
uniform mat4 u_viewMatrix;
uniform mat4 u_projMatrix;
void main() {
v_color = a_color;
// P · V · M
// gl_Position = u_projMatrix * u_viewMatrix * u_modelMatrix * vec4(a_position, 1.0);
gl_Position = u_projMatrix * u_viewMatrix * vec4(a_position, 1.0);
}`;
const fragmentShader = /*glsl*/`
precision mediump float;
varying vec3 v_color;
void main() {
gl_FragColor = vec4(v_color, 1.0);
}`;
initShaders(gl, vertexShader, fragmentShader);
initVertexBuffers(gl);
// 视图矩阵 ViewMatrix
const viewMatrix = mat4.create();
// lookAt(out, eye, center, up)
const eye = [3, 4, 5];
mat4.lookAt(viewMatrix, eye, [0, 0, 0], [0, 1, 0]);
const u_viewMatrix = gl.getUniformLocation(gl.program, "u_viewMatrix");
gl.uniformMatrix4fv(u_viewMatrix, false, viewMatrix);
// 投影矩阵ProjectionMatrix - 【正交投影Orthography】
const u_projMatrix = gl.getUniformLocation(gl.program, "u_projMatrix");
// ortho(out, left, right, bottom, top, near, far)
const orthoMatrix = mat4.create();
mat4.ortho(orthoMatrix, -1, 1, -1, 1, 0, 100);
// gl.uniformMatrix4fv(u_projMatrix, false, orthoMatrix);
// 投影矩阵ProjectionMatrix - 【透视投影Perspective】
// perspective(out, fovy, aspect, near, far)
const perspectiveMatrix = mat4.create();
mat4.perspective(
perspectiveMatrix,
(50 / 180) * Math.PI,
canvas.width / canvas.height,
0.1,
100
);
gl.uniformMatrix4fv(u_projMatrix, false, perspectiveMatrix);
function tick() {
let time = Date.now() * 0.005;
// eye[0] = Math.sin(time);
eye[1] = Math.sin(time);
eye[2] = Math.cos(time);
// 调整视图矩阵
mat4.lookAt(viewMatrix, eye, [0, 0, 0], [0, 1, 0]);
gl.uniformMatrix4fv(u_viewMatrix, false, viewMatrix);
draw(gl);
requestAnimationFrame(tick);
}
tick();
draw(gl);
function initVertexBuffers(gl) {
let FSIZE = positions.BYTES_PER_ELEMENT;
let positionsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
let a_position = gl.getAttribLocation(gl.program, "a_position");
gl.vertexAttribPointer(a_position, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 0);
gl.enableVertexAttribArray(a_position);
let colorsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
let a_color = gl.getAttribLocation(gl.program, "a_color");
gl.vertexAttribPointer(a_color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 0);
gl.enableVertexAttribArray(a_color);
}
function draw(gl) {
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
for (let i = 0; i < 24; i += 4) {
gl.drawArrays(gl.TRIANGLE_FAN, i, 4);
}
}
完成基础图元的绘制后,若是需要渲染复杂的纹理,需要用到 纹理映射 技术,即指定物体的每个顶点对应纹理图片中的位置。纹理坐标是顶点上属性的之一,和顶点位置、顶点法线、顶点颜色一样,都属于常见的顶点信息。即使再复杂的3D模型,也能够展开为一张平面,可以理解为将一张图片"包裹"到物体表面。通常纹理是一张 2D 图片,整个纹理映射的过程涉及到 4 个坐标系的转换,如上图所示。习惯将纹理坐标系称为 UV 坐标系或是 st 坐标系,使用纹理坐标获取纹理颜色的方式称之为采样,每个顶点会关联着一个 纹理坐标,用来指明采样位置。
import initShaders from "./initShaders.js";
const gl = document.getElementById("webgl").getContext("webgl");
const vertexShader = `
attribute vec3 a_position;
attribute vec2 a_uv;
varying vec2 v_uv;
void main() {
v_uv = a_uv;
gl_Position = vec4(a_position, 1.0);
}`;
const fragmentShader = `
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_sampler;
void main() {
// 纹理采样
vec4 color = texture2D(u_sampler, v_uv);
// 着色
gl_FragColor = color;
}`;
initShaders(gl, vertexShader, fragmentShader);
initVertexBuffers(gl);
initTextures(gl);
function initTextures(gl) {
const texture = gl.createTexture();
const u_sampler = gl.getUniformLocation(gl.program, "u_sampler");
const image = new Image();
image.crossOrigin = ''; // 处理跨域
image.src = "./imgs/logo.jpg"; // 注意图片尺寸为512倍数
// 异步加载完成回调
image.onload = function () {
// 翻转图片Y轴,默认不翻转,即采样原点为左上角(0,0),启用翻转Y后原点为左下角(0,0)
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
// 激活贴图,放置0号纹理单元上(通用设备支持最少支持8个单元,现代中高端设备支持会更多) gl.TEXTURE0~8
gl.activeTexture(gl.TEXTURE0);
// 绑定贴图(贴图类型)
gl.bindTexture(gl.TEXTURE_2D, texture);
// 设置贴图参数 gl.texParameteri(贴图类型,参数名,参数值)
// 【1】设置纹理过滤方式
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); // 大图贴小形状,临近过滤
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); // 小图贴大形状,线性过滤
// 【2】设置ST方向的纹理的环绕方式 gl.REPEAT / gl.MIRRORED_REPEAT / gl.CLAMP_TO_EDGE
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// gl.texImage2D(target, level, internalformat, format, type, HTMLImageElement? pixels);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.uniform1i(u_sampler, 0); // 0单元
draw(gl);
};
}
function initVertexBuffers(gl) {
// shader目标位置,此案例为右上角
const positions = new Float32Array([
-0.5, -0.5, 0.0,
1, -0.5, 0.0,
1, 1, 0.0,
-0.5, 1, 0.0,
]);
// 图片采样位置,默认原点为左上角(0,0),启用翻转Y后为左下角(0,0),此案例采样左下角
const uvs = new Float32Array([
0.0, 0.0,
0.5, 0.0,
0.5, 0.5,
0.0, 0.5,
]);
// 纹理采样超出 0~1
/*
const uvs = new Float32Array([
-1.5, -1.5,
1.0, -1.5,
1.0, 1.0,
-1.5, 1.0,
]);
*/
const FSIZE = positions.BYTES_PER_ELEMENT; // Float32 Size = 4
const positionsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
const a_position = gl.getAttribLocation(gl.program, "a_position");
gl.vertexAttribPointer(a_position, 3, gl.FLOAT, false, FSIZE * 3, 0);
gl.enableVertexAttribArray(a_position);
// 第二个 buffer 用于存储uv,也可以合并到同一个顶点缓冲
const uvsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, uvsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, uvs, gl.STATIC_DRAW);
const a_uv = gl.getAttribLocation(gl.program, "a_uv");
gl.vertexAttribPointer(a_uv, 2, gl.FLOAT, false, FSIZE * 2, 0);
gl.enableVertexAttribArray(a_uv);
}
function draw(gl) {
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
gl.drawArrays(gl.POINTS, 0, 4);
}
在上面代码中有几处细节:
gl.REPEAT
/ gl.MIRRORED_REPEAT
/ gl.CLAMP_TO_EDGE
,CLAMP_TO_EDGE 是指用边缘色扩展拉伸,默认为 REPEAT
。需要注意,REPEAT
在一些如 WebGL1.0
的低端 API 中要求纹理尺寸为2幂
,所以3D模型渲染尽量使用2幂
纹理以增强兼容性。(1) NEAREST 临近过滤,选择中心点最接近纹理坐标的像素,简单高效,但放大时采集效果不好;
(2) LINEAR 线性过滤:选择中心点周围最近的 4 个纹理像素加权计算,输出效果更平滑。
当对纹理图像放大和缩小时可以选择不同的过滤选项,一般缩小时采用高效的临近过滤算法,放大时采用表现更好的线性过滤算法。
此外,支持传入多张贴图,通用设备支持最少支持8个单元,对应 gl.TEXTURE0~8,现代中高端设备支持会更多,传入第二张贴图并处理颜色叠加的方式如下:
const fragmentShader = `
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_sampler1;
uniform sampler2D u_sampler2;
void main() {
vec4 color1 = texture2D(u_sampler1, v_uv);
vec4 color2 = texture2D(u_sampler2, v_uv);
// 纹理色值相乘,黑色xRGB=黑色,白色xRGB=白色
// gl_FragColor = color1 * color2;
// PS混合模式-变量
gl_FragColor = color1 * (vec4(1.0, 1.0, 1.0, 2.0) - color2);
}`;
...
// 加载第二张图片
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.activeTexture(gl.TEXTURE1); // 1单元
gl.bindTexture(gl.TEXTURE_2D, texture2);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image2);
gl.uniform1i(u_sampler2, 1); // 1单元
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。