不知不觉又过一个月,懒惰的作者把水文章忽略,贫乏的智商经不起数学的肆虐,发际线也在随着加班退却。读者大佬观后觉得有用还请点个赞,觉得没用就当随意看一看,作者的水平不好不烂,更新的频率也不快不慢,您要是点个关注那是千金不换。
当研发手机游戏的时候,手机硬件性能就成为了美术效果的瓶颈,当你想要一次性生成几千个模型,并且模型还需要随机旋转的时候,如果每个模型都使用CPU来处理transform信息肯定会对性能产生很大的压力。Unity的静态合批是当物体设置为静态,并且使用同一个材质(参数可以不同,但是贴图需要相同)的时候,可以执行静态合批。静态合批可以减少Draw Call,提高渲染速度。但是静态合批之后肯定是不能再旋转模型了,一个可以旋转模型的材质可以在保持静态合批的条件下,对模型进行旋转。
想要使用Shader控制模型的旋转,有两种思路,一种是构建矩阵来旋转模型,另一种是求出每个顶点和旋转轴的相对关系,然后进行旋转。
首先说一下构建矩阵进行旋转的方式。在上一篇写模型空间到世界空间的变换矩阵的时候,我有过这样的想法:模型绕xyz三个轴旋转,每个轴需要一个矩阵。那么我把这三个矩阵先相乘一下,再去做旋转,岂不是一个矩阵搞定?但是事实上一个旋转矩阵代表的几何意义是物体围绕某个轴旋转了任意角度,并不能是物体在空间中想怎么转就怎么转,它必须围绕中心轴来旋转。所以简单的把三个矩阵相乘之后得到的矩阵并不能让模型做正确的变换,会出现的结果是,当你把模型围绕xyz中的某个轴旋转时,结果是正确的,但是如果把每个轴都旋转一定角度之后,结果就是错误的了。
上面说到一个矩阵代表的只能是物体围绕某个轴进行旋转,那么想要实现物体的任意旋转,就需要定义任意轴,然后围绕这个轴进行旋转任意角度的矩阵。这个矩阵的推导过程就不写在这里了,直接引用知乎大神 chongbin li
所以一个任意旋转的矩阵是下面这样的:
这其中,n是一个三维单位向量,代表旋转轴。
在Unity中实现
根据上面这个矩阵,我们可以在Unity中写一个旋转模型的Shader,思路是把顶点位置转换到世界空间,然后减去模型的世界空间位置把模型移到世界原点,对顶点做旋转变换,然后再移回原位。当然直接在模型空间变换完成再把顶点位置转换到世界空间可以节省一点计算,但是个人习惯统一在世界空间变换顶点坐标。
Shader "Unity Shaders/Matrix Rotate Shader"
{
Properties
{
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_RV ("Rotate Vector" , Vector) = (1, 0, 0, 0)
}
SubShader
{
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
float4 _RV;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
//把顶点位置转到世界空间
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float4 RV = float4(normalize(_RV.xyz) , _RV.w);
//构建任意旋转矩阵
float sinR = sin(_RV.w);
float cosR = cos(_RV.w);
float FcosR = 1 - cosR;
float3 R1 = float3(RV.x * RV.x * FcosR + cosR , RV.x * RV.y * FcosR - RV.z * sinR , RV.x * RV.z * FcosR + RV.y * sinR );
float3 R2 = float3(RV.x * RV.y * FcosR + RV.z * sinR , RV.y * RV.y * FcosR + cosR , RV.y * RV.z * FcosR - RV.x * sinR );
float3 R3 = float3(RV.x * RV.z * FcosR - RV.y * sinR , RV.y * RV.z * FcosR + RV.x * sinR , RV.z * RV.z * FcosR + cosR );
//获取模型原点在世界空间的位置
float3 ObjectPosition = mul(unity_ObjectToWorld, float4(0.0, 0.0, 0.0, 1.0)).xyz;
//先把模型放在世界空间的原点进行旋转变换后再移动回原来的位置
worldPos -= ObjectPosition;
worldPos = float3 (dot(worldPos, R1), dot(worldPos, R2), dot(worldPos, R3));
worldPos += ObjectPosition;
//把顶点位置变换到裁剪空间
o.pos = UnityWorldToClipPos(worldPos);
//矫正法线
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
worldNormal = float3(dot(worldNormal, R1),dot(worldNormal, R2),dot(worldNormal, R3));
o.worldNormal = worldNormal;
o.worldPos = ObjectPosition;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
//使用兰伯特照明来查看效果是否正确
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * max(0, dot(i.worldNormal, lightDir)) + ambient;
diffuse *= _Color;
return float4 (diffuse,1);
}
ENDCG
}
}
Fallback "Specular"
}
这个shader的效果是这样的,可以直接在材质里调整数值来旋转模型。
在UE4中实现
在UE4中对顶点进行变换和在Unity中略有些不同,在Unity中,可以直接对顶点的位置进行各种修改,但是在UE4中需要计算好了顶点的偏移量之后,加到顶点上。如果用一个图来表示的话就是:
UE4的顶点计算很不直观,而且直接使用矩阵计算的结果输出到材质中是错误的,需要减去世界位置才行。
直接使用单个的节点连接一个矩阵出来太过复杂,让人不好观察,所以还是直接使用代码,防止连线看得人头晕眼花。这里使用UE4提供的Custom节点,把矩阵的代码复制进去就可以正常使用了。
Custom节点中的代码如下所示:
这个材质在UE4中的效果是这样的:
除了这个使用矩阵旋转模型的方式之外,还有一种更加巧妙的方式,节省了大部分计算。
这个方式也是现在UE4中的RotateAboutAxis节点所使用的方式,它的思路是,围绕输入的旋转轴为Y轴,用Cross函数计算出的垂直于它的两个向量为新坐标系的三个方向,以物体中心或者输入的位置为原点,来构建一个新的坐标系。计算出每个顶点在这个坐标系中的位置,然后对顶点的X和Y的位置进行旋转。
Shader "Unity Shaders/Rotate Shader"
{
Properties
{
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_RV ("Rotate Vector" , Vector) = (1, 0, 0, 0)
}
SubShader
{
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
float4 _RV;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
//把顶点位置转到世界空间
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
//RV的xyz分量是旋转轴,RV的w分量是旋转角度
float4 RV = float4(normalize(_RV.xyz) , _RV.w);
//获取模型在世界空间中的中心位置
float3 ObjectPosition = mul(unity_ObjectToWorld, float4(0.0, 0.0, 0.0, 1.0)).xyz;
//计算模型上的每个顶点在旋转轴上的值,相当于获取了模型每个点在新坐标系下的Y轴位置
float DotRV = dot(RV.xyz, worldPos - ObjectPosition);
//获得每个顶点在原空间下的与旋转轴的相对关系,也可以说是把顶点在新坐标系的位置变换到了原空间
float3 ClosestPointOnAxis = RV.xyz * DotRV;
//这两个值可以看作是新坐标系下点的X和Z轴位置变换到原空间下的数值
float3 UAxis = (worldPos - ObjectPosition) - ClosestPointOnAxis;
float3 VAxis = cross(RV.xyz, UAxis);
float CosAngle;
float SinAngle;
sincos(RV.w, SinAngle, CosAngle);//sincos函数的作用是输入一个浮点数,输出这个浮点数的sin和cos值
//对X和Z轴进行旋转
float3 R = UAxis * CosAngle + VAxis * SinAngle;
//把旋转后的X和Z 与Y轴结合起来,然后再移动到原来的位置
worldPos = ClosestPointOnAxis + R + ObjectPosition;
o.worldPos = worldPos;
//把顶点位置变换到裁剪空间
o.pos = UnityWorldToClipPos(lerp(worldPos,ClosestPointOnAxis, _Color.w));
//矫正法线
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
DotRV = dot(RV.xyz, worldNormal);
ClosestPointOnAxis = RV.xyz * DotRV;
UAxis = worldNormal - ClosestPointOnAxis;
VAxis = cross(RV.xyz, UAxis);
R = UAxis * CosAngle + VAxis * SinAngle;
worldNormal = ClosestPointOnAxis + R;
o.worldNormal = worldNormal;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
//使用兰伯特照明来查看效果是否正确
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * max(0, dot(i.worldNormal, lightDir)) + ambient;
diffuse *= _Color;
return float4 (diffuse,1);
}
ENDCG
}
}
Fallback "Specular"
}
其实对这个方法也可以换一种不同的思维来理解,既然我们是要围绕输入的这个轴来旋转模型,那么,我们沿着这个轴,把这个模型压平不就把问题简化成了一个二维平面的旋转了吗。如何把这个模型压平?就是通过点积计算出这个轴对每个顶点的影响数值,然后减去这个值,就把这个模型压平到垂直于旋转轴的平面上了。压平了之后,就可以通过旋转二维平面的计算方式来旋转模型了。
这个计算的代码已经封装在了UE4的RotateAboutAxis节点中,但是也可以再通过连接节点的方式来重新实现这个计算的结果。
这里实现的结果和前面是一样的。