C for Graphic:语言(插值动作②)
紧接上一篇:https://blog.****.net/yinhun2012/article/details/82835549
上一篇我们为学习插值动作原理做了准备,也就是创建了需要进行插值补间的三个角色网格动作(站立、拍翅、收翅),这一篇我们就来讨论如何将这些网格动作处理成shader能使用的数据,同时使用权重weight值进行动作之间的插值补间,最后输出成完整的角色动作。
顺便提醒一下,看这篇之前必须先看上一篇,是一个连贯的篇章。
CG shader数据传递我们前面也说过,就是用Properties{}封住数据传递,这里我又要详细展开讲解一下unity的Properties了,为什么我要强调是unity的Properties呢?那是因为unity在Nvidia CG的基础上修改封装成unity CG,在继承了Nvidia CG的基础上扩展了其他特性,比如Properties{}这种字段封装的特性,再定义完字段后,编译器(CG静态动态编译两种都支持)“编译解释”Properties{}后,在Editor面板生成相应的gui以便我们开发操作,但是Properties{}有个很不方便的地方那就是无法传递大量数据比如数组等,这就限制了我们使用unity CG的Properties{}。但是这是不是意味着unity CG就不能沿用Nvidia CG那一套原始的数据传递方式呢?这个必须可以,unity CG依旧保持Nvidia CG的特性!这里就来介绍一下原始的Nvidia CG如何传递数据字段。
uniform关键字。
uniform类型修饰符修饰的字段指明了一个变量初始值的来源,当一个字段声明为uniform的时候,就代表这个变量来自与Nvidia CG的外部环境。用通俗的语言来描叙就是CPU或内存中的数据经过uniform修饰符的桥接,传递给GPU硬件资源比如寄存器显存等,然后我们就能在CG中使用CPU或内存数据。
这里先简单的做个CG shader测试一下。
CG shader代码:
Shader "Unlit/UniformTestUnlitShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
uniform float4x4 _mat;
v2f vert (appdata v)
{
v2f o;
//外部传入矩阵进行变换
float4 rf = mul(_mat,v.vertex);
o.vertex = UnityObjectToClipPos(rf);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
c#外部调用代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UniformCtrl : MonoBehaviour {
public Material mMat;
private float mRadian;
private Matrix4x4 mMatrix;
void Start () {
mMatrix = new Matrix4x4();
}
void Update()
{
//弧度值随着时间增加
mRadian += Time.time/30.0f;
//构建旋转矩阵
mMatrix.m00 = 1;
mMatrix.m01 = 0;
mMatrix.m02 = 0;
mMatrix.m03 = 0;
mMatrix.m10 = 0;
mMatrix.m11 = Mathf.Cos(mRadian);
mMatrix.m12 = -Mathf.Sin(mRadian);
mMatrix.m13 = 0;
mMatrix.m20 = 0;
mMatrix.m21 = Mathf.Sin(mRadian);
mMatrix.m22 = Mathf.Cos(mRadian);
mMatrix.m23 = 0;
mMatrix.m30 = 0;
mMatrix.m31 = 0;
mMatrix.m32 = 0;
mMatrix.m33 = 1;
//给material的shader中uniform矩阵赋值
//赋予的是x轴旋转矩阵
mMat.SetMatrix("_mat", mMatrix);
}
}
效果如下:
c#和CG shader代码的意义已经有了注释,这里我只简单说下,就是通过CPU c#代码控制GPU shader代码中的uniform矩阵,达到绕x轴旋转的效果。
这里说一个很尴尬的事情,就是unity CG的CG编译器省去了uniform修饰符,也就是说就算你定义字段不添加uniform,也可以c#调用,但是我们学习Nvidia CG可不仅仅是为了unity开发,我们学好Nvidia CG,那就够我们进行几乎所有三维图形引擎渲染开发了,所以我的建议就是严格按照Nvidia CG语法定义uniform。
好了,继续开始后续的探索。
为什么我需要提前讲解uniform字段,因为unity CG的Properties{}封装并不能传递数组,这是一个很大的弊端,所以我们只有通过uniform数组字段传递,好,说到这里我们就来实现c#和shader代码来传递网格数据。
CG shader代码:
Shader "Unlit/VertexMotionUnlitShader"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_Weight("Weight",Range(0.0,1.0)) = 0.0
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Cull off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _Weight; /*权重值0-0.5为flap到stand/0.5-1.0为stand到back*/
uniform int meshVertexCount; /*数组长度count也可以通过外部传递,那么我们定义网格顶点数组就必须至少大于实际的count*/
uniform float4 flapVertex[33]; /*扑腾网格顶点数组*/
uniform float4 standVertex[33]; /*站立网格顶点数组*/
uniform float4 backVertex[33]; /*收翅网格顶点数组*/
//简单的插值函数
float4 clampVertex(float4 a, float4 b, float weight)
{
weight = weight<0.0?0.0:weight;
weight = weight>1.0?1.0:weight;
return a + (b - a)*weight;
}
//我解释下SV_vertexID这个语义是什么意思
//这个shader承载的“羊”的0-32顶点id编号
//那么绑定了SV_vertexID语义的vid就是传递0-32这一串顶点编号
//那么flapVertex/standVertex/backVertex[vid]则依次取出扑腾/站立/收翅的网格顶点
//最后进行插值补间,则能达到补间动画效果
v2f vert(appdata v, uint vid : SV_vertexID)
{
v2f o;
float4 pos;
if (_Weight >= 0.0 && _Weight <= 0.5)
{
pos = clampVertex(flapVertex[vid], standVertex[vid], _Weight * 2.0);
}
else if (_Weight > 0.5 && _Weight <= 1.0)
{
pos = clampVertex(standVertex[vid], backVertex[vid], (_Weight - 0.5)*2.0);
}
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.vertex = UnityObjectToClipPos(pos);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
c#代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MotionCtrl : MonoBehaviour
{
public MeshFilter mFlapFilter;
public MeshFilter mStandFilter;
public MeshFilter mBackFilter;
public Material mVertexMotionMat;
void Start()
{
//给shader传递三种网格状态的顶点数据,注意要使用齐次坐标形式
mVertexMotionMat.SetVectorArray("flapVertex", GetHomogenCoords(mFlapFilter.sharedMesh.vertices));
mVertexMotionMat.SetVectorArray("standVertex", GetHomogenCoords(mStandFilter.sharedMesh.vertices));
mVertexMotionMat.SetVectorArray("backVertex", GetHomogenCoords(mBackFilter.sharedMesh.vertices));
}
//转成齐次坐标形式
public Vector4[] GetHomogenCoords(Vector3[] vec3s)
{
Vector4[] vec4s = new Vector4[vec3s.Length];
for(int i =0;i< vec3s.Length;i++)
{
vec4s[i] = new Vector4(vec3s[i].x, vec3s[i].y, vec3s[i].z, 1);
}
return vec4s;
}
}
效果图如下:
上面的c#和shader代码我做了详细注释,但是我还是要特别讲解:
①.vertex顶点和fragment片段函数需要根据图形渲染流程上下文理解,意思是说vertex顶点函数由CG runtime根据网格顶点数量进行for(int i = 0;i<vertexCount;i++) {vertexFunction(vertices[i])}的调用方式,这个我之前详细讲解过好几次,这里再次提醒。
②.uint vid : SV_vertexID绑定顶点ID语义的vid就是类似上面函数中的int i,意义就是网格顶点编号,结合“羊”的网格,就是0-32标号。
③.使用c#CPU代码传入三种网格状态的顶点齐次坐标数组到shader GPU硬件资源,最后在vertex顶点函数中进行插值补间,改变_Weight值(0-0.5||0.5-1.0)就能达到网格顶点插值动画效果。
下面最后使用_Time来个鬼畜的动画,代码如下:
_FlapTime("FlapTime",Range(1.0,3.0)) = 1.0
float _FlapTime; /*扑腾一次的时间*/
v2f vert(appdata v, uint vid : SV_vertexID)
{
v2f o;
//pingpong动画
if (((int)_Time.y / (int)_FlapTime) % 2 == 0)
{
_Weight = (_Time.y%_FlapTime)/ _FlapTime;
}
else
{
_Weight = (_FlapTime - _Time.y%_FlapTime) / _FlapTime;
}
float4 pos;
if (_Weight >= 0.0 && _Weight <= 0.5)
{
pos = clampVertex(flapVertex[vid], standVertex[vid], _Weight * 2.0);
}
else if (_Weight > 0.5 && _Weight <= 1.0)
{
pos = clampVertex(standVertex[vid], backVertex[vid], (_Weight - 0.5)*2.0);
}
//使用y轴平移矩阵处理飞行运动
float4x4 _tranMat = float4x4(1, 0, 0, 0,
0, 1, 0, _Time.y / 2.0,
0, 0, 1, 0,
0, 0, 0, 1);
float4 tf = mul(_tranMat, pos);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.vertex = UnityObjectToClipPos(tf);
return o;
}
代码只贴关键部分,意思就是一个pingpong动画和translate矩阵位移,效果图如下:
讲到这里基本上已经详细学习了美术同事们做的网格顶点补间动画的原理了,那么小伙伴们再思考一下,美术他们做好角色网格补间动画,又没有写代码runtime提取网格顶点,那么假设需要我们开发也不使用CPU代码在runtime运行时赋值网格顶点数据,我们该怎么办呢?大家先思考一下,后续我还会扩展讲解。
好了,我又要继续学习五线谱了。