Unity3D 移动平台实现一种大规模(其实跟PC比还是算小规模)动画角色渲染的方案---绝对原创方案。。。

手机硬件限制,很多PC上的渲染优化技术是没办法直接拿过来用的。目前有些游戏为了实现多部队战斗的效果,各种降低骨骼数目,模型面数的方案,但都逃不过骨骼动画计算这一环节。

在上个公司的时候,自己瞎想了一张方案,没想到最后还写出来了, 没想到最后还用上了。。。

先上张图,里面有100个士兵和10个萌宝宝的场景,每个角色的动作是分开控制的,在小米3上可以60fps的帧率流畅运行,之前也尝试过,300多个角色带动画也可以55左右的fps运行

先说明一下这套方案的优缺点:

优点:可不用计算骨骼动画,并能流畅播放骨骼动画(其实已经不是骨骼动画了);每个角色动作也可以单独控制;对cpu很小消耗;

缺点:动作切换没有过渡(群体作战的时候,这点基本不影响审美);对内存占用有一点消耗(其实也还好)

再说明方案思路:

为了省掉骨骼动画计算这一环节,就不能用现有骨骼动画系统。那么可选的方案也只有顶点变形动画了,而传统的顶点动画不管是内存占用还是不低的,而且unity支持也不算好。

那么既然只能选顶点动画且为了降低内存消耗,那么可以通过改写shader,通过gpu来插值实现顶点动画播放骨骼动画的效果。

shader插值的方案确实也不少,但结合具体实现上来,我选了两种实现相对简单的方案:

1.vertex shader里通过每帧采样的uv偏移采样纹理来控制顶点的位置,当我已经在pc端实现的时候才突然发现一个显而易见的问题:在vertex shader里采样纹理,pc端部分显卡是支持的,但手机gpu就不要想了。

2.于是只能第二种方案:还是vextex shader里插值顶点来播放动画,那么就有一个问题,vertex data从哪里来? unity的mesh结构有很多通道,顶点,颜色,uv,uv2,法线,切线。。这些通道里面除了color(主要是精度问题)和uv(还是必须给纹理坐标一个位置的)通道以外我都可以用来存顶点数据,然后通过控制时间点来组合顶点进行插值,然而还有个问题需要解决,这种方式一个mesh只能插值4个关键顶点,那么较长的动画怎么办呢,可以通过提前生成多个mesh,来切换。

动画截取可以通过unity的BakeMesh函数或者美工来帮助实现,下面是组合mesh 的代码:

