看了GDC关于ABZU的演讲,感觉很受启发,决定在Unity中实现其中讲到的鱼群的渲染。
1、用顶点动画代替骨骼节点
首先是一个基础的shader,这里我写了一个支持主贴图和法线贴图的只有漫反射的shader 大概就是这样:
Shader "FishTest/FishShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
[NoScaleOffset]_NormalMap("NormalMap",2D)="bump"{}
}
SubShader
{
Tags { "RenderType"="Opaque"}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f{
float4 pos:SV_POSITION;
float3 lightDir:TEXCOORD0;
half2 uv:TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalMap;
v2f vert(appdata_tan v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
TANGENT_SPACE_ROTATION;
o.lightDir=mul(rotation,ObjSpaceLightDir(v.vertex));
return o;
}
fixed4 frag(v2f i):SV_TARGET{
float3 normal=normalize(UnpackNormal(tex2D(_NormalMap,i.uv)));
float3 lightDir=normalize(i.lightDir);
float3 albedo=tex2D(_MainTex,i.uv).rgb;
float3 diffuse=albedo*_LightColor0*(saturate(dot(normal,lightDir))*0.5+0.5);
float3 finalCol=diffuse+UNITY_LIGHTMODEL_AMBIENT.rgb;
return float4(finalCol,1);
}
ENDCG
}
}
}
因为我们做的是顶点动画,所以实际上只需要偏移顶点,无需多余的修改,所以我们创建一个单独的方法来完成顶点动画。
float4 VertexAni(float3 vertex){
return float4(vertex,1);
}
然后在顶点函数中将顶点动画应用至顶点
v.vertex=VertexAni(v.vertex);
现在我们的鱼长这样
因为我们的计算堵在模型的顶点空间中,所以注意一下鱼的顶点空间坐标,如果你的模型的顶点空间不一致,要做相应的更改,这里我们的x轴为鱼身长度。
接下来就是在VertexAni实现顶点动画了,演讲中鱼游动的动画有四个部分组成
第一部分 鱼游动时的左右移动
这一步很简单,就是z轴的左右偏移。我们sin和时间变量实现,
首先设置相关的属性,
Properties
{
...
_Speed("游动速度",Float)=1
_MoveOffset("左右移动幅度",Range(0,30))=0
}
将上面这些值添加至pass中后修改VertexAni
float4 VertexAni(float3 vertex){
float time=_Time.y*_Speed;
float MoveOffset=sin(time)*_MoveOffset;
vertex.z+=MoveOffset;
return float4(vertex,1);
}
当当!现在我们的鱼学会了左右横跳。
第二部分 鱼游动时的左右旋转
这个部分也简单,主要就是需要生成旋转矩阵,我们先写一个生成旋转矩阵的方法,把它放在CGINCLUDE块里
CGINCLUDE
float3x3 AngleAxis3x3(float angle, float3 axis)
{
float c, s;
sincos(angle, s, c);
float t = 1 - c;
float x = axis.x;
float y = axis.y;
float z = axis.z;
return float3x3(
t * x * x + c, t * x * y - s * z, t * x * z + s * y,
t * x * y + s * z, t * y * y + c, t * y * z - s * x,
t * x * z - s * y, t * y * z + s * x, t * z * z + c
);
}
ENDCG
添加对应的着色器属性。
Properties
{
...
_RoaAngle("整体(Y轴)旋转幅度(弧度制)",Range(0,2))=0
}
然后再次修改VertexAni,要记得先旋转再位移,这样才能确保旋转是绕鱼的中心旋转。
float4 VertexAni(float3 vertex){
float time=_Time.y*_Speed;
//2.旋转鱼身 扰y旋转
float3x3 roaMat=AngleAxis3x3(sin(time)*_RoaAngle,float3(0,1,0));
vertex=mul(roaMat,vertex);
float MoveOffset=sin(time)*_MoveOffset;
vertex.z+=MoveOffset;
return float4(vertex,1);
}
OK,现在我们的鱼学会了摇头
第三部分 脊椎方向的旋转
鱼类作为柔韧性杠杠的生物,我们的鱼当然不可以这么僵硬。下面两步就是让鱼变得更加柔软
着色器属性,这里的相位就是正弦函数中的那个相位,因为我想不到好的词描述这个值,值越大,它的波动越频繁
Properties
{
...
_Roa2Angle("身体旋转幅度(弧度制)",Range(0,2))=0
_Roa2Phase("身体(X轴)旋转相位",Range(5,50))=10
}
float4 VertexAni(float3 vertex){
float time=_Time.y*_Speed;
//2.旋转鱼身 扰y旋转
float3x3 roaMat=AngleAxis3x3(sin(time)*_RoaAngle,float3(0,1,0));
//3. 鱼身的x轴旋转
float3x3 roaMat2=AngleAxis3x3(sin(time+vertex.x/_Roa2Phase)*_Roa2Angle,float3(1,0,0));
vertex=mul(roaMat2,mul(roaMat,vertex));
float MoveOffset=sin(time)*_MoveOffset;
vertex.z+=MoveOffset;
return float4(vertex,1);
}
效果如下,看起来有点沙雕。
第四部分 脊椎方向的位移
不知道怎么解释这种移动,直接看后面的图吧
Properties
{
_OffsetAmplitude("身体摇摆振幅",Range(0,15))=0
_OffsetPhase("身体摇摆相位",Range(5,50))=10
}
float4 VertexAni(float3 vertex){
float time=_Time.y*_Speed;
//2.旋转鱼身 扰y旋转
float3x3 roaMat=AngleAxis3x3(sin(time)*_RoaAngle,float3(0,1,0));
//3. 鱼身的x轴旋转
float3x3 roaMat2=AngleAxis3x3(sin(time+vertex.x/_Roa2Phase)*_Roa2Angle,float3(1,0,0));
vertex=mul(roaMat2,mul(roaMat,vertex));
loat MoveOffset=sin(time)*_MoveOffset;
//4.随鱼身的移动偏移,z向
float MoveOffset2=sin((vertex.x)/_OffsetPhase+time)*_OffsetAmplitude;
vertex.z+=MoveOffset+MoveOffset2;
return float4(vertex,1);
}
第五部分 旋转蒙版
综合前四部分的动画你会得到这样一个效果
说不出的诡异感,我们的鱼头应该要比鱼身硬,所以我们要用蒙版撤销掉鱼头的旋转动画,我们依距x值来获取蒙版值,为了方便设置,我们添加一个选项来显示蒙版值。所以添加下列的着色器属性
Properties
{
...
[Toggle(_DEBUG_CHECK_MASK)]_CheckMask("查看蒙版",Float)=0
_Roa2Mask("身体旋转蒙版",Float)=0
}
获取蒙版值的方法如下,过渡值可暗实际情况调整,因为我这个模型有点大,所以值设为50。
float GetMask(float vertexX){
return smoothstep(vertexX,vertexX+50,_Roa2Mask);
}
接下来就是按照蒙版值在旋转和不旋转的顶点之间插值。
float4 VertexAni(float3 vertex){
float time=_Time.y*_Speed;
//2.旋转鱼身 扰y旋转
float3x3 roaMat=AngleAxis3x3(sin(time)*_RoaAngle,float3(0,1,0));
//3. 鱼身的x轴旋转
float3x3 roaMat2=AngleAxis3x3(sin(time+vertex.x/_Roa2Phase)*_Roa2Angle,float3(1,0,0));
//vertex=mul(roaMat2,mul(roaMat,vertex));
//5.蒙版
float mask=GetMask(vertex.x);
vertex=lerp(vertex,mul(roaMat2,mul(roaMat,vertex)),mask);
float MoveOffset=sin(time)*_MoveOffset;
//4.随鱼身的移动偏移,z向
float MoveOffset2=sin((vertex.x)/_OffsetPhase+time)*_OffsetAmplitude;
vertex.z+=MoveOffset+MoveOffset2;
return float4(vertex,1);
}
为了方便设置蒙版我们设置一个debug模式以观察蒙版。
因为我们只在编辑室才需要这个显示蒙版的着色器变体,所以将关键字设为着色器特性。
#pragma shader_feature _DEBUG_CHECK_MASK
接着我们要将mask值传给片元函数
struct v2f{
...
#ifdef _DEBUG_CHECK_MASK
float MaskCol:TEXCOORD2;
#endif
};
然后修改顶点和片元函数
v2f vert(appdata_tan v){
...
#ifdef _DEBUG_CHECK_MASK
o.MaskCol=GetMask(v.vertex.x);
#endif
...
}
fixed4 frag(v2f i):SV_TARGET{
...
#ifdef _DEBUG_CHECK_MASK
finalCol=i.MaskCol;
#endif
...
}
这样我们就可以查看蒙版了
综上,最终的动画如下
2、用网格实例化渲染鱼群
因为我们的鱼群使用同一个网格,所以我们可以使用实例化技术,大量的鱼的实例共用同一个网格,这里我们不考虑阴影和多光源的实例化,所以实现方式很简单。
#pragma multi_compile_instancing
首先添加一个多重编译指令,Unity会检查你的shader是否支持实例化,如果是,那么材质面板会出现这样一个toggle。
接下来就是让shader支持实例化
先是输出结构,添加一个宏
struct v2f{
UNITY_VERTEX_INPUT_INSTANCE_ID
...
};
然后是顶点函数
v2f vert(appdata_tan v){
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
...
}
好了,接着我生成了2000条鱼,并开启实例化。
PS:虽然的确的确减了draw call,但是我开启和关闭实例化都是70多帧,不知道为什么- -|||
3、鱼群的整体游动
todo ...演讲最后有人问演讲者有关于鱼群游动的行为,演讲者提到每条鱼都会根据周围的鱼进行判断。emmm,先挖个坑吧