1. 概论
mesh的顶点可定义属性,比较常见的自定义通道是顶点色color和uv3 uv4及以上,因为一般uv1是贴图坐标,uv2是lightmap坐标,所以不太考虑这俩。那就这些我们就已经有 3 * 4 = 12个通道了。这里面可以存的数据一般包括:
- 运动强度 1个频率1个通道就够了
- 运动相位,防止运动sync,取决于有几层相位叠加了,1个频率1个通道
- 位置,意义上可能是运动的目标位置,或者LOD的位置,这个一般需要比较精确还不能压缩,肯定要3个通道了
- 方向,意义上可以是运动方向,法线等很多,这个一般不用太精确,如果压缩的话3个通道,不压缩的话甚至pack成一个通道都可以
- 其它gameplay相关的参数了
数据存在顶点上可能的问题是,模型一压缩,数据就坏掉了。另外毕竟顶点上支持的数据套数比较少。因此比较高精度和大数据量的数据存在贴图上可能更有效。要注意的是前一条理由对移动平台不一定有用,unity在ios/android只支持到RGBA32,可能还没顶点的精度高。数据存贴图的话用uv访问就好了,这样uv的数据一般不用很多套了。
运动的函数最常见的是三角函数,一般来讲几个频率的三角函数叠加足够满足需求了,但是有时也有一些特殊波形要求比如Gertsner尖波(海)。甚至有时可能还要把参数暴露出来,用手k的动画曲线来设置。
Animating With Math讲的便是相当基本的顶点动画操作,以下用一些实例分析
2. Houdini烘焙顶点动画的原理
烘出来模型,会发现UV2在uv空间的最上面一排,烘出来的贴图排列横向x坐标是vertex的id,竖向是帧数。所以取贴图的坐标就是
float4 texturePos = tex2Dlod(_posTex,float4(v.texcoord1.x, (timeInFrames + v.texcoord1.y), 0, 0));
这个坐标是是boundingbox内normalize出来的,shader会有boundingbox_min和boundingbox_max的参数
因此动画里的坐标就是
1 | float expand = _boundingMax - _boundingMin |
有一点问题是有可能烘焙的动画帧数不高,而且移动平台上只能RGBA32,如果boundingbox很大的话卡顿感会比较严重,就需要shader里自己做插帧了
3. SpeedTree的风
SpeedTree除了制作速度比较快外,就是风的效果比较好了,看unity的shader基本就能明白原理
以下是几个texcoord的定义,
1 | // texcoord setup |
uv1是lod的位置,用来做lod的过渡
树枝走BranchWind,
branch wind xy x记录风的大小fWeight,一个记录风的法线fOffset,这个是pack起来的法线,这么看这个通道至少12bit了,压缩可能会傻掉
1 | float3 UnpackNormalFromFloat(float fValue) |
按枝条的法线做Occilation运动,occilation是叠加了不同相位的sin
树叶走LeafWind,uv2的xyz存的是anchor,也就是一团树叶的锚点;uv3分别是fScale, fPackedGrowthDir(只对best有用), fPackedRippleDir
然后按波动方向做一个sin近似运动
之后还有一个整体的GlobleWind,按全局风方向做Occilation
代码里用了一个三角函数的近似计算,和三角函数比起来比较接近
1 | float4 CubicSmooth(float4 vData) |
主要读SpeedTreeWind.cginc, SpeedTreeVertex.cginc就好了
4. Fornite物体的出现动画
在vertex shader里hardcode了一条曲线,0对应物体被裁剪时候的缩放,1对应物体完全出现时候的缩放,这样物体出现时渐变的过程会是先变大到1.25倍再缩小的过程
shader很简单,拟合一条曲线就好了,然后外部LOD组件传入一个状态切换的alpha值
1 | float cull = phase > 0.75f ? 2.0f - phase : 1.667f * phase; |
5. Fornite的可破坏墙体
Shader需要暴露一个参数驱动破坏过程,左侧时0,右侧是1.
顶点或者贴图上存储的数据包括:
每一个木片都需要存储pivot的位置,三个通道(local位置用v.vertex - pivot),这样才能单块木片整体移动
一个随机数,每个木片从同一个数值就好,一个通道就可以了,甚至不用8bit,4个bit一般就够了
需要一个动画顺序,每个木片同一个数值,这样整体参数0-1时才能知道哪个木片可以动了,可以直接用存储的随机数代替
需要一个旋转轴,木片绕它旋转,如果能pack起来一个通道就够了,也可以预先存一个数组,用随机数来取
需要flight path,这个表示超前飞还是朝后飞,1bit,这个其实也不用,完全可以从随机数构造出来。
需要木块的数量,这个可能不是存顶点上的。
这么算下来只需要定点色就够了,构成是RGB时pivot的坐标,A是随机数,其他参数都用随机数构造就好了。
笔者写了个伪码示意:
1 | float animatePhase = saturate((_Phase - i.color.a) * 10.0f); // 一个magicnumber,用以缩放动画的长度 |
参考资料
Animating With Math, GDC Vault 2016, Natalie Burk
The Inner Workings of Fortnite’s Shader Based Procedural Animations, GDC 2013, Jonathan Lindquist
http://mehm.net/blog/?p=1246 Reimplementation of Fortnite’s Shader Based Procedural Animations