byte[] Make(Mesh mesh1, Mesh mesh2, Mesh mesh3, Mesh mesh4, float clipTimeLenghts, float frame2Pos, float frame3Pos)
{
Mesh[] meshs = new Mesh[] { mesh1, mesh2, mesh3, mesh4};
VertexAnimationResManager.ClipMeshData meshData = new VertexAnimationResManager.ClipMeshData();
meshData.subMeshCount = meshs[0].subMeshCount;

int count = meshs[0].vertices.Length;
//顶点
if (meshs[0].vertices != null && meshs[0].vertices.Length > 0)
{
meshData.vertexBuffer = new float[count * 3];

for (int i = 0; i < meshs[0].vertices.Length; i++)
{
meshData.vertexBuffer[i * 3] = meshs[0].vertices[i].x;
meshData.vertexBuffer[i * 3 + 1] = meshs[0].vertices[i].y;
meshData.vertexBuffer[i * 3 + 2] = meshs[0].vertices[i].z;
}
}
//uv
if (meshs[0].uv != null && meshs[0].uv.Length > 0)
{
meshData.uvBuffer = new float[count * 2];

for (int i = 0; i < meshs[0].vertices.Length; i++)
{
meshData.uvBuffer[i * 2] = meshs[0].uv[i].x;
meshData.uvBuffer[i * 2 + 1] = meshs[0].uv[i].y;
}

//GCHandle verSrcHand = GCHandle.Alloc(meshs[0].uv, GCHandleType.Pinned);
//Marshal.Copy(verSrcHand.AddrOfPinnedObject(), meshData.uvBuffer, 0, meshData.uvBuffer.Length);
//verSrcHand.Free();
}

//法线 这里用来存动画第二帧的顶点信息
if (meshs[1].vertices != null && meshs[1].vertices.Length > 0)
{
meshData.normalBuffer = new float[count * 3];

for (int i = 0; i < meshs[0].vertices.Length; i++)
{
meshData.normalBuffer[i * 3] = meshs[1].vertices[i].x;
meshData.normalBuffer[i * 3 + 1] = meshs[1].vertices[i].y;
meshData.normalBuffer[i * 3 + 2] = meshs[1].vertices[i].z;
}
}

//切线 这里用来存动画第三帧的顶点信息
if (meshs[2].vertices != null && meshs[2].vertices.Length > 0)
{
meshData.tangentBuffer = new float[count * 4];

for (int i = 0; i < meshs[0].vertices.Length; i++)
{
meshData.tangentBuffer[i * 4] = meshs[2].vertices[i].x;
meshData.tangentBuffer[i * 4 + 1] = meshs[2].vertices[i].y;
meshData.tangentBuffer[i * 4 + 2] = meshs[2].vertices[i].z;
meshData.tangentBuffer[i * 4 + 3] = meshs[3].vertices[i].x;
}
}

//UV2 用来存第四个关键帧率的 顶点YZ 坐标 X坐标由切线的W通道来存
if (meshs[3].vertices != null && meshs[3].vertices.Length > 0)
{
meshData.uv2Buffer = new float[count * 2];

for (int i = 0; i < meshs[0].vertices.Length; i++)
{
meshData.uv2Buffer[i * 2] = meshs[3].vertices[i].y;
meshData.uv2Buffer[i * 2 + 1] = meshs[3].vertices[i].z;
}
}

//颜色 用来存第5个顶点信息
//if (meshs[Indexs[4]].vertices != null && meshs[Indexs[4]].vertices.Length > 0)
//{
// //颜色通道貌似没有负数,且范围为0到1 所有这里需要将模型顶点映射到[0,1]之间,映射范围为[-1,1]之间

// meshData.colorBuffer = new float[count * 4];
// for (int i = 0; i < meshs[Indexs[4]].vertices.Length; i++)
// {
// meshData.colorBuffer[i * 4] = (meshs[Indexs[4]].vertices[i].x * 0.5f) + 0.5f;
// meshData.colorBuffer[i * 4 + 1] = (meshs[Indexs[4]].vertices[i].y * 0.5f) + 0.5f;
// meshData.colorBuffer[i * 4 + 2] = (meshs[Indexs[4]].vertices[i].z * 0.5f) + 0.5f;
// }
//}

count = 0;
int len = 0;
meshData.subMeshTriangleLens = new int[meshData.subMeshCount];
for (int i = 0; i < meshData.subMeshCount; i++)
{
len = meshs[0].GetTriangles(i).Length;
count += len;
meshData.subMeshTriangleLens[i] = len;
}

meshData.triangleBuffer = new int[count];

len = 0;
for (int i = 0; i < meshData.subMeshCount; i++)
{
meshs[0].GetTriangles(i).CopyTo(meshData.triangleBuffer, len);
len += meshData.subMeshTriangleLens[i];
}

ByteBuffer bbuffer = new ByteBuffer();
bbuffer.WriteFloat(clipTimeLenghts);
bbuffer.WriteFloat(frame2Pos);
bbuffer.WriteFloat(frame3Pos);
bbuffer.WriteInt(meshs[0].subMeshCount);

for(int i=0;i<meshData.subMeshTriangleLens.Length;i++ )
{
bbuffer.WriteInt(meshData.subMeshTriangleLens[i]);
}

bbuffer.WriteInt(meshData.triangleBuffer.Length);
for (int i = 0; i < meshData.triangleBuffer.Length; i++)
{
bbuffer.WriteInt(meshData.triangleBuffer[i]);
}

bbuffer.WriteInt(meshData.vertexBuffer.Length);
for (int i = 0; i < meshData.vertexBuffer.Length; i++)
{
bbuffer.WriteFloat(meshData.vertexBuffer[i]);
}

bbuffer.WriteInt(meshData.normalBuffer.Length);
for (int i = 0; i < meshData.normalBuffer.Length; i++)
{
bbuffer.WriteFloat(meshData.normalBuffer[i]);
}

bbuffer.WriteInt(meshData.tangentBuffer.Length);
for (int i = 0; i < meshData.tangentBuffer.Length; i++)
{
bbuffer.WriteFloat(meshData.tangentBuffer[i]);
}

bbuffer.WriteInt(meshData.uvBuffer.Length);
for (int i = 0; i < meshData.uvBuffer.Length; i++)
{
bbuffer.WriteFloat(meshData.uvBuffer[i]);
}

bbuffer.WriteInt(meshData.uv2Buffer.Length);
for (int i = 0; i < meshData.uv2Buffer.Length; i++)
{
bbuffer.WriteFloat(meshData.uv2Buffer[i]);
}
return bbuffer.ToBytes();
}

