WebGL透视投影

2023-09-14 16:45:47

目录

透视投影

透视投影可视空间 

可视空间构造效果图

Matrix4.setPerspective()

三角形与可视化空间的相对位置

示例代码

代码详解 

示例效果

投影矩阵的作用

透视投影矩阵对物体进行了两次变换

透视投影变换示意图


透视投影

在透视投影下,产生的三维场景看上去更是有深度感,更加自然,因为我们平时观察真实世界用的也是透视投影。在大多数情况下,比如三维射击类游戏中,我们都应当采用透视投影。

在下图的场景中,道路两边都有成排的树木。树应该都是差不多高的,但是在照片上,越远的树看上去越矮。同样,道路尽头的建筑看上去比近处的树矮,但实际上那座建筑比树高很多。这种“远处的东西看上去小”的效果赋予了照片深度感,或称透视感。我们的眼睛就是这样观察世界的。有趣的是,孩童的绘画往往会忽视这一点。

在正射投影的可视空间中,不管三角形与视点的距离是远是近,它有多大,那么画出来就有多大。为了打破这条限制,我们可以使用透视投影可视空间,它将使场景具有图上图那样的深度感。 

本例PerspectiveView使用了一个透视投影可视空间,视点在(0,0,5),视线沿着Z轴负方向。下图显示了程序的运行效果,以及程序的场景中各三角形的位置。

如上图(右)所示,沿着Z轴负半轴(也就是视线方向),在轴的左右侧各依次排列着3个相同大小的三角形,场景与本例第一张图中的道路和树木有一点相似。在使用透视投影矩阵后,WebGL就能够自动将距离远的物体缩小显示,从而产生上图(左)中的深度感。 

透视投影可视空间 

透视投影可视空间如下图所示。就像盒状可视空间那样,透视投影可视空间也有视点、视线、近裁剪面和远裁剪面,这样可视空间内的物体才会被显示,可视空间外的物体则不会显示。那些跨越可视空间边界的物体则只会显示其在可视空间内的部分。

可视空间构造效果图

不论是透视投影可视空间还是盒状可视空间,我们都用投影矩阵来表示它,但是定义矩阵的参数不同。Matrix4对象的setPerspective()方法可用来定义透视投影可视空间。(WebGL矩阵变换库_山楂树の的博客-CSDN博客) 

Matrix4.setPerspective()

定义了透视投影可视空间的矩阵被称为透视投影(perspective projection matrix)。

注意,第2个参数aspect是近裁剪面的宽高比,而不是水平视角(第1个参数是垂直视角)比如说,如果近裁剪面的高度是100而宽度是200,那么宽高比就是2。

在本例中,各个三角形与可视空间的相对位置如下图所示。我们指定了near=1.0,far=100,aspect=1.0(宽度等于高度,与画面相同),以及fov=30.0。

三角形与可视化空间的相对位置

 

示例代码

var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_ViewMatrix;\n' +
  'uniform mat4 u_ProjMatrix;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_ProjMatrix * u_ViewMatrix * a_Position;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';

var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_FragColor = v_Color;\n' +
  '}\n';

function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) return;
  // 设置顶点坐标和颜色,蓝色三角形在最前面
  var n = initVertexBuffers(gl);
  gl.clearColor(0, 0, 0, 1);
  var u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
  var u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
  var viewMatrix = new Matrix4(); // 视图矩阵
  var projMatrix = new Matrix4();  // 模型矩阵
  viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 1, 0);  // 视点、被观察点、正方向
  projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100); // 视角顶底面夹角、近裁面宽高比、near、far
  // 将视图和投影矩阵传递给u_ViewMatrix、u_ProjectMatrix
  gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);
  gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
}

