一起学 WebGL:可视空间之透视矩阵

495 阅读7分钟

大家好,我是前端西瓜哥。

前面讲了视图矩阵,可以让我们像摄像机一样,在特定视点去观察模型,但还有个问题,就是它并没有近大远小的透视效果

对此我们需要引入 透视投影(Perspective Project)。

和正射投影一样,透视投影也是一种常见的可视空间,能够根据深度信息产生近大远小的透视关系。

透视投影矩阵的代码实现:

/***** 构建透视矩阵 *****/
function createPerspective(fov, aspect, near, far) {
  fov = angleToRadian(fov); // 角度转弧度
  const f = 1.0 / Math.tan(fov / 2);
  const nf = 1 / (near - far);
  // prettier-ignore
  return new Float32Array([
    f / aspect, 000,
    0, f, 00,
    00, (far + near) * nf, -1,
    002 * far * near * nf, 0,
  ]);
}

function angleToRadian(angle) {
  return (Math.PI * angle) / 180;
}

参数说明:

  • fov:视角(Fileld of View),即垂直方向的角度,透视产生的躺下的四棱柱空间的顶面和底面的夹角。这里选择使用角度值,所以在函数中转弧度值。

  • aspect:视口宽高比,通常我们会绘制为 canvas 的宽高比,这样就不会有留白的地方。

  • near:近裁剪面的位置

  • far:远裁剪面的位置

上面这个四个参数就能确定一个棱柱形的可视空间啦。

图片

图片来自 《WebGL 编程指南》

我们绘制 6 个矩形,左边放 3 个,它们的 x,y 都一样,z 不同。右边也对称放三个。

如果我们用上一节学到的正射投影,它们会因为重叠在一起,只能看到两个三角形。

图片

对应线上 demo:

codesandbox.io/s/hsvkwj?fi…

这时候我们用透视投影矩阵,就能得到透视的效果:

图片

完整源码:

/** @type {HTMLCanvasElement} */
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
const infoDiv = document.createElement('div');
document.body.appendChild(infoDiv);

const vertexShaderSrc = `
attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_ViewMatrix;  // 视图矩阵
uniform mat4 u_ProjMatrix; // 正射投影矩阵
varying vec4 v_Color;
void main() {
 gl_Position = u_ProjMatrix * u_ViewMatrix * a_Position;
 v_Color = a_Color;
}
`;

const fragmentShaderSrc = `
precision highp float;
varying vec4 v_Color;
void main() {
  gl_FragColor = v_Color;
}
`;

/**** 渲染器生成处理 ****/
// 创建顶点渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 创建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序对象
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;

// prettier-ignore
const verticesColors = new Float32Array([
  // 右边三个三角形
  // 绿(最底下)
  0.751.0, -40.410.4,
  0.25, -1, -40.410.4,
  1.25, -1, -410.40.4,
  // 黄
  0.751, -2110.4,
  0.25, -1, -2110.4,
  1.25, -1, -210.40.4, 
  // 蓝
  0.75100.410.4,
  0.25, -100.40.41,
  1.25, -1010.40.4,

  // 左边三个三角形
  // 绿(最底下)
  -0.751.0, -40.410.4,
  -0.25, -1, -40.410.4,
  -1.25, -1, -410.40.4,
  // 黄
  -0.751, -2110.4,
  -0.25, -1, -2110.4,
  -1.25, -1, -210.40.4, 
  // 蓝
  -0.75100.410.4,
  -0.25, -100.40.41,
  -1.25, -1010.40.4,

]);
// 每个数组元素的字节数
const SIZE = verticesColors.BYTES_PER_ELEMENT;
// 创建缓存对象
const vertexColorBuffer = gl.createBuffer();
// 绑定缓存对象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
// 向缓存区写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

// 获取 a_Position 变量地址
const a_Position = gl.getAttribLocation(gl.program'a_Position');
const a_Color = gl.getAttribLocation(gl.program'a_Color');
/****** 正射投影 ******/
const u_ViewMatrix = gl.getUniformLocation(gl.program'u_ViewMatrix');