截取好后,保存为自己的二进制文件,运行的时候加载并 解析的代码如下:

public void AddAnimationInfo(string aniName, byte[] clipData)
{
VertexAnimationClipInfo clipInfo = null;

AnimationClipInfos.TryGetValue(aniName, out clipInfo);

if(clipInfo!=null)
{
Debug.LogError("animation clip has exits!");
return;
}

clipInfo = new VertexAnimationClipInfo();

ByteBuffer bbuffer = new ByteBuffer(clipData);
int Count = bbuffer.ReadInt();

for (int i = 0; i < Count; i++)
{
ClipMeshData meshData = GetMeshData(bbuffer);
clipInfo.clipTotalTimeLen += meshData.timeLenth;
clipInfo.clipLenghts.Add(meshData.timeLenth);
clipInfo.everyClipFrameTimePoints.Add(new Vector3(meshData.Frame2TimePoint, meshData.Frame3TimePoint)); //,meshData.Frame4TimePoint
clipInfo.clipMeshs.Add(meshData.GenMesh());
}

bbuffer.Close();

AnimationClipInfos.Add(aniName, clipInfo);
}

VertexAnimationClipInfo定义如下:

[Serializable]
public class VertexAnimationClipInfo
{
public float clipTotalTimeLen = 0;
public List<Mesh> clipMeshs = new List<Mesh>();
public List<Vector2> everyClipFrameTimePoints = new List<Vector2>();
public List<float> clipLenghts = new List<float>();
}

ClipMeshData定义如下:

public class ClipMeshData
{
public float timeLenth;

///Frame1TimePoint =0 Frame4TimePoint = 1
public float Frame2TimePoint = 0.333f;
public float Frame3TimePoint = 0.666f;
//public float Frame4TimePoint = 0.75f;

public int subMeshCount;
public int[] subMeshTriangleLens;
public int[] triangleBuffer;
public float[] vertexBuffer;
public float[] normalBuffer;
public float[] tangentBuffer;
public float[] uvBuffer;
public float[] uv2Buffer;
//public float[] colorBuffer;

public Mesh GenMesh()
{
Mesh mesh = new Mesh();

int vertexCount = vertexBuffer.Length / 3;

mesh.subMeshCount = subMeshCount;
//顶点
Vector3[] vertexs = new Vector3[vertexCount];
for (int i = 0; i < vertexCount; i++)
{
vertexs[i] = new Vector3(vertexBuffer[i * 3], vertexBuffer[i * 3 + 1], vertexBuffer[i * 3 + 2]);
}
mesh.vertices = vertexs;
//uv
Vector2[] uv = new Vector2[vertexCount];
for (int i = 0; i < uv.Length; i++)
{
uv[i] = new Vector2(uvBuffer[i * 2], uvBuffer[i * 2 + 1]);
}
mesh.uv = uv;
//uv2
Vector2[] uv2 = new Vector2[vertexCount];
for (int i = 0; i < uv.Length; i++)
{
uv2[i] = new Vector2(uv2Buffer[i * 2], uv2Buffer[i * 2 + 1]);
}
mesh.uv2 = uv2;

//法线
Vector3[] normals = new Vector3[vertexCount];
for (int i = 0; i < normals.Length; i++)
{
normals[i] = new Vector3(normalBuffer[i * 3], normalBuffer[i * 3 + 1], normalBuffer[i * 3 + 2]);
}
mesh.normals = normals;

//切线
var tangents = new Vector4[vertexCount];
for (int i = 0; i < tangents.Length; i++)
{
tangents[i] = new Vector4(tangentBuffer[i * 4], tangentBuffer[i * 4 + 1], tangentBuffer[i * 4 + 2], tangentBuffer[i * 4 + 3]);
}
mesh.tangents = tangents;
////颜色
//Color[] colors = new Color[colorBuffer.Length / 4];
//for (int i = 0; i < colors.Length; i++)
//{
// colors[i] = new Vector4(colorBuffer[i * 4], colorBuffer[i * 4 + 1], colorBuffer[i * 4 + 2], 1);
//}
//mesh.colors = colors;

//三角形
int startIndex = 0;
int bufferLen = 0;

for (int i = 0; i < subMeshCount; i++)
{
bufferLen = subMeshTriangleLens[i];
if (bufferLen <= 0) continue;
var triIndexBuffer = new int[bufferLen];
Array.Copy(triangleBuffer, startIndex, triIndexBuffer, 0, bufferLen);
mesh.SetTriangles(triIndexBuffer, i);
startIndex += bufferLen;
}
return mesh;
}
}

