首先我们先借助Unity的SpriteAtlas构建一个图集,同一个图集只会占用一个DrawCall。
接着写一个脚本,把需要参与ECS渲染的Sprite拖上去。
渲染的原理就是 m_BatchRendererGroup.AddBatch
ECS负责坐标的修改
1.参与渲染的sprite数量发生变化(增加、删除)需要重新m_BatchRendererGroup.AddBatch
2.参与渲染的sprite数量没有发生变化,只是坐标发生变化,那么在Job里重新计算坐标
3.参与渲染的sprite数量没有发生变化、坐标也没有发生变化,例如:血条 显示图片一部分,只需要重新刷新MaterialPropertyBlock即可。
using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Rendering;
using static Unity.Mathematics.math;
public class GSprite : MonoBehaviour
{
public struct ECS
{
public Vector3 position; //显示坐标
public Vector3 scale; //显示缩放
public Vector2 pivot; //锚点区域
}
private BatchRendererGroup m_BatchRendererGroup;
private Mesh m_Mesh;
private Material m_Material;
private int m_BatchIndex = -1;
private JobHandle m_Hadle;
private NativeList<ECS> m_SpriteData;
private List<Vector4> m_SpriteDataOffset = new List<Vector4>();
public Sprite sprite1;
public Sprite sprite2;
public Sprite sprite3;
public Shader shader;
void Start()
{
m_SpriteData = new NativeList<ECS>(100, Allocator.Persistent);
m_Mesh = Resources.GetBuiltinResource<Mesh>("Quad.fbx");
m_Material = new Material(shader) { enableInstancing = true };
m_Material.mainTexture = sprite1.texture;
//添加图片
AddSprite(sprite1, Vector3.zero, Vector3.one,0.5f); //显示图片一部分(横向0.5f)
AddSprite(sprite2, Vector3.zero + new Vector3(1,0,0), Vector3.one * 0.5f, 1f); //显示完整图片(整体缩小0.5)
AddSprite(sprite3, Vector3.zero + new Vector3(1, 1, 0), new Vector3(10,2,1), 1f); //显示完整图片
Refresh();
}
void AddSprite(Sprite sprite, Vector2 localPosition, Vector2 localScale ,float slider)
{
float perunit = sprite.pixelsPerUnit;
Vector3 scale = new Vector3((sprite.rect.width / perunit) * localScale.x, (sprite.rect.height / perunit) * localScale.y, 1f);
Vector4 rect = GetSpreiteRect(sprite);
scale.x *= slider;
rect.x *= slider;
ECS obj = new ECS();
obj.position = localPosition;
obj.pivot = new Vector2(sprite.pivot.x / perunit * localScale.x, sprite.pivot.y / perunit * localScale.y);
obj.scale = scale;
m_SpriteData.Add(obj);
m_SpriteDataOffset.Add(rect);
}
private void Refresh()
{
//1.参与渲染的sprite数量发生变化(增加、删除)需要重新m_BatchRendererGroup.AddBatch
RefreshElement();
//2.参与渲染的sprite数量没有发生变化,只是坐标发生变化,那么在Job里重新计算坐标
RefreshPosition();
//3.参与渲染的sprite数量没有发生变化、坐标也没有发生变化,例如:血条 显示图片一部分,只需要重新刷新MaterialPropertyBlock即可。
//RefreshBlock();
}
private void RefreshElement()
{
if (m_BatchRendererGroup == null)
{
m_BatchRendererGroup = new BatchRendererGroup(OnPerformCulling);
}
else
{
m_BatchRendererGroup.RemoveBatch(m_BatchIndex);
}
MaterialPropertyBlock block = new MaterialPropertyBlock();
block.SetVectorArray("_Offset", m_SpriteDataOffset);
m_BatchIndex = m_BatchRendererGroup.AddBatch(
m_Mesh,
0,
m_Material,
0,
ShadowCastingMode.Off,
false,
false,
default(Bounds),
m_SpriteData.Length,
block,
null);
}
void RefreshPosition()
{
m_Hadle.Complete();
m_Hadle = new UpdateMatrixJob
{
Matrices = m_BatchRendererGroup.GetBatchMatrices(m_BatchIndex),
objects = m_SpriteData,
}.Schedule(m_SpriteData.Length, 32);
}
void RefreshBlock()
{
MaterialPropertyBlock block = new MaterialPropertyBlock();
block.SetVectorArray("_Offset", m_SpriteDataOffset);
m_BatchRendererGroup.SetInstancingData(m_BatchIndex, m_SpriteData.Length, block);
}
public JobHandle OnPerformCulling(BatchRendererGroup rendererGroup, BatchCullingContext cullingContext)
{
//sprite不需要处理镜头裁切,所以这里直接完成job
m_Hadle.Complete();
return m_Hadle;
}
[BurstCompile]
private struct UpdateMatrixJob : IJobParallelFor
{
public NativeArray<Matrix4x4> Matrices;
[ReadOnly] public NativeList<ECS> objects;
public void Execute(int index)
{
//通过锚点计算sprite实际的位置
ECS go = objects[index];
var position = go.position;
float x = position.x + (go.scale.x * 0.5f) - go.pivot.x;
float y = position.y + (go.scale.y * 0.5f) - go.pivot.y;
Matrices[index] = Matrix4x4.TRS(float3(x, y, position.z),
Unity.Mathematics.quaternion.identity,
objects[index].scale);
}
}
private Vector4 GetSpreiteRect(Sprite sprite)
{
var uvs = sprite.uv;
Vector4 rect = new Vector4();
rect[0] = uvs[1].x - uvs[0].x;
rect[1] = uvs[0].y - uvs[2].y;
rect[2] = uvs[2].x;
rect[3] = uvs[2].y;
return rect;
}
private void OnDestroy()
{
if (m_BatchRendererGroup != null)
{
m_BatchRendererGroup.Dispose();
m_BatchRendererGroup = null;
}
m_SpriteData.Dispose();
}
}
渲染区域需要动态的传入shader中。
Shader "ECSSprite"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Offset("_Offset",Vector)=(1,1,0,0)
}
SubShader
{
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "UnityCG.cginc"
#pragma target 3.0
struct appdata
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _Offset) //渲染区域
UNITY_INSTANCING_BUFFER_END(Props)
v2f vert (appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
o.vertex = UnityObjectToClipPos(v.vertex);
fixed4 l = UNITY_ACCESS_INSTANCED_PROP(Props, _Offset);
o.uv = v.uv.xy * l.xy + l.zw;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
最后就是结果 图片锚点、显示图片一部分、或者完整显示图片,一个drawcall全部渲染完毕。
- 本文固定链接: https://www.xuanyusong.com/archives/4687
- 转载请注明: 雨松MOMO于 雨松MOMO程序研究院 发表
捐 赠写博客不易,如果您想请我喝一杯星巴克的话?就进来看吧!




向站长学习
最近看了DOTS的哔哩哔哩的《Unity DOTS技术详解 – 宣雨松》,是不是3D游戏的DOTS实现要自己写ECS渲染,才能实现视频里的千人同屏60帧,而如果只是用DOTS修改GameObject数据,是达不到优化效果的?第一遍看演讲的时候’ECS渲染’最后才讲,甚至没给我感觉很重要,最后发现这部分才是最困难最吓人的,将创建出来的Entities 3D数据最终渲染到画面上,这要从什么领域入手?最后发现看蒙了。
目前 unity提供将 meshrenderer直接转成ECS 并且不支持lighmap ,还有别的渲染组件 比如 动画、 粒子、这些都需要开发者自己来做。
根据unity公布的消息,后面会慢慢都支持,现阶段只能自己来做。
我想请教一下 ECS能不能使用类似Renderer的sortingOrder进行渲染排序的呢
ECS是处理数据的,本身是不包含渲染的, 你可以想象ECS先把需要显示的数据在JOB中 处理完,然后在通过GPU instancing m_BatchRendererGroup.AddBatch 、 SRPBather渲染出来。 渲染的时候是支持sortingOrder的