希望这篇小文章对您有所帮助。它概括了我所学到的关于 WebGL 和 3D 的大部分内容。顺便说一句,如果我有任何错误,请有人纠正我——因为我也在学习!
架构
浏览器就是这样,一个网络浏览器。它所做的只是公开 WebGL API(通过 JavaScript),程序员可以用它来做其他事情。
据我所知,WebGL API 本质上只是一组(浏览器提供的)JavaScript 函数,它们围绕着 OpenGL ES 规范。因此,如果您了解 OpenGL ES,则可以很快采用 WebGL。不过,不要将此与纯 OpenGL 混淆。 “ES”很重要。
WebGL 规范被刻意留得很低级,留下了很多东西
从一个应用程序到下一个应用程序重新实现。这取决于
社区为自动化编写框架,并由开发人员决定
选择使用哪个框架(如果有)。这并不完全困难
推出自己的,但这确实意味着花费大量的开销
重新发明轮子。 (FWIW,我一直在自己工作 WebGL
framework called Jax 一段时间
现在。)
图形驱动程序提供实际运行代码的 OpenGL ES 实现。此时,它在机器硬件上运行,甚至低于 C 代码。虽然这首先使 WebGL 成为可能,但它也是一把双刃剑,因为 OpenGL ES 驱动程序中的错误(我已经注意到很多)会出现在您的 Web 应用程序中,而您不会除非您可以依靠您的用户群来提交连贯的错误报告,包括操作系统、视频硬件和驱动程序版本,否则您必须知道它。 Here's what the debug process for such issues ends up looking like.
在 Windows 上,WebGL API 和硬件之间存在一个额外的层:ANGLE, or "Almost Native Graphics Layer Engine"。因为 Windows 上的 OpenGL ES 驱动程序通常很糟糕,所以 ANGLE 接收这些调用并将它们转换为 DirectX 9 调用。
3D 绘图
既然您已经知道这些部分是如何组合在一起的,那么让我们看一下关于如何将所有部分组合在一起以生成 3D 图像的较低层次的解释。
JavaScript
首先,JavaScript 代码从 HTML5 canvas 元素中获取 3D 上下文。然后它注册一组着色器,这些着色器是用 GLSL([Open] GL 着色语言)编写的,本质上类似于 C 代码。
流程的其余部分非常模块化。您需要使用在着色器中定义的制服和属性将顶点数据和您打算使用的任何其他信息(例如顶点颜色、纹理坐标等)向下传递到图形管道,但确切的布局和命名此信息很大程度上取决于开发人员。
JavaScript 设置初始数据结构并将它们发送到 WebGL API,后者将它们发送到 ANGLE 或 OpenGL ES,最终将其发送到图形硬件。
顶点着色器
一旦信息可供着色器使用,着色器必须分两个阶段转换信息以生成 3D 对象。第一个阶段是顶点着色器,它设置网格坐标。 (这个阶段完全在视频卡上运行,在上面讨论的所有 API 之下。)大多数情况下,在顶点着色器上执行的过程看起来像这样:
gl_Position = PROJECTION_MATRIX * VIEW_MATRIX * MODEL_MATRIX * VERTEX_POSITION
其中VERTEX_POSITION 是一个 4D 向量(x、y、z 和 w,通常设置为 1); VIEW_MATRIX 是一个 4x4 矩阵,代表相机对世界的看法; MODEL_MATRIX 是一个 4x4 矩阵,它将对象空间坐标(即,在应用旋转或平移之前对象的局部坐标)转换为世界空间坐标; PROJECTION_MATRIX 代表相机的镜头。
大多数情况下,VIEW_MATRIX 和 MODEL_MATRIX 是预先计算的,并且
叫MODELVIEW_MATRIX。有时,所有 3 个都被预先计算为
MODELVIEW_PROJECTION_MATRIX 或只是 MVP。这些通常是指
作为优化,虽然我想找时间做一些基准测试。它是
JavaScript 中的预计算可能实际上更慢,如果它是
每一帧都完成,因为 JavaScript 本身并不是那么快。在
在这种情况下,通过对
GPU 可能比在 JavaScript 中的 CPU 上执行更快。我们可以
当然希望未来的 JS 实现能够解决这个潜力
只需更快地解决问题。
剪辑坐标
当所有这些都被应用后,gl_Position 变量将具有一组范围为 [-1, 1] 的 XYZ 坐标和一个 W 分量。这些被称为剪辑坐标。
值得注意的是,剪辑坐标是顶点着色器真正唯一的东西
需要生产。您可以完全跳过矩阵变换
上面执行,只要你产生一个剪辑坐标结果。 (我什至有
尝试用四元数替换矩阵;有效
很好,但我放弃了这个项目,因为我没有得到
我希望的性能改进。)
在向gl_Position 提供剪辑坐标后,WebGL 将结果除以gl_Position.w,生成所谓的标准化设备坐标。
从那里开始,将像素投影到屏幕上很简单,只需将屏幕尺寸乘以 1/2,然后再加上屏幕尺寸的 1/2。[1] 以下是一些剪辑坐标转换的示例在 800x600 显示器上转换为 2D 坐标:
clip = [0, 0]
x = (0 * 800/2) + 800/2 = 400
y = (0 * 600/2) + 600/2 = 300
clip = [0.5, 0.5]
x = (0.5 * 800/2) + 800/2 = 200 + 400 = 600
y = (0.5 * 600/2) + 600/2 = 150 + 300 = 450
clip = [-0.5, -0.25]
x = (-0.5 * 800/2) + 800/2 = -200 + 400 = 200
y = (-0.25 * 600/2) + 600/2 = -150 + 300 = 150
像素着色器
一旦确定了应该在哪里绘制像素,该像素就会被传递给像素着色器,由像素着色器选择像素的实际颜色。这可以通过多种方式完成,从简单地硬编码特定颜色到纹理查找到更高级的法线和视差映射(本质上是“欺骗”纹理查找以产生不同效果的方法)。
深度和深度缓冲区
现在,到目前为止,我们已经忽略了剪辑坐标的 Z 分量。这是怎么回事。当我们乘以投影矩阵时,第三个剪辑分量产生了一些数字。如果该数字大于 1.0 或小于 -1.0,则该数字超出了投影矩阵的视野范围,分别对应于矩阵 zFar 和 zNear 值。
因此,如果它不在 [-1, 1] 范围内,则它会被完全裁剪。如果它是在那个范围内,那么Z值被缩放到0到1[2]并与深度缓冲区比较[3]。深度缓冲区等于屏幕尺寸,因此如果使用 800x600 的投影,深度缓冲区为 800 像素宽和 600 像素高。我们已经有了像素的 X 和 Y 坐标,因此将它们插入深度缓冲区以获取当前存储的 Z 值。如果 Z 值大于新 Z 值,则新 Z 值更接近比之前绘制的任何值,并替换它[4]。此时点亮相关像素是安全的(或者在 WebGL 的情况下,将像素绘制到画布上),并将 Z 值存储为新的深度值。
如果 Z 值大于存储的深度值,则认为它在“后面”已经绘制的任何内容,并且该像素被丢弃。
[1]实际转换使用gl.viewport设置将标准化设备坐标转换为像素。
[2]它实际上被缩放到gl.depthRange 设置。它们默认为 0 到 1。
[3]假设您有一个深度缓冲区并且您已使用 gl.enable(gl.DEPTH_TEST) 开启深度测试。
[4]您可以设置Z值与gl.depthFunc的比较方式@