首先说明本人机器CPU是 i5-7400,GPU是GTX 1060 3G。模型三角面大约2K,分辨率1080P。
先显示效果,帧率在70左右
性能分析
渲染阴影大约用时2ms
渲染物体大约用时2.3ms 渲染一万个物体 cpu一共耗时2.3ms
BaseRenderStruct[] baseRenderStructs = new BaseRenderStruct[10000];
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 100; j++)
{
baseRenderStructs[i*100+j].position = new War.Vector2(j,i);
baseRenderStructs[i*100+j].rotation = j * 36;
}
}
Observable.EveryUpdate()
.Subscribe(_ =>
{
Profiler.BeginSample("CharacterRender");
m_renderManger.DrawCharacterInstanced(baseRenderStructs);
Profiler.EndSample();
});
大量具有相同动画的物体,首先想到的是GPU Instancing
物体具有动画和Skinned Mesh,而且mesh不只有一个。物体可以投射阴影和接受阴影。思想如下:
首先Skin Mesh合并,一个物体下面的所有mesh合并,并且共用一个材质球。(这一步目前没做)
链接: SkinMesh合并
设置一个结构体,这个结构体里面有物体的位置和角度信息。通过job system,转换为本地2世界坐标矩阵
public struct BaseRenderStruct
{
public Vector2 position;
public float rotation;
}
public struct MyParallelJob : IJobParallelFor
{
[ReadOnly]
public NativeArray<BaseRenderStruct> datas;
[ReadOnly] public Matrix4x4 selfRotation;
public NativeArray<Matrix4x4> result;
public void Execute(int i)
{
Matrix4x4 mat = Matrix4x4.identity;
mat.m03 = (float) datas[i].position.x;
mat.m13 = 0.0f;
mat.m23 = (float) datas[i].position.y;
result[i] = mat * Matrix4x4.Rotate(Quaternion.Euler(0, datas[i].rotation - 90, 0)) * selfRotation;
}
}
得到物体每帧动画的mesh,然后用GPU Instancing大批量渲染这些mesh。
这里主要使用skinnedMeshRenderers.BakeMesh的方法烘培一个Mesh。
然后用Graphics.DrawMesh绘制大量Mesh。
C#代码
// 渲染结构体
public struct BaseRenderStruct
{
public Vector2 position;
public float rotation;
}
public class Player3DCharacterRender
{
private List<SkinnedMeshRenderer> m_skinnedMeshRenderers;
private List<Mesh> m_animedMeshs;
private int m_CurFrameCount = -1;
private float m_StartTime;
private Animator m_Animator;
private GameObject m_go;
private AnimationClip m_clip;
private Matrix4x4 m_selfRotation;
MyParallelJob myParallelJob;
public void Init(GameObject go)
{
m_go = go;
m_StartTime = Time.time;
m_skinnedMeshRenderers = new List<SkinnedMeshRenderer>();
m_animedMeshs = new List<Mesh>();
m_Animator = m_go.GetComponent<Animator>();
m_selfRotation = Matrix4x4.Rotate(Quaternion.Euler(-90, 0, 0));
SkinnedMeshRenderer[] skinnedMeshRenderers = m_go.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (var skinnedMeshRenderer in skinnedMeshRenderers)
{
m_skinnedMeshRenderers.Add(skinnedMeshRenderer);
m_animedMeshs.Add(new Mesh());
}
foreach (var clip in m_Animator.runtimeAnimatorController.animationClips)
{
if (clip.name.Equals("run(WeaponOneHand)"))
{
m_clip = clip;
break;
}
}
myParallelJob = new MyParallelJob();
myParallelJob.selfRotation = m_selfRotation;
}
public void Render(BaseRenderStruct data)
{
if(m_go == null)
return;
if(m_CurFrameCount != Time.frameCount)
BakeMesh();
MaterialPropertyBlock properties = new MaterialPropertyBlock();
Matrix4x4 m;
CalculateWorldMatAndPlayIndex(data, out m);
for (int i = 0; i < m_animedMeshs.Count; i++)
{
Graphics.DrawMesh(m_animedMeshs[i] ,m,m_skinnedMeshRenderers[i].sharedMaterial,0,Camera.main,0,properties,ShadowCastingMode.On);
}
}
public void Render(BaseRenderStruct[] data)
{
if(m_go == null)
return;
if(m_CurFrameCount != Time.frameCount)
BakeMesh();
int group = data.Length / 1000;
group += data.Length % 1000 > 0 ? 1 : 0;
MaterialPropertyBlock properties = new MaterialPropertyBlock();
/*for(int i = 0;i<data.Length;i++)
CalculateWorldMatAndPlayIndex(data[i], out m[i]);*/
myParallelJob.datas = new NativeArray<BaseRenderStruct>(data.Length, Allocator.TempJob);
myParallelJob.datas.CopyFrom(data);
NativeArray<Matrix4x4> result = new NativeArray<Matrix4x4>(data.Length, Allocator.TempJob);
myParallelJob.result = result;
JobHandle handle = myParallelJob.Schedule(data.Length, 1);
handle.Complete();
Matrix4x4[] m = result.ToArray();
Matrix4x4[] subM = new Matrix4x4[1000];
for (int n = 0; n < group; n++)
{
int count = data.Length - n * 1000;
count = count > 1000 ? 1000 : count;
Array.Copy(m, n * 1000, subM, 0, count);
for (int i = 0; i < m_animedMeshs.Count; i++)
{
Graphics.DrawMeshInstanced(m_animedMeshs[i] ,0,m_skinnedMeshRenderers[i].sharedMaterial,subM,count,properties);
}
}
myParallelJob.datas.Dispose();
result.Dispose();
}
void CalculateWorldMatAndPlayIndex(BaseRenderStruct data ,out Matrix4x4 mat)
{
mat = Matrix4x4.identity;
mat.m03 = (float) data.position.x;
mat.m13 = 0.0f;
mat.m23 = (float) data.position.y;
mat = mat*Matrix4x4.Rotate(Quaternion.Euler(0,data.rotation - 90,0))*m_selfRotation;
}
public struct MyParallelJob : IJobParallelFor
{
[ReadOnly]
public NativeArray<BaseRenderStruct> datas;
[ReadOnly] public Matrix4x4 selfRotation;
public NativeArray<Matrix4x4> result;
public void Execute(int i)
{
Matrix4x4 mat = Matrix4x4.identity;
mat.m03 = (float) datas[i].position.x;
mat.m13 = 0.0f;
mat.m23 = (float) datas[i].position.y;
result[i] = mat * Matrix4x4.Rotate(Quaternion.Euler(0, datas[i].rotation - 90, 0)) * selfRotation;
}
}
void BakeMesh()
{
m_CurFrameCount = Time.frameCount;
float time = (Time.time - m_StartTime) % m_clip.length;
m_clip.SampleAnimation(m_go, time);
for (int i = 0; i < m_skinnedMeshRenderers.Count; i++)
{
m_animedMeshs[i].Clear();
m_skinnedMeshRenderers[i].BakeMesh(m_animedMeshs[i]);
}
}
}
调用代码,渲染一万个物体
// A code block
BaseRenderStruct[] baseRenderStructs = new BaseRenderStruct[10000];
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 100; j++)
{
baseRenderStructs[i*100+j].position = new War.Vector2(j,i);
baseRenderStructs[i*100+j].rotation = j * 36;
}
}
Observable.EveryUpdate()
.Subscribe(_ =>
{
Profiler.BeginSample("CharacterRender");
m_renderManger.DrawCharacterInstanced(baseRenderStructs);
Profiler.EndSample();
});
shader代码
Shader "Unlit/CharacterDefault"
{
Properties
{
_BaseMap ("Base Texture",2D) = "white"{}
_BaseColor("Base Color",Color)=(1,1,1,1)
[Toggle]_IsSpecular("是否开启高光", Float) = 1
}
SubShader
{
Tags
{
"RenderPipeline"="UniversalPipeline"
"Queue"="Geometry"
"RenderType"="Opaque"
}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
half _IsSpecular;
CBUFFER_END
ENDHLSL
Pass
{
Tags{"LightMode"="UniversalForward"}
HLSLPROGRAM //CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
struct Attributes
{
float4 positionOS : POSITION;
float4 normalOS : NORMAL;
float2 uv : TEXCOORD;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varings//这就是v2f
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD;
float3 positionWS : TEXCOORD1;
float3 viewDirWS : TEXCOORD2;
float3 normalWS : TEXCOORD3;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
Varings vert(Attributes IN)
{
Varings OUT;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS.xyz);
OUT.positionCS = positionInputs.positionCS;
OUT.uv=TRANSFORM_TEX(IN.uv,_BaseMap);
OUT.positionWS = positionInputs.positionWS;
OUT.viewDirWS = GetCameraPositionWS() - positionInputs.positionWS;
OUT.normalWS = normalInputs.normalWS;
return OUT;
}
float4 frag(Varings IN):SV_Target
{
UNITY_SETUP_INSTANCE_ID(IN);
half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
float4 SHADOW_COORDS = TransformWorldToShadowCoord(IN.positionWS);
Light light = GetMainLight(SHADOW_COORDS);
half3 n = normalize(IN.normalWS);
half3 v = normalize(IN.viewDirWS);
half3 h = normalize(light.direction + v);
half nl = max(0.0,dot(light.direction ,n));
half nh = max(0.0,dot(h ,n));
half atten = step(0.5, light.shadowAttenuation);
half3 diffuse = atten * lerp(0.5*baseMap.xyz ,baseMap.xyz ,nl) + (1 - atten) * 0.4 * baseMap.xyz * light.color ;
half3 specular = _IsSpecular * atten * light.color * step(0.8,pow(nh ,8));
uint pixelLightCount = GetAdditionalLightsCount();
for (uint lightIndex = 0; lightIndex < pixelLightCount; ++lightIndex)
{
Light add_light = GetAdditionalLight(lightIndex, IN.positionWS);
half3 add_h = normalize(add_light.direction + v);
half add_nl = max(0.0,dot(add_light.direction ,n));
half add_nh = max(0.0,dot(add_h ,n));
diffuse += baseMap.xyz * add_nl* add_light.color * add_light.distanceAttenuation;
specular += _IsSpecular * add_light.color * add_light.distanceAttenuation * step(0.8,pow(add_nh ,8));
}
half3 color=diffuse*_BaseColor.xyz;
return half4(color ,1.0);
}
ENDHLSL //ENDCG
}
pass {
Tags{ "LightMode" = "ShadowCaster" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
struct Attributes
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varings
{
float4 pos : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
sampler2D _MainTex;
float4 _MainTex_ST;
Varings vert(Attributes v)
{
Varings o = (Varings)0;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
return o;
}
float4 frag(Varings i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
return half4(0.0,0.0,0.0,1.0);
}
ENDHLSL
}
}
}
|