播放动画切换对应的mesh就可以了,下面是gpu插值所用的shader代码:

Shader "LXZ_TEST/VertexAnimation-NoColorBuf" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_CurTime("Time", Float) = 0
_Frame2Time("Frame2Time", Float) = 0.333
_Frame3Time("Frame3Time", Float) = 0.666
_Color ("MainColor", color) = (1,1,1,1)
}
SubShader {
// Tags { "QUEUE"="Geometry" "RenderType"="Opaque" }

Pass {
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// #include "UnityCG.cginc"

#pragma glsl_no_auto_normalization

sampler2D _MainTex;
float _CurTime;
float _Frame2Time;
float _Frame3Time;
float4 _Color;

struct appdata {
float4 vertex : POSITION;
float3 vertex1 : NORMAL;
float4 vertex2 : TANGENT;
float2 texcoord : TEXCOORD0;
float2 vertex3: TEXCOORD1;
//float3 vertex4: COLOR;
};

struct v2f {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
};

v2f vert(appdata v) {
v2f result;

float a = _CurTime - _Frame2Time;
float b = _CurTime - _Frame3Time;

float3 vec;

float3 vertex3 = float3(v.vertex2.w,v.vertex3.xy);

if(a<0)
vec = v.vertex.xyz + (v.vertex1 - v.vertex.xyz)* _CurTime/_Frame2Time;
else if(a>=0 && b<0)
{
vec = v.vertex1 + (v.vertex2.xyz - v.vertex1)* a/(_Frame3Time-_Frame2Time);
}
else
vec = v.vertex2.xyz + (vertex3 - v.vertex2.xyz)* b/(1-_Frame3Time);

result.pos = mul(UNITY_MATRIX_MVP, float4(vec,1));
//result.pos = mul(UNITY_MATRIX_MVP, float4(v.vertex1.xyz,1));
//result.pos = mul(UNITY_MATRIX_MVP, float4(v.vertex2.xyz,1));
// result.pos = mul(UNITY_MATRIX_MVP, float4(vertex3.xyz,1));
// result.pos = mul(UNITY_MATRIX_MVP, float4(vertex4.xyz,1));
result.uv = v.texcoord;

return result;
}

float4 frag(v2f i) : COLOR
{
float4 color = tex2D(_MainTex, i.uv);
return color *_Color;
}

ENDCG 
}
}
FallBack "Diffuse"
}

如果各位还有更好的方案,欢迎交流。。不擅长文字的东西,所以直接贴代码了,有需要可以与我连续交流。。。

时间: 2024-10-29 07:11:18

Unity3D 移动平台实现一种大规模(其实跟PC比还是算小规模)动画角色渲染的方案---绝对原创方案。。。的相关文章

慕容小匹夫 Unity3D移动平台动态读取外部文件全解析

Unity3D移动平台动态读取外部文件全解析 c#语言规范 阅读目录 前言: 假如我想在editor里动态读取文件 移动平台的资源路径问题 移动平台读取外部文件的方法 补充: 回到目录 前言: 一直有个想法,就是把工作中遇到的坑通过自己的深挖,总结成一套相同问题的解决方案供各位同行拍砖探讨.眼瞅着2015年第一个工作日就要来到了,小匹夫也休息的差不多了,寻思着也该写点东西活动活动大脑和手指了.那么今天开始,小匹夫会记录一些平时工作中遇到的坑,以及小匹夫的应对方法,欢迎各位拍砖讨论.那么今天主要讨

Unity3D移动平台动态读取外部文件全解析