function initVertexBuffers(gl) {
  var verticesColors = new Float32Array([
    // 顶点坐标、颜色
    0.75,  1.0,  -4.0,  0.4,  1.0,  0.4, // 后面的绿色
    0.25, -1.0,  -4.0,  0.4,  1.0,  0.4,
    1.25, -1.0,  -4.0,  1.0,  0.4,  0.4, 
    0.75,  1.0,  -2.0,  1.0,  1.0,  0.4, // 中间的黄色
    0.25, -1.0,  -2.0,  1.0,  1.0,  0.4,
    1.25, -1.0,  -2.0,  1.0,  0.4,  0.4, 
    0.75,  1.0,   0.0,  0.4,  0.4,  1.0,  // 前面的蓝色
    0.25, -1.0,   0.0,  0.4,  0.4,  1.0,
    1.25, -1.0,   0.0,  1.0,  0.4,  0.4, 
    // 左侧有三个三角形
   -0.75,  1.0,  -4.0,  0.4,  1.0,  0.4, // 后面的绿色
   -1.25, -1.0,  -4.0,  0.4,  1.0,  0.4,
   -0.25, -1.0,  -4.0,  1.0,  0.4,  0.4, 
   -0.75,  1.0,  -2.0,  1.0,  1.0,  0.4, // 中间的黄色
   -1.25, -1.0,  -2.0,  1.0,  1.0,  0.4,
   -0.25, -1.0,  -2.0,  1.0,  0.4,  0.4, 
   -0.75,  1.0,   0.0,  0.4,  0.4,  1.0,  // 前面的蓝色
   -1.25, -1.0,   0.0,  0.4,  0.4,  1.0,
   -0.25, -1.0,   0.0,  1.0,  0.4,  0.4, 
  ]);
  var n = 18; // 六个三角形18个顶点
  var vertexColorbuffer = gl.createBuffer();  
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
  var FSIZE = verticesColors.BYTES_PER_ELEMENT;
  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
  gl.enableVertexAttribArray(a_Position);
  var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
  gl.enableVertexAttribArray(a_Color);
  return n;
}

代码详解 

main()函数中首先调用initVertexBuffers()函数,向缓冲区对象中写入这6个三角形的顶点坐标和颜色数据(第26行),在这6个三角形中,右侧3个的数据从第44行开始,左侧3个的数据从第54行开始。而且,需要绘制的顶点的个数为18(第75行,6个三角形,3×6=18)。

接着,我们获取了着色器中视图矩阵和透视投影矩阵uniform变量的存储地址(第28行和第29行),并创建了两个对应的矩阵对象(第30和第31行)。

然后,我们计算了视图矩阵(第32行),视点设置在(0,0,5),视线为Z轴负方向,上方向为Y轴正方向。最后,我们按照金字塔状的可视空间建立了透视投影矩阵(第33行)。 

其中,第2个参数aspect宽高比(近裁剪面的宽度与高度的比值)应当与<canvas>保持一致,我们根据<canvas>的width和height属性来计算出该参数,这样如果<canvas>的大小发生变化,也不会导致显示出来的图形变形。 

接下来,将准备好的视图矩阵和透视投影矩阵传给着色器中对应的uniform变量(第35和第36行)。最后将三角形绘制出来(第38行),就获得了如下图所示的效果。

示例效果

到目前为止,还有一个重要的问题没有完全解释,那就是矩阵为什么可以用来定义可视空间。接下来,我们尽量避开其中复杂的数学过程,稍做一些探讨。

投影矩阵的作用

首先看一下本例的运行效果如上图,可以看到:运用透视投影矩阵后,场景中的三角形有了两处变化

透视投影矩阵对物体进行了两次变换

首先,距离较远的三角形看上去变小了;其次,三角形被不同程度地平移以贴近中心线(即视线),使得它们看上去在视线的左右排成了两列。实际上,如下图(左)所示,这些三角形的大小是完全相同的透视投影矩阵对三角形进行了两次变换:(1)根据三角形与视点的距离,按比例对三角形进行了缩小变换;(2)对三角形进行平移变换,使其贴近视线,如下图右所示。经过了这两次变换之后,就产生了上图0那张照片中的深度效果。

透视投影变换示意图

这表明,可视空间的规范(对透视投影可视空间来说,就是近、远裁剪面,垂直视角,宽高比)可以用一系列基本变换(如缩放、平移)来定义Matrix4对象的setPerspective()方法自动地根据上述可视空间的参数计算出对应的变换矩阵

换一个角度来看,透视投影矩阵实际上将金字塔状的可视空间变换为了盒状的可视空间,这个盒状的可视空间又称规范立方体(Canonical View Volume),如上图(右)所示。

注意,正射投影矩阵不能产生深度感。正射投影矩阵的工作仅仅是将顶点从盒状的可视空间映射到规范立方体中。顶点着色器输出的顶点都必须在规范立方体中,这样才会显示在屏幕上。

更多推荐

FPGA/数字IC(芯海科技2022)面试题 2(解析版)

