自更新纹理
如果可以并行化模拟或渲染任务,通常最好在GPU中运行它们。 在本文中,我将解释一种技术,该技术利用这一事实来以低性能开销创建令人印象深刻的视觉技巧。 我将演示的所有效果都是使用纹理实现的,纹理在更新后会“
渲染”自身 ; 当渲染新框架时,纹理将更新,并且下一个纹理状态完全取决于前一个状态。 在这些纹理上,您可以绘制并引起某些更改,并且纹理本身可以直接或间接地用于渲染有趣的动画。 我称它们为
卷积纹理 。
图1:卷积双缓冲在继续之前,我们需要解决一个问题:纹理不能同时读写,OpenGL和DirectX等图形API不允许这样做。 由于纹理的下一个状态取决于上一个状态,因此我们需要以某种方式解决此限制。 我需要从一种不同的纹理中读取内容,而不是从我正在撰写的内容中读取。
解决方案是
双重缓冲 。 图1显示了它是如何工作的:实际上,有两种纹理而不是一种纹理,但是其中一种被写入而另一种被读取。 正在写入的纹理称为
后缓冲区 ,渲染的纹理称为
前缓冲区 。 由于卷积测试是“写入自身”的,因此每个帧中的辅助缓冲区都将写入主缓冲区,然后渲染或将其用于渲染。 在下一帧中,角色更改,并且先前的主缓冲区用作下一主缓冲区的源。
通过使用片段着色器(或
像素着色器 )将先前的状态渲染为新的卷积纹理,可以提供有趣的效果和动画。 着色器确定状态如何改变。 文章的所有示例(以及其他示例)的源代码都可以
在GitHub的
存储库中找到 。
简单的应用示例
为了演示该技术,我选择了一个著名的模拟,其中在更新时,状态完全取决于先前的状态:
Conway游戏“ Life” 。 此仿真是在正方形的网格中执行的,每个正方形的单元都处于活动状态或死亡状态。 以下单元状态的规则很简单:
- 如果一个活细胞的邻居少于两个,那么它就会死掉。
- 如果一个活细胞有两个或三个活着的邻居,它仍然活着。
- 如果一个生物细胞有三个以上的生物邻居,那么它就会死亡。
- 如果一个死细胞有三个活着的邻居,它就会活着。
为了将该游戏实现为卷积纹理,我将纹理解释为游戏的网格,然后着色器根据上述规则进行渲染。 透明像素是一个死细胞,而白色不透明像素是一个活细胞。 交互式实现如下所示。 要访问GPU,我使用
myr.js ,它需要
WebGL 2 。 大多数现代浏览器(例如Chrome和Firefox)都可以使用它,但是如果演示不起作用,则很可能浏览器不支持它。 使用鼠标(或触摸屏)[在原始文章中]在纹理上绘制活细胞。
片段着色器代码(在GLSL中,因为我使用WebGL进行渲染)如下所示。 首先,我实现了
get
函数,该函数允许我从当前偏移量的特定偏移量读取像素。
pixelSize
变量是一个预先创建的2D向量,其中包含每个像素的UV偏移量,并且
get
函数使用它来读取相邻的单元格。 然后,
main
功能根据当前状态(
live
)和居住的邻居数确定单元格的新颜色。
uniform sampler2D source; uniform lowp vec2 pixelSize; in mediump vec2 uv; layout (location = 0) out lowp vec4 color; int get(int dx, int dy) { return int(texture(source, uv + pixelSize * vec2(dx, dy)).r); } void main() { int live = get(0, 0); int neighbors = get(-1, -1) + get(0, -1) + get(1, -1) + get(-1, 0) + get(1, 0) + get(-1, 1) + get(0, 1) + get(1, 1); if (live == 1 && neighbors < 2) color = vec4(0); else if (live == 1 && (neighbors == 2 || neighbors == 3)) color = vec4(1); else if (live == 1 && neighbors == 3) color = vec4(0); else if (live == 0 && neighbors == 3) color = vec4(1); else color = vec4(0); }
另一个简单的卷积纹理是
带有落沙的
游戏 ,其中用户可以在场景中扔出五颜六色的沙子,然后掉落并形成山脉。 尽管其实现稍微复杂一些,但规则更简单:
- 如果一粒沙子下面没有沙子,那么它会掉落一个像素。
- 如果沙粒下有沙子,但它可以向左或向右滑动45度,则可以这样做。
此示例中的管理与“生活”游戏中的管理相同。 由于在这样的规则下,沙子可能会以每帧仅一个像素的速度掉落,从而稍微加快了处理速度,因此每帧的纹理更新了3次。 该应用程序的源代码在
此处 。
前进了一步
图2:像素波。上面的例子直接使用了卷积纹理。 其内容按原样呈现在屏幕上。 如果仅将图像解释为像素,则此技术的使用范围非常有限,但是由于有了现代化的设备,它们可以扩展。 除了将像素计算为颜色之外,我将对它们的解释有所不同,这可以用于创建具有另一个纹理或3D模型的动画。
首先,我将卷积纹理解释为高度图。 纹理将模拟水平面上的
波浪和
振动 ,并且结果将用于渲染反射和阴影波浪。 我们不再需要将纹理作为图像读取,因此我们可以使用其像素存储任何信息。 对于水着色器,我将在红色通道中存储波高,在绿色通道中存储波脉冲,如图2所示。蓝色和alpha通道尚未使用。 通过在卷积纹理上绘制红色斑点来创建波浪。
我不会考虑从
Hugo Elias网站借来的高度图的更新方法,该方法似乎已从Internet上消失了。 他还从一位不知名的作者那里学习了这种算法,并用C语言实现了该算法,以便在CPU中执行。 以下应用程序的源代码在
此处 。
在这里,我仅使用高度图来偏移纹理并添加阴影,但是在第三维中,可以实现更多有趣的应用程序。 当顶点着色器解释卷积纹理时,可以细分平坦的细分平面以创建三维波。 您可以对生成的形状应用通常的阴影和照明。
值得注意的是,上面显示的示例的卷积纹理中的像素有时会存储非常小的值,由于舍入误差而不会消失。 因此,此纹理的颜色通道应具有更高的分辨率,而不是标准的8位。 在此示例中,我将每个颜色通道的大小增加到16位,这给出了相当准确的结果。 如果您不存储像素,则通常需要提高纹理的准确性。 幸运的是,现代图形API支持此功能。
我们使用所有渠道
图3:像素草。在水示例中,仅使用了红色和绿色通道,但是在下一个示例中,我们将应用所有这四个通道。 模拟有草(或树木)的字段,可以使用光标移动该字段。 图3显示了哪些数据存储在一个像素中。 偏移量存储在红色和绿色通道中,速度存储在蓝色和alpha通道中。 该速度会更新,以随着逐渐减弱的波动向静止位置移动。
在以水为例的示例中,创建波浪非常简单:可以在纹理上绘制斑点,并且alpha混合可以提供平滑的形状。 您可以轻松创建多个重叠点。 在此示例中,所有事情都比较棘手,因为alpha通道已在使用中。 我们不能在中心绘制一个alpha值为1,从边缘开始的alpha值为0的斑点,因为这会给草带来不必要的脉冲(因为垂直脉冲存储在alpha通道中)。 在这种情况下,编写了单独的着色器以在卷积纹理上绘制效果。 此着色器可确保Alpha混合不会产生意外的效果。
该应用程序的源代码可以在
这里找到。
Grass是在2D模式下创建的,但是效果将在3D环境下起作用。 顶点移动而不是像素位移,这也更快。 此外,借助山峰,还可以实现另一种效果:树枝的强度不同-草在最小的风中容易弯曲,而强壮的树木仅在暴风雨期间波动。
尽管有许多算法和着色器可用来创建风和植被位移的影响,但这种方法具有很大的优势:在卷积纹理上绘制效果是非常低成本的过程。 如果在游戏中应用了效果,那么植被的运动可以通过数百种不同的影响来确定。 不仅主角而且所有物体,动物和动作都可以以微不足道的代价牺牲对世界的影响。
其他用例和缺陷
您可以提出许多其他技术应用程序,例如:
- 使用卷积纹理,您可以模拟风速。 在纹理上,您可以绘制使空气围绕它们的障碍物。 粒子(雨,雪和树叶)可以使用此纹理在障碍物周围飞行。
- 您可以模拟烟雾或火势的蔓延。
- 纹理可以编码雪或沙层的厚度。 痕迹和与图层的其他交互作用会在图层上产生凹痕和印迹。
使用此方法时,存在困难和局限性:
- 很难根据不断变化的帧频调整动画。 例如,在有落沙的应用中,沙粒以恒定速度落下-每次更新一个像素。 一种可能的解决方案是以恒定频率更新卷积纹理,类似于大多数物理引擎的工作方式。 物理引擎以恒定频率运行,并对其结果进行插值。
- 将数据传输到GPU是一个快速简便的过程,但是,将数据取回并不是那么容易。 这意味着该技术产生的大多数效果都是单向的; 它们被转移到GPU,GPU无需进一步干预和反馈即可完成其工作。 如果我想将水示例中的波长嵌入物理计算中(例如,以便使船只随波一起振荡),那么我将需要卷积纹理中的值。 从GPU检索纹理数据是一个非常缓慢的过程,不需要实时进行。 解决该问题的方法可以是两种仿真的实现:一种对水图形进行卷积纹理具有高分辨率,另一种对CPU的水物理学具有较低的分辨率。 如果算法相同,则差异可能是可以接受的。
本文中的演示可以进一步优化。 在草示例中,可以使用分辨率低得多的纹理而没有明显的缺陷。 在大型场景中这将有很大帮助。 另一个优化:您可以使用较低的刷新率,例如,每四帧或每帧四分之一(因为此技术不会导致分段更新出现问题)。 为了保持平滑的帧速率,可以插值卷积纹理的先前状态和当前状态。
由于卷积纹理使用内部双缓冲,因此您可以同时使用两个纹理进行渲染。 主缓冲区是当前状态,辅助缓冲区是前一个状态。 这对于随时间插值纹理或计算纹理值的导数可能很有用。
结论
GPU(尤其是2D程序中的GPU)通常处于空闲状态。 尽管它似乎只能用于渲染复杂的3D场景,但本文中演示的技术还显示了至少另一种使用GPU的功能。 使用开发GPU的功能,您可以实现通常对于CPU来说过于昂贵的有趣效果和动画。