假如我想在editor里动态读取文件 实际的游戏开发中,其实有相当一部分静态数据是可以放在客户端的,所以势必会产生要动态读取这些文件的需求,比如csv(其实就是文本文件),xml等等.我相信大家不管是用win还是用mac来做unity3d的开发,都一定要先在editor中去实现基本的功能,在具体到各个移动平台上去调试.所以作为要读取外部文件的第一步,显然我们要先在editor也就是pc上实现这个功能. 下面给各位举一个读取xml的例子,也是我在以前的一篇文章<自己动手之使用反射和泛型,动态读取X

编写Unity3D着色器的三种方式

不管你会不会写Unity3D的shader,估计你会知道,Unity3D编写shader有三种方式,这篇东西主要就是说一下这三种东西有什么区别,和大概是怎样用的. 先来列一下这三种方式: fixed function shader vertex and fragment shader surface shader 为什么Unity3D要提供三种shader的编写方式呢?那是因为三种方式的编写的难易度有区别,对应着不同的使用人群.其实我觉得这是Uniy3D想得有点多了,着色器不单止是为了实现效果,

Unity3D各平台Application.xxxPath的路径

前几天我们游戏在一个同事的Android手机上启动时无法正常进入,经查发现Application.temporaryCachePath和Application.persistentDataPath返回空字符串.便花时间认真研究了一下Unity3D的路径问题.我们常用的是以下四个路径: Application.dataPath Application.streamingAssetsPath Application.persistentDataPath Application.temporaryCa

Unity3D 多平台_预编译相关宏定义

预编译 原文地址:http://docs.unity3d.com/Documentation/Manual/PlatformDependentCompilation.html 平台定义     UNITY_EDITOR 编辑器调用. UNITY_STANDALONE_OSX 专门为Mac OS(包括Universal,PPC和Intelarchitectures)平台的定义. UNITY_DASHBOARD_WIDGET Mac OS Dashboard widget (Mac OS仪表板小部件

一个功能,两个平台,三种语言 -(iOS,Swift,Android)App代码实现对比篇

-调研 话说移动互联网正值风起云涌期间,各路编程高手都是摩拳擦掌,何况企业公司都开始接受现实,走移动办公,信息云端,大数据处理的步伐,在这本该三足鼎立的时刻,微软显得有点步履蹒跚,导致移动端最值得进军的平台被iOS 和 Android 几乎瓜分,这不符合历史轨迹啊,希望 WP 能厚积薄发,重回当年PC时代的辉煌. -前序 这里就不再指点江山,直奔主题吧,来看看做同样一个功能,在iOS平台和Android平台都是具体如何实现的,代码是如何写的,这里有个分支就是iOS平台开发又分为Objective

一个优秀的Unity3d开发者必备的几种设计模式

Unity脚本编程 如何写脚本架构 参考书籍 设计模式 原则1:单一职责原则 用一个类描述动物呼吸这个场景 当需求变动 改动量小的方法 隐患 另一种修改方式 遵循单一职责原的优点有 原则2:里氏替换原则 名字的由来 定义 继承的风险 需求变动 影响了正常的功能 里氏替换原则通俗的来讲就是 原则3:依赖倒置原则 定义 依赖倒置原则核心思想 情景举例 需求变动 抽象的接口 在实际编程中,我们一般需要做到如下3点 原则4:接口隔离原则 定义 未遵循接口隔离原则的设计 示例代码 遵循接口隔离原则的设计

unity3d android平台剪贴板的实现

也是在网上翻了很多资料,这里参考了一下网友的http://www.cnblogs.com/xiaozefeng/p/Unity_Android_IOS.html 但是他写的不全,导致写android程序eclipse最后会报错 他这里写道 Unity3D调用Android剪贴板 public class ClipboardTools { public static ClipboardManager clipboard = null; // 向剪贴板中添加文本 public void copyTe

微信平台下两种消息处理流程

简单的微信公共账号的开发貌似很简单.相当于汇总了我们所有程序的入口. 但是微信的消息处理模式主要有两种,今天我们主要看看一下其间的区别. 1 编辑模式下的消息处理模式 步骤一:用户使用微信客户端像公众号发送消息. 二:根据公众号运营者配置的规则进行处理 三:将处理结构返回给微信客户端,通过公众号呈现给用户. 2开发模式下的处理流程 步骤一:用户使用微信客户端向公众号发送消息. 二:通过HTTP POST 消息 三:接受处理消息 四:返回处理结果 五:将处理结果返回,通过公众号呈现给用户 两者的却