// prettier-ignore
const viewMatrix = createViewMatrix(
  005// 观察点
  00, -100// 视点
  010 // 上方向
)
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix);

/****** 正射投影 ******/
const u_ProjMatrix = gl.getUniformLocation(gl.program'u_ProjMatrix');
// prettier-ignore
const projMatrix = createPerspective(
  30, canvas.width / canvas.height1100
)
gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix);
console.log(projMatrix);

gl.vertexAttribPointer(a_Position, 3, gl.FLOATfalseSIZE * 60);
gl.enableVertexAttribArray(a_Position);

gl.vertexAttribPointer(a_Color, 3, gl.FLOATfalseSIZE * 6SIZE * 3);
gl.enableVertexAttribArray(a_Color);

/*** 绘制 ***/
// 清空画布,并指定颜色
gl.clearColor(0001);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES018);

function angleToRadian(angle) {
  return (Math.PI * angle) / 180;
}

/***** 构建透视矩阵 *****/
function createPerspective(fov, aspect, near, far) {
  fov = angleToRadian(fov); // 角度转弧度
  const f = 1.0 / Math.tan(fov / 2);
  const nf = 1 / (near - far);
  // prettier-ignore
  return new Float32Array([
    f / aspect, 000,
    0, f, 00,
    00, (far + near) * nf, -1,
    002 * far * near * nf, 0,
  ]);
}

/**** 构造视图矩阵 ****/
function createViewMatrix(eyeX, eyeY, eyeZ, atX, atY, atZ, upX, upY, upZ) {
  const normalize = (v) => {
    const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
    return [v[0] / length, v[1] / length, v[2] / length];
  };
  const subtract = (v1, v2) => {
    return [v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2]];
  };
  const cross = (v1, v2) => {
    return [
      v1[1] * v2[2] - v1[2] * v2[1],
      v1[2] * v2[0] - v1[0] * v2[2],
      v1[0] * v2[1] - v1[1] * v2[0],
    ];
  };

  const zAxis = normalize(subtract([eyeX, eyeY, eyeZ], [atX, atY, atZ]));
  const xAxis = normalize(cross([upX, upY, upZ], zAxis));
  const yAxis = normalize(cross(zAxis, xAxis));

  return new Float32Array([
    xAxis[0],
    yAxis[0],
    zAxis[0],
    0,
    xAxis[1],
    yAxis[1],
    zAxis[1],
    0,
    xAxis[2],
    yAxis[2],
    zAxis[2],
    0,
    -(xAxis[0] * eyeX + xAxis[1] * eyeY + xAxis[2] * eyeZ),
    -(yAxis[0] * eyeX + yAxis[1] * eyeY + yAxis[2] * eyeZ),
    -(zAxis[0] * eyeX + zAxis[1] * eyeY + zAxis[2] * eyeZ),
    1,
  ]);
}

线上体验 demo:

codesandbox.io/s/9ujd77?fi…

const orthoMatrix = createOrthoMatrix(-11, -111.05, -1);

/********* 构造正射投影 *********/
function createOrthoMatrix(left, right, bottom, top, near, far) {
  const width = right - left;
  const height = top - bottom;
  const depth = far - near;

  // prettier-ignore
  return new Float32Array([
    2 / width, 000,
    02 / height, 00,
    00, -2 / depth, 0,
    -(right + left) / width-(top + bottom) / height-(far + near) / depth, 1
  ]);
}

结尾

我是前端西瓜哥,欢迎关注我,学习更多前端图形学知识。


相关阅读,

一起学 WebGL:可视空间之正射投影

一起学 WebGL:感受三维世界之视图矩阵

一起学 WebGL:三角形加上渐变色

一起学 WebGL:复合矩阵

一起学 WebGL:绘制图片

一起学 WebGL:图元的类型

一起学 WebGL:绘制三角形

一起学 WebGL:改变点的颜色

一起学 WebGL:动态绘制点

一起学 WebGL:绘制一个点

一起学 WebGL:坐标系