对于本届 BOOOM,我本来觉得我其实没什么特别的经验值得分享,直到今天我发现了这个:
“探路者”这三个字一下子就戳中了我的心巴,好想得到这个徽章啊卧槽。话说这个能做实体的吗?
好吧,这个徽章我实在是太爱了,为了得到这个徽章,我决定分享一下《寄生绒毛》这个作品中用到的绒毛系统是如何制作的。
说是技术分享,其实就是把我平时的“开发日志”拿出来给大家看看,而且由于这个所谓的日志本来是我自己用来做总结记录的文字笔记,所以大部分内容看起来是不成系统不成体系的,有些甚至前言不搭后语(就我自己能看懂)。在这种情况下,我只能摘取其中比较能看的部分出来给有兴趣的朋友做参考。
还要说明的是,这段技术分享中的内容是《寄生绒毛》这个作品比较早期时的实验结果,后期做了很多调整和优化,这里只提供最基本的思路,同时,《寄生绒毛》中使用到的绒毛技术要比这段技术分享中所阐述的技术内容更加复杂,模块也更多。这段技术分享只为抛砖引玉(还有我想要的徽章),如果你也想做这么一个绒毛系统,还需付出更多努力。
正文:
1. compute shader 离线构造绒毛网格
绒毛网格的构造依赖 compute shader 的 gpu 计算,具体分为两个步骤:曲面细分(Tess),绒毛分节(Tri)
FK 项目中曲面细分这一步骤的真正实现方法其实可以被叫做暴力细分或者手工细分,是提前规划好每个三角形的细分方法,然后在每个三角形内创建规划好的顶点,并创建顶点索引形成新三角形。
上图中,左侧为原始网格,右侧为 Tess2 方法细分后的网格
绒毛分节指的是在每个三角形内最少创建一个新的顶点和三个新的三角形,完成后的网格只要拉高这个新创建的顶点就能构造一个三棱锥,我们用这个三棱锥来模拟一根绒毛的形态。为了使绒毛看起来更自然,就势必要使绒毛能够随着外力而表现出抖动效果。为了使抖动效果更自然,就要将每根绒毛分为更多的段,段数越多,绒毛抖动的效果模拟的就越自然。
图中为被分为两节的三棱锥,用以模拟绒毛效果
上图左侧为原始网格,右侧为 Tri2 方法形成的被分为两节的三棱锥
制作绒毛网格时,先用 Tess 做细分,再用 Tri 做绒毛分节,这会产生大量顶点。这时就会遇到网格顶点数限制问题。这个问题体现在两个方面,第一个方面是我们在使用 Tess 和 Tri 方法后,产生的大量顶点因为网格顶点数限制而无法成功被保存为新的网格。第二个问题是即使我们使用一个网格来保存和展示拥有大量顶点的绒毛,那么后面要做绒毛剪切效果时,每次检测都要检测所有顶点,而不能简单的只检测固定位置周边的少数顶点,除非引入更复杂顶点裁剪再做检测。
为了解决这两个问题,这里的做法是将一个球分割成多个块。然后为每个块做 TessTri 操作,再将块拼接成新的球。这样每个小块都拥有大量绒毛,效果看起来会比较逼真。不过这个做法也要引入新的网格制作流程。
首先在 blender 中制作球体网格我们面临着两个选择:uv sphere 和 quad sphere。
左:uv sphere 右:quad sphere
从动图中可以看到两种 sphere 在上述制作绒毛的流程中产生的两种绒毛的效果区别。uv sphere 产生的绒毛效果主要问题来自它 uv 展开方式和它所携带的三角形的分布。
uv sphere 做 uv 展开非常容易和规整:
针对FK 项目应用场景 uv sphere 有两个缺点:
1. 首先是这种做法网格被分割的块大小差别比较大,这就导致我们上述制作绒毛网格流程会产生密度差别较大绒毛块。
2. 其次就是 uv sphere 产生的绒毛在视觉上显得非常不均匀,绒毛的形态和分布具有规律性(一圈一圈的)
3. 最后就是贴图不连续导致的撕裂问题。贴图撕裂会影响所有依赖贴图的效果,比如我们只用噪点贴图模拟风吹动绒毛的效果时,由于噪点贴图也会被撕裂,所以我们可以观察到风作用在绒毛上的效果也会有很明显的撕裂。
与 uv sphere 相比 quad sphere 与 FK 项目的适配性更强。下面是 quad sphere 在绒毛生成流程中的制作要点:
1.在对 quad sphere 做 uv 展开时,要以半球为单位展开,并且保证两个半球的 uv 在最外圈的部分取值相同。实现方式是上下翻转 uv 并且对齐贴图:
2.如上述绒毛制作流程中所述,我们要将球体分为很多小块,在 blender 对球体做分割后,每个小块的边缘顶点法线也会产生偏移,如果不做处理,那么制作出来的绒毛在长度不为 0 时,就会因为生长方向产生问题:
右侧图中可以看到没有对齐法线导致的空洞
对齐法线的做法是,在 blender 中分割球体前,保留原始网格,原始网格保有原始法线。在分割后先将所有子块设置 auto smooth,然后为每个子块添加 Normal Editor 修改器,target 选择原始网格,这样就可以将原始网格的法线复制到对应的子块中。
右侧为法线对齐后的示意图
2. vertex shader 模拟绒毛
FK 项目中绒毛的模拟是使用 vertex shader 来实现的,同时实现的效果依赖于 compute shader 制作绒毛网格时,计算并存储在顶点法线和 uv 中的值,具体规则如下:
1.绒毛(三棱锥)的最高点顶点(后称 top 顶点)法线为所在三角形顶点的法线的平均值
2.绒毛的节点的所有顶点(后称节点顶点)的法线为所属绒毛的 top 顶点法线,这样能保证在 vs 中做生长操作时,构成一根绒毛的所有顶点(一个 top 顶点 + 多个节点顶点)都有相同的生长方向
3.保证了绒毛生长方向后,还要保证绒毛节点顶点的生长长度和节点顶点所处位置保持一致。例如一根绒毛有两个节点,如果生长长度为 1,那么最下面的节点顶点要生长 1 * 0.33,以此类推。有两种做法可以达成这个目的,一是节点顶点法线(后称节点法线)在存储时,要乘以与节点位置相应的系数,二是单独将这个系数保存在 uv 中。由于这个系数还要在其他方法中使用,所以为了方便,当前实现是直接保存在 uv2.y 中。
4. compute shader 制作绒毛网格时并不知道绒毛生长的全部信息(除了方向和节点顶点系数),FK 项目目前也没有将绒毛模拟和生成统一到 compute shader 中,所以如 3 所述节点法线信息存储的并不是真实的法线,而是生长方向。这就要求我们要在 vs 中重新计算节点法线来保证渲染正确。为了计算节点法线,我们需要在生成绒毛的 Tri 阶段中,保存与节点相连的绒毛的根顶点的位置,这个值存储在 uv1 + uv2.x 中。
5. 节点和 top 顶点的 uv3 存储了当前绒毛在所有绒毛中的索引值,在绒毛的剪切效果实现中,要剪切的绒毛位置会被 compute shader 计算并存储在 RT 中,存储时用来标记存储位置的 uv 就是这个 uv3 中的值。vs 每个顶点都使用 uv3 读取这个 RT,读取到的值,会限制该顶点的生长长度,从而达到绒毛被剪切了一部分长度的效果。
3. 绒毛的裁剪效果
首先,一个绒毛是否被裁剪,取决于该绒毛的 top 顶点是否在裁剪的范围内,和其他顶点无关。其次,哪些绒毛会被裁剪可以通过动态绘制纹理的方式来记录。
结合上述两点我们得知,动态纹理要填充的值是 top 顶点被裁剪的程度值,top 顶点在动态纹理中什么位置存储这个值,取决于 top 顶点的索引。
这个索引如何计算?
使用 Tess4 + Tri3 生成的每个子块大概包含 4000 个 top 顶点,一个完成的球包含 24 个这种程度的子块。也就是说全部 top 顶点数量不会超过 10万。一个大小为 512 * 512 的纹理包含 26万+ 像素,完全满足每个像素用来存储一个 top 顶点的索引值的需求。在 compute shader 生成每个子块时,都传入当前已经生产的所有子块所包含的 top 顶点总数,然后 top 顶点在这个总数上计算一个自己的二维索引值(相当找到自己在一个 512 * 512 表格中的位置),并将其存储在 uv3 中。当前 top 顶点相关的节点顶点保持和这个 top 顶点一致的 uv3 值,vs 中使用 uv3 读取这个动态纹理中的值,top 顶点和节点顶点就能计算出自己被裁剪的值是多少。
在上述这个过程中,纹理存储的值是一个用来计算的确定值,所以就必须要保证这个纹理被读取时,所要读取的值就是我们计算并存储的那个值。那么纹理中存储的值在被读取时,会因为哪些影响而变化?
1.sRGB
2.mipmap
3.各向异性
4.纹理过滤,过滤器要选择 point,否者读取的值会被过滤器再计算,例如和临近像素进行插值等
5.存储时,计算出的 uv 值会被纹理大小和 float 精度影响。512 * 512 大小的纹理,1 纹素 uv = 1 / 512 = 0.001953125。满足 float 精度,如果是 1024 大小纹理计算 uv 就可能会有问题了
在 FK 实现中,这个动态裁剪位置标记纹理还要用来决定被裁剪的位置的颜色,在 fs 中采样这个纹理时要使用非 point 过滤器,这样才能保证颜色值正确,因为 fs 中的 uv 值是被插值后的,使用 point 过滤器会导致最终颜色来回闪烁。并且,出于同样的原因,动态裁剪位置标记纹理要使用 1024 * 1024,否则其他绒毛渲染颜色时,也会被标记裁剪(虽然不影响长度):
图中可以看出,除了车子前进轨迹外,其他部分绒毛也有变色