以下仅为学习参考(非原创),如有疑惑欢迎评论区指出!一、单选题(共20题,每题3分,共60分)1.D触发器:Tsetup=3ns,Thold=1ns,Tck2q=1ns,该D触发器最大可运行时钟频率是()A、1GHZB、500MHZC、250MHZD、200MHZ解:C最大可运行时钟频率与保持时间无关,1/(Tsetu

redis群集

目录redis群集模式主从复制主从复制的作用主从复制流程主从复制模型搭建Redis主从复制哨兵模式哨兵模式原理哨兵结构故障转移机制主节点选举机制搭建Redis哨兵模式群集模式集群的作用集群的数据分片搭建群集模式redis群集模式redis群集有三种模式,分别是主从同步/复制、哨兵模式、Cluster,下面会讲解一下三种

Qt5开发及实例V2.0-第九章-Qt文件及磁盘处理

Qt5开发及实例V2.0-第九章-Qt文件及磁盘处理第9章Qt5文件及磁盘处理9.1读写文本文件9.1.1QFile类读写文本9.1.2QTextStream类读写文本9.2读写二进制文件9.3目录操作与文件系统9.3.1文件大小及路径获取实例9.3.2文件系统浏览9.4获取文件信息9.5监视文件和目录变化本章相关例程

基于Java+SpringBoot+vue前后端分离校园周边美食探索分享平台设计实现

博主介绍:✌全网粉丝30W+,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌🍅文末获取源码联系🍅👇🏻精彩专栏推荐订阅👇🏻不然下次找不到哟2022-2024年最全的计算机软件毕业设计选题

Neutron — DHCP Agent 实现原理

目录文章目录目录DHCPDHCP协议格式DHCP报文类型DHCP协议流程DHCPAgent关键配置dnsmasq服务进程高可用方案DHCPDHCP(DynamicHostConfigurationProtocol,动态主机配置协议)用于当Host加入一个L3网络的时候动态的从一个IPPool中为Host租用一个IP地址

系统架构设计师(第二版)学习笔记----需求工程

【原文链接】系统架构设计师(第二版)学习笔记----需求工程文章目录一、需求定义1.1需求包含的内容1.2软件需求的3个不同层次1.3需求工程的阶段1.4需求管理的主要内容二、需求获取2.1需求获取的基本步骤2.2需求获取方法2.3需求讨论会参与人员2.4专题讨论会的优点三、需求变更3.1需求变更管理过程3.2需求变更

上海亚商投顾:沪指震荡调整 两市成交金额跌破6000亿

上海亚商投顾前言:无惧大盘涨跌,解密龙虎榜资金,跟踪一线游资和机构资金动向,识别短期热点和强势个股。一.市场情绪三大指数昨日集体调整,创业板指续创3年多以来新低。ST板块继续走强,*ST柏龙、ST恒久等十余股涨停。华为产业链午后活跃,捷荣技术涨停,股价创出历史新高。减肥药概念股逆势走强,翰宇药业20cm涨停。脑机接口概

Django(18):中间件原理和使用

目录概述Django自带中间件Django的中间件执行顺序自定义中间件函数使用类其它中间件钩子函数process_viewprocess_exceptionprocess_template_response如何使用这3个钩子函数?全局异常处理小结概述中间件(middleware)是一个镶嵌到Django的request

网络爬虫-----http和https的请求与响应原理

目录前言简介HTTP的请求与响应浏览器发送HTTP请求的过程:HTTP请求主要分为Get和Post两种方法查看网页请求常用的请求报头1.Host(主机和端口号)2.Connection(链接类型)3.Upgrade-Insecure-Requests(升级为HTTPS请求)4.User-Agent(浏览器名称)5.Ac

Spring Cloud Gateway快速入门(一)——网关简介

文章目录前言一、什么是网关1.1gateway的特点1.2为什么要使用gateway二、使用Nginx实现网关服务什么是网关服务?为什么选择Nginx作为网关服务?如何使用Nginx实现网关服务?1.安装Nginx2.配置Nginx3.启动Nginx4.测试网关服务总结代码编写三、使用Gateway实现网关服务什么是网

【Java 基础篇】Java后台线程和守护线程详解

在Java多线程编程中,有两种特殊类型的线程:后台线程(DaemonThread)和守护线程(DaemonThread)。这两种线程在一些特定的场景下非常有用,但也需要谨慎使用。本文将详细介绍后台线程和守护线程的概念、特性、用法,以及注意事项。什么是后台线程和守护线程?后台线程(DaemonThread)后台线程是一种

热文推荐