前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【前端er入门Shader系列】04—MVP矩阵与纹理映射

【前端er入门Shader系列】04—MVP矩阵与纹理映射

原创
作者头像
CS逍遥剑仙
修改2025-02-04 07:36:19
修改2025-02-04 07:36:19
22300
代码可运行
举报
文章被收录于专栏:禅林阆苑
运行总次数:0
代码可运行

【前端er入门Shader系列】04—MVP矩阵与纹理映射

我们生活在三维的由各种色彩构成的大千世界里,包含了复杂的细节纹理,而纹理采样作为重要的图形学技术,能够在渲染物体表面时,使用一张图片来提供颜色信息,从而增强物体的细节和真实感。本章将结合少量数学知识,介绍如何使用MVP矩阵将三维场景投射到二维屏幕,最终执行纹理采样的过程。

1. MVP矩阵概览

在顶点着色器中,会使用 MVP 矩阵进行坐标转换,P · V · M · 原始矩阵 = 最终坐标,MVP矩阵分别为:

Model Matrix:模型矩阵,处理模型自身的平移、旋转、缩放;

View Matrix:视图矩阵,处理摄像机视角转换;

Projection Matrix:投影矩阵,处理到裁剪空间的转换,分为正交投影 Orthography 和透视投影 perspective。

2. 模型变换矩阵的应用

在图形学中会大量应用三类变换:平移(translate)、旋转(rotate)、缩放(scale)。

根据数学基础知识,平移和缩放可以通过简单的向量运算实现 [x1, y1, z1] => [x2, y2, z2]

代码语言:glsl
复制
[x1, y1, z1] + [tx, ty, tz] = [x2, y2, z2] // 平移[tx, ty, tz]
k[x1, y1, z1] = [x2, y2, z2] // 缩放k倍

旋转相对复杂些,可以推导出:

代码语言:glsl
复制
[x1*cosB - y1*sinB, x1*sinB + y1*cosB, z1] = [x2, y2, z2] // 旋转B度

Shader 中的处理如下:

代码语言:javascript
代码运行次数:0
复制
...
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));
...

在图形学中,会使用矩阵来高效地进行变换运算,平移、旋转、缩放矩阵分别如下所示:

代码语言:javascript
代码运行次数:0
复制
...
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矩阵在后面会介绍

3. 使用glMatrix库简化代码编写

glMatrix 是一款高性能 JavaScript 矩阵运算库:https://github.com/toji/gl-matrix,有两种使用方式:

代码语言:glsl
复制
【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 能够简化矩阵的代码编写

代码语言:javascript
代码运行次数:0
复制
...
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]);

4. 视图矩阵 & 投影矩阵

接着继续通过代码来理解视图矩阵和投影矩阵:

代码语言:javascript
代码运行次数:0
复制
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);
  }
}

5. 纹理映射

完成基础图元的绘制后,若是需要渲染复杂的纹理,需要用到 纹理映射 技术,即指定物体的每个顶点对应纹理图片中的位置。纹理坐标是顶点上属性的之一,和顶点位置、顶点法线、顶点颜色一样,都属于常见的顶点信息。即使再复杂的3D模型,也能够展开为一张平面,可以理解为将一张图片"包裹"到物体表面。通常纹理是一张 2D 图片,整个纹理映射的过程涉及到 4 个坐标系的转换,如上图所示。习惯将纹理坐标系称为 UV 坐标系或是 st 坐标系,使用纹理坐标获取纹理颜色的方式称之为采样,每个顶点会关联着一个 纹理坐标,用来指明采样位置。

代码语言:javascript
代码运行次数:0
复制
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.UNPACK_FLIP_Y_WEBGL 图片上下翻转:这是因为在加载图片到纹理时,图片资源坐标的原点 (0,0) 在左上角,而 UV 坐标系的原点在左下角,因此需要执行一次 flipY 倒置操作。
  • gl.TEXTURE_WRAP 设置纹理环绕方式:当采样范围超出 0~1 时,需要指定水平和垂直两个方向的纹理环绕方式,有个三种可选值:gl.REPEAT / gl.MIRRORED_REPEAT / gl.CLAMP_TO_EDGE,CLAMP_TO_EDGE 是指用边缘色扩展拉伸,默认为 REPEAT 。需要注意,REPEAT 在一些如 WebGL1.0的低端 API 中要求纹理尺寸为2幂,所以3D模型渲染尽量使用2幂纹理以增强兼容性。
  • gl.TEXTURE_MIN_FILTER / gl.TEXTURE_MAG_FILTER 设置纹理过滤方式:归一化后,虽然纹理坐标能映射到不同尺寸的物体表面,但需要通过采样算法确定采样值,有两种采样算法:

(1) NEAREST 临近过滤,选择中心点最接近纹理坐标的像素,简单高效,但放大时采集效果不好;

(2) LINEAR 线性过滤:选择中心点周围最近的 4 个纹理像素加权计算,输出效果更平滑。

当对纹理图像放大和缩小时可以选择不同的过滤选项,一般缩小时采用高效的临近过滤算法,放大时采用表现更好的线性过滤算法。

此外,支持传入多张贴图,通用设备支持最少支持8个单元,对应 gl.TEXTURE0~8,现代中高端设备支持会更多,传入第二张贴图并处理颜色叠加的方式如下:

代码语言:javascript
代码运行次数:0
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 【前端er入门Shader系列】04—MVP矩阵与纹理映射
    • 1. MVP矩阵概览
    • 2. 模型变换矩阵的应用
    • 3. 使用glMatrix库简化代码编写
    • 4. 视图矩阵 & 投影矩阵
    • 5. 纹理映射
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档