百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 博客教程 > 正文

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

connygpt 2024-11-18 10:41 6 浏览

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

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

对此我们需要引入 透视投影(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, 0, 0, 0,
    0, f, 0, 0,
    0, 0, (far + near) * nf, -1,
    0, 0, 2 * 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:

https://codesandbox.io/s/hsvkwj?file=/index.js

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

完整源码:

/** @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.75, 1.0, -4, 0.4, 1, 0.4,
  0.25, -1, -4, 0.4, 1, 0.4,
  1.25, -1, -4, 1, 0.4, 0.4,
  // 黄
  0.75, 1, -2, 1, 1, 0.4,
  0.25, -1, -2, 1, 1, 0.4,
  1.25, -1, -2, 1, 0.4, 0.4, 
  // 蓝
  0.75, 1, 0, 0.4, 1, 0.4,
  0.25, -1, 0, 0.4, 0.4, 1,
  1.25, -1, 0, 1, 0.4, 0.4,
  // 左边三个三角形
  // 绿(最底下)
  -0.75, 1.0, -4, 0.4, 1, 0.4,
  -0.25, -1, -4, 0.4, 1, 0.4,
  -1.25, -1, -4, 1, 0.4, 0.4,
  // 黄
  -0.75, 1, -2, 1, 1, 0.4,
  -0.25, -1, -2, 1, 1, 0.4,
  -1.25, -1, -2, 1, 0.4, 0.4, 
  // 蓝
  -0.75, 1, 0, 0.4, 1, 0.4,
  -0.25, -1, 0, 0.4, 0.4, 1,
  -1.25, -1, 0, 1, 0.4, 0.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(
  0, 0, 5, // 观察点
  0, 0, -100, // 视点
  0, 1, 0 // 上方向
)
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix);
/****** 正射投影 ******/
const u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
// prettier-ignore
const projMatrix = createPerspective(
  30, canvas.width / canvas.height, 1, 100
)
gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix);
console.log(projMatrix);
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, SIZE * 6, 0);
gl.enableVertexAttribArray(a_Position);
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, SIZE * 6, SIZE * 3);
gl.enableVertexAttribArray(a_Color);
/*** 绘制 ***/
// 清空画布,并指定颜色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 18);
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, 0, 0, 0,
    0, f, 0, 0,
    0, 0, (far + near) * nf, -1,
    0, 0, 2 * 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:

https://codesandbox.io/s/9ujd77?file=/index.js

const orthoMatrix = createOrthoMatrix(-1, 1, -1, 1, 1.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, 0, 0, 0,
    0, 2 / height, 0, 0,
    0, 0, -2 / depth, 0,
    -(right + left) / width, -(top + bottom) / height, -(far + near) / depth, 1
  ]);
}

结尾

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

相关推荐

自学Python,写一个挨打的游戏代码来初识While循环

自学Python的第11天。旋转~跳跃~,我~闭着眼!学完循环,沐浴着while的光芒,闲来无事和同事一起扯皮,我说:“编程语言好神奇,一个小小的循环,竟然在生活中也可以找到原理和例子”,同事也...

常用的 Python 工具与资源,你知道几个?

最近几年你会发现,越来越多的人开始学习Python,工欲善其事必先利其器,今天纬软小编就跟大家分享一些常用的Python工具与资源,记得收藏哦!不然下次就找不到我了。1、PycharmPychar...

一张思维导图概括Python的基本语法, 一周的学习成果都在里面了

一周总结不知不觉已经自学Python一周的时间了,这一周,从认识Python到安装Python,再到基本语法和基本数据类型,对于小白的我来说无比艰辛的,充满坎坷。最主要的是每天学习时间有限。只...

三日速成python?打工人,小心钱包,别当韭菜

随着人工智能的热度越来越高,许多非计算机专业的同学们也都纷纷投入到学习编程的道路上来。而Python,作为一种相对比较容易上手的语言,也越来越受欢迎。网络上各类网课层出不穷,各式广告令人眼花缭乱。某些...

Python自动化软件测试怎么学?路线和方法都在这里了

Python自动化测试是指使用Python编程语言和相关工具,对软件系统进行自动化测试的过程。学习Python自动化测试需要掌握以下技术:Python编程语言:学习Python自动化测试需要先掌握Py...

Python从放弃到入门:公众号历史文章爬取为例谈快速学习技能

这篇文章不谈江流所专研的营销与运营,而聊一聊技能学习之路,聊一聊Python这门最简单的编程语言该如何学习,我完成的第一个Python项目,将任意公众号的所有历史文章导出成PDF电子书。或许我这个Py...

【黑客必会】python学习计划

阅读Python文档从Python官方网站上下载并阅读Python最新版本的文档(中文版),这是学习Python的最好方式。对于每个新概念和想法,请尝试运行一些代码片段,并检查生成的输出。这将帮助您更...

公布了!2025CDA考试安排

CDA数据分析师报考流程数据分析师是指在不同行业中专门从事行业数据搜集、整理、分析依据数据作出行业研究评估的专业人员CDA证书分为1-3级,中英文双证就业面广,含金量高!!?报考条件:满18...

一文搞懂全排列、组合、子集问题(经典回溯递归)

原创公众号:【bigsai】头条号:程序员bigsai前言Hello,大家好,我是bigsai,longtimenosee!在刷题和面试过程中,我们经常遇到一些排列组合类的问题,而全排列、组合...

「西法带你学算法」一次搞定前缀和

我花了几天时间,从力扣中精选了五道相同思想的题目,来帮助大家解套,如果觉得文章对你有用,记得点赞分享,让我看到你的认可,有动力继续做下去。467.环绕字符串中唯一的子字符串[1](中等)795.区...

平均数的5种方法,你用过几种方法?

平均数,看似很简单的东西,其实里面包含着很多学问。今天,分享5种经常会用到的平均数方法。1.算术平均法用到最多的莫过于算术平均法,考试平均分、平均工资等等,都是用到这个。=AVERAGE(B2:B11...

【干货收藏】如何最简单、通俗地理解决策树分类算法?

决策树(Decisiontree)是基于已知各种情况(特征取值)的基础上,通过构建树型决策结构来进行分析的一种方式,是常用的有监督的分类算法。决策树算法是机器学习中的一种经典算法,它通过一系列的规则...

面试必备:回溯算法详解

我们刷leetcode的时候,经常会遇到回溯算法类型题目。回溯算法是五大基本算法之一,一般大厂也喜欢问。今天跟大家一起来学习回溯算法的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~什么是回溯...

「机器学习」决策树——ID3、C4.5、CART(非常详细)

决策树是一个非常常见并且优秀的机器学习算法,它易于理解、可解释性强,其可作为分类算法,也可用于回归模型。本文将分三篇介绍决策树,第一篇介绍基本树(包括ID3、C4.5、CART),第二篇介绍Ran...

大话AI算法: 决策树

所谓的决策树算法,通俗的说就是建立一个树形的结构,通过这个结构去一层一层的筛选判断问题是否好坏的算法。比如判断一个西瓜是否好瓜,有20条西瓜的样本提供给你,让你根据这20条(通过机器学习)建立起...