Unity 大规模鱼群的渲染

看了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,先挖个坑吧

 

  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值