希望这篇小文对你有所帮助。它概述了我在WebGL和3D方面所学到的大部分内容。顺便说一句,如果我有任何错误,请有人纠正我,因为我也还在学习!
架构
浏览器只是一个Web浏览器。它所做的就是通过JavaScript公开WebGL API,程序员使用API完成其他所有操作。
据我所知,WebGL API本质上只是一组(由浏览器提供的)JavaScript函数,其包装OpenGL ES规范。因此,如果您了解OpenGL ES,则可以很快地采用WebGL。不要将其与纯OpenGL混淆,"ES"很重要。
WebGL规范故意保持非常低级,使得大量实现需要在不同应用程序之间重新实现。编写自动化框架取决于社区,选择使用哪个框架(如果有的话)则取决于开发人员。自己编写不是很难,但这意味着要花费大量的开销来重新发明轮子。(我的
WebGL框架Jax已经开发了一段时间了。)
图形驱动程序提供了OpenGL ES的实现,它实际上运行您的代码。此时,它在机器硬件上运行,甚至低于C代码。虽然这是使WebGL成为可能的原因,但这也是一把双刃剑,因为OpenGL ES驱动程序中的错误(我已经注意到了相当多)将显示在您的Web应用程序中,并且您不会知道,除非您可以指望您的用户群体提交包括操作系统、视频硬件和驱动程序版本在内的连贯错误报告。
这里是这些问题的调试过程。
在Windows上,WebGL API和硬件之间存在一个额外的层:
ANGLE或“Almost Native Graphics Layer Engine”。由于Windows上的OpenGL ES驱动程序通常很糟糕,所以ANGLE接收这些调用并将它们转换为DirectX 9调用。
在三维空间中绘制
现在您知道了这些组件是如何组合在一起的,让我们来看看更低级别的解释,说明所有内容是如何组合在一起生成3D图像的。
JavaScript
首先,JavaScript代码从HTML5
canvas元素中获取一个3D上下文。然后,它注册了一组着色器,这些着色器是用GLSL([Open] GL Shading Language)编写的,基本上类似于C语言。
其余的过程非常模块化。您需要使用在着色器中定义的uniform和attribute将顶点数据和任何其他要使用的信息(例如顶点颜色、纹理坐标等)传送到图形管线中,但是这些信息的确切布局和命名取决于开发人员。
JavaScript设置初始数据结构并将其发送到WebGL API,该API将其发送到ANGLE或OpenGL ES,最终将其发送到图形硬件。
顶点着色器
一旦着色器可以使用这些信息,着色器必须经过两个阶段来转换信息以生成3D对象。第一阶段是顶点着色器,它设置网格坐标。(此阶段完全在视频卡上运行,在上述所有API之下。)通常,顶点着色器执行的过程看起来像这样:
gl_Position = PROJECTION_MATRIX * VIEW_MATRIX * MODEL_MATRIX * VERTEX_POSITION
其中VERTEX_POSITION
是一个四维向量(x、y、z 和 w,通常设置为 1);VIEW_MATRIX
是代表相机视角的4×4矩阵;MODEL_MATRIX
是一个4×4矩阵,将对象空间坐标(即,在应用旋转或平移之前与对象本地相关的坐标)转换为世界空间坐标;PROJECTION_MATRIX
代表相机的透镜。
通常情况下,
VIEW_MATRIX
和
MODEL_MATRIX
会被预先计算并称为
MODELVIEW_MATRIX
,有时候三者都会被预先计算到
MODELVIEW_PROJECTION_MATRIX
或
MVP
中。虽然这些通常被认为是优化,但我希望有时间进行一些基准测试。如果每帧都这样做,那么在 JavaScript 中进行预计算实际上可能比较慢,因为 JavaScript 本身并不是很快。在这种情况下,通过 GPU 上的数学计算获得的硬件加速可能比在 JavaScript 中使用 CPU 更快。当然,我们可以希望未来的 JS 实现将通过更快地运行来解决这个潜在的问题。
剪裁坐标
应用所有这些后,
gl_Position
变量将具有一组 XYZ 坐标,在 [-1,1] 范围内,并具有 W 分量。这些被称为剪裁坐标。
值得注意的是,剪辑坐标是顶点着色器真正需要生成的唯一内容。只要你产生了剪辑坐标结果,就可以完全跳过上面执行的矩阵变换。(我甚至尝试过用四元数替换矩阵;它能正常工作,但我放弃了这个项目,因为我没有获得我所希望的性能改进。)
在将剪辑坐标提供给
gl_Position
之后,WebGL通过
gl_Position.w
除以结果,从而产生所谓的归一化设备坐标。从那里,将像素投影到屏幕上只需要乘以1/2的屏幕尺寸,然后加上1/2的屏幕尺寸即可。
[1]以下是一些将剪辑坐标转换为800x600显示器上的二维坐标的示例:
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]您可以使用gl.depthFunc
设置如何比较Z值。