前言:
? ? ? ? 设一个1024*1024的地块,需要实时计算有哪些地块在相机范围内。为了提高运行效率,这里用ComputeShader来计算。
1、准备工作
? ? ? ? 先在Unity右键创建一个ComputeShader,然后再创建一个Canvas,放一张RawImage(用于接受调试用的图片)。
? ? ? ? 然后创建一个控制类,添加以下控制组件:
public class ComputeTest : MonoBehaviour
{
#region 外部赋值属性
/// <summary>
/// 用于显示用的调试图片;
/// </summary>
public RawImage ShowImage;
/// <summary>
/// 当前计算Shader
/// </summary>
public ComputeShader shader;
/// <summary>
/// 当前主相机
/// </summary>
public Camera mainCamera;
#endregion
}
2、写ComputeShader代码:
? ? ? ? 原理就是,由客户端传过来摄像机的VP矩阵,然后在Shader里计算当前格子是否显示。然后写入到目标图片里就可以了。
? ? ? ? (具体的ComputeShader的介绍网上很多了,这里就不赘述了)
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel WolrdMapVisable
// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWTexture2D<float4> Result;
float4x4 WorldToCameraMatrix;
//使用VP矩阵计算是否可见
bool IsVisableInCamera (float x,float z)
{
float4 clipPos =mul(WorldToCameraMatrix, float4(x,0,z,1));
float3 ndcPos = float3(clipPos.x / clipPos.w, clipPos.y / clipPos.w, clipPos.z / clipPos.w);
float view_x = 0.5f + 0.5f * clipPos.x / clipPos.w;
float view_y = 0.5f + 0.5f * clipPos.y / clipPos.w;
return view_x>=0 && view_x<=1 && view_y>=0 && view_y<=1 && clipPos.w>0;
}
[numthreads(8,8,1)]
void WolrdMapVisable (uint3 id : SV_DispatchThreadID)
{
float x=id.x;//0~1024
float z=id.y;//0~1024
bool IsVisialbe = IsVisableInCamera(x,z);
float retValue = IsVisialbe?1:0;
//如果可见显示白色,不可见显示黑色
half4 col=half4(retValue,retValue,retValue,1);
Result[id.xy] =col;
}
? ? ? ? 这里的图片是外部传过来,定好的大小(1024*1024);
3、编写Unity C#控制代码
private const int TextureSize = 1024;//整个地图大小
private const int ThreadGroup = 8;
private const int ThreadGroupSize = TextureSize / ThreadGroup;
//各种属性名;
private const string ComputeShaderName = "WolrdMapVisable";
private const string ComputeTextureName = "Result";
private const string ComputeMatrixName = "WorldToCameraMatrix";
private int kID;
private int matixNameID;
private RenderTexture texture;
// Start is called before the first frame update
void Start() { RunComputeShader(); }
private void RunComputeShader()
{
//设置用于ComputeShader的贴图;
texture = new RenderTexture(TextureSize, TextureSize, 24);
texture.enableRandomWrite = true;
texture.filterMode = FilterMode.Point;
texture.Create();
//将图片显示在UI上,调试用;
ShowImage.texture = texture;
//获取Shader的一些属性;
kID = shader.FindKernel(ComputeShaderName);
matixNameID = Shader.PropertyToID(ComputeMatrixName);
//启动Shader:
shader.SetTexture(kID, ComputeTextureName, texture);
UpdateComputeShader();
}
void Update() { UpdateComputeShader(); }
/// <summary>
/// 每帧调用一次,设置相机矩阵
/// </summary>
private void UpdateComputeShader()
{
shader.SetMatrix(matixNameID, mainCamera.projectionMatrix * mainCamera.worldToCameraMatrix);
shader.Dispatch(kID, ThreadGroupSize, ThreadGroupSize, 1);
}
? ? ? ? 这个C#代码就比较简单了,简单看一看就OK了。
? ? ? ? 之后运行起来看看:
? ? ? ? ?可以看到可视范围(白色)就随着相机的变化而改变了。
4、将计算结果返回
? ? ? ? 然而,以上通过ComputeShader计算的结果仅仅是一张图片而已,C# 里面并不能获取到具体的值。在某些情况下(如显示小地图),这种情况就已经OK了。但如果我们要知道哪些坐标点是在相机范围内的,这个就需要使用ComputeBuffer来处理了。
? ? ? ? 在C#代码里做以下补充:
……
private const string ComputeBufferName = "VisableCellBuffer";
private int ComputeBufferID;
private ComputeBuffer AppendBuffer;
……
private void RunComputeShader()
{
……
//这里的初始化 AppendBuffer 的时候就要传入理论最大值,不然后续读取的时候会失败;
AppendBuffer = new ComputeBuffer(TextureSize * TextureSize, sizeof(int), ComputeBufferType.Append);
ComputeBufferID = Shader.PropertyToID(ComputeBufferName);
……
}
private void UpdateComputeShader()
{
……
SetAppedBuffer();
……
}
private void SetAppedBuffer()
{
AppendBuffer.SetCounterValue(0);
shader.SetBuffer(kID, ComputeBufferID, AppendBuffer);
}
?在ComputeShader里需要做以下补充:
……
AppendStructuredBuffer<int2> VisableCellBuffer;
……
[numthreads(8,8,1)]
void WolrdMapVisable (uint3 id : SV_DispatchThreadID)
{
……
if(IsVisialbe)
{
//把可见的格子转换成int值,然后存进VisableCellBuffer里面。
int2 index=x*10000+z;
VisableCellBuffer.Append(index);
}
……
}
????????这样就可以把显示个格子转成一个坐标ID传出来了。
????????然后读取方法如下:
private int[] ArrRet = new int[ThreadGroupSize];
private void ReadAppendBuffer()
{
var countBuffer = new ComputeBuffer(1, sizeof(int), ComputeBufferType.IndirectArguments);
ComputeBuffer.CopyCount(AppendBuffer, countBuffer, 0);
//通过这个方法拿到第一个数据,就是AppendBuffer的数量
int[] counter = new int[1] { 0 };//ToDo,这里还可以继续优化;
countBuffer.GetData(counter);
int leftCount = counter[0];
int startIndex = 0;
while (leftCount > 0)
{
int leftSize = Mathf.Min(leftCount, ThreadGroupSize);
AppendBuffer.GetData(ArrRet, 0, startIndex, leftSize);//分几次读取出来;
for (int i = 0; i < ThreadGroupSize; i++)
{
//Debug.Log($"[{startIndex}|{i}] {ArrRet[i]}");
}
leftCount = leftCount - ThreadGroupSize;
startIndex = startIndex + ThreadGroupSize;
}
}
????????这样就可以读取ComputeShader的计算结果了。
PS:
? ? ? ? 通过ComputeBuffer读取其实有一定的性能消耗,如果数据太大则需要考虑一下是否合算。
?
?
|