【Unity】万人同屏, 从入门到放弃之——自定义BatchRendererGroup合批渲染海量物体

这篇具有很好参考价值的文章主要介绍了【Unity】万人同屏, 从入门到放弃之——自定义BatchRendererGroup合批渲染海量物体。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Unity万人同屏动态避障 GPU动画 Entities Graphics高性能合批渲染插件的使用_哔哩哔哩_bilibili

由于Dots的限制太多,对于需要dlc或热更的项目来说,Dots就爱莫能助。能不能不用Entities,只用Entities Graphics呢?

当然是可以的,Entities Graphics背后使用的接口就是Batch Renderer Group; 

自定义BatchRenderGroup合批渲染, 可以参考Unity官方文档:Initializing a BatchRendererGroup object - Unity 手册

1. 创建一个BatchRenderGroup对象和Graphics Buffer:

m_BRG = new BatchRendererGroup(this.OnPerformCulling, IntPtr.Zero);

m_InstanceData = new GraphicsBuffer(GraphicsBuffer.Target.Raw,
            BufferCountForInstances(kBytesPerInstance, kNumInstances, kExtraBytes),
            sizeof(int));

2. 注册需要渲染的Mesh和对应的Material:

m_MeshID = m_BRG.RegisterMesh(mesh);
m_MaterialID = m_BRG.RegisterMaterial(material); 

3. 为所有需要渲染的目标创建矩阵数组并传入Graphics Buffer里:

 m_InstanceData.SetData(objectToWorld, 0, (int)(byteAddressObjectToWorld / kSizeOfPackedMatrix), objectToWorld.Length);
        m_InstanceData.SetData(worldToObject, 0, (int)(byteAddressWorldToObject / kSizeOfPackedMatrix), worldToObject.Length);

4. 把Graphics Buffer添加到BatchRenderGroup进行批次渲染:

m_BatchID = m_BRG.AddBatch(metadata, m_InstanceData.bufferHandle);

创建BatchRenderGroup需要指定一个OnPerformCulling,在相机Culling时自动回调,这里可以直接使用Unity手册里的示例代码:Creating draw commands - Unity 手册

这里我主要测试的使用BatchRenderGroup合批渲染的性能,使用Job System多线程并行修改矩阵数组的位置和旋转,以控制小人移动起来。

控制小人移动的Job System代码如下:

[BurstCompile]
partial struct RandomMoveJob : IJobParallelFor
{
    [ReadOnly]
    public Unity.Mathematics.Random random;
    [ReadOnly]
    public float4 randomPostionRange;
    [ReadOnly]
    public float m_DeltaTime;

    public NativeArray<Matrix4x4> matrices;
    public NativeArray<float3> targetMovePoints;
    public NativeArray<PackedMatrix> obj2WorldArr;
    public NativeArray<PackedMatrix> world2ObjArr;
    [BurstCompile]
    public void Execute(int index)
    {
        float3 curPos = matrices[index].GetPosition();
        float3 dir = targetMovePoints[index] - curPos;
        if (Unity.Mathematics.math.lengthsq(dir) < 0.4f)
        {
            var newTargetPos = targetMovePoints[index];
            newTargetPos.x = random.NextFloat(randomPostionRange.x, randomPostionRange.y);
            newTargetPos.z = random.NextFloat(randomPostionRange.z, randomPostionRange.w);
            targetMovePoints[index] = newTargetPos;
        }

        dir = math.normalizesafe(targetMovePoints[index] - curPos, Vector3.forward);
        curPos += dir * m_DeltaTime;// math.lerp(curPos, targetMovePoints[index], m_DeltaTime);

        var mat = matrices[index];
        mat.SetTRS(curPos, Quaternion.LookRotation(dir), Vector3.one);
        matrices[index] = mat;
        var item = obj2WorldArr[index];
        item.SetData(mat);
        obj2WorldArr[index] = item;

        item = world2ObjArr[index];
        item.SetData(mat.inverse);
        world2ObjArr[index] = item;
    }
}

然后在主线程Update每帧Jobs逻辑,把Jobs运算结果传入Graphics Buffer更新即可:

private void Update()
    {
        NativeArray<Matrix4x4> tempMatrices = new NativeArray<Matrix4x4>(matrices, Allocator.TempJob);
        NativeArray<float3> tempTargetPoints = new NativeArray<float3>(m_TargetPoints, Allocator.TempJob);//worldToObject
        NativeArray<PackedMatrix> tempobjectToWorldArr = new NativeArray<PackedMatrix>(matrices.Length, Allocator.TempJob);
        NativeArray<PackedMatrix> tempWorldToObjectArr = new NativeArray<PackedMatrix>(matrices.Length, Allocator.TempJob);
        random = new Unity.Mathematics.Random((uint)Time.frameCount);
        var moveJob = new RandomMoveJob
        {
            matrices = tempMatrices,
            targetMovePoints = tempTargetPoints,
            random = random,
            m_DeltaTime = Time.deltaTime * 4f,
            randomPostionRange = m_randomRange,
            obj2WorldArr = tempobjectToWorldArr,
            world2ObjArr = tempWorldToObjectArr
        };
        var moveJobHandle = moveJob.Schedule(tempMatrices.Length, 64);
        moveJobHandle.Complete();
        matrices = moveJob.matrices.ToArray();
        m_TargetPoints = moveJob.targetMovePoints.ToArray();
        m_InstanceData.SetData(moveJob.obj2WorldArr, 0, (int)(byteAddressObjectToWorld / kSizeOfPackedMatrix), objectToWorld.Length);
        m_InstanceData.SetData(moveJob.world2ObjArr, 0, (int)(byteAddressWorldToObject / kSizeOfPackedMatrix), worldToObject.Length);
        tempMatrices.Dispose();
        tempTargetPoints.Dispose();
        tempobjectToWorldArr.Dispose();
        tempWorldToObjectArr.Dispose();
    }

Okay,跑起来看看:

【Unity】万人同屏, 从入门到放弃之——自定义BatchRendererGroup合批渲染海量物体,Unity,unity,游戏引擎,Unity Dots,万人同屏

 瞬间惊呆了,你没看错,使用Batch Renderer Group创建一万的小人居然能跑600多帧!!! 

难道万人同屏要成了?继续加大药量,创建10万个带有移动逻辑的小人:

【Unity】万人同屏, 从入门到放弃之——自定义BatchRendererGroup合批渲染海量物体,Unity,unity,游戏引擎,Unity Dots,万人同屏

 10万个奔跑的3D人物,仍然有100帧以上,有23个线程并行计算移动。

看看性能分析:

【Unity】万人同屏, 从入门到放弃之——自定义BatchRendererGroup合批渲染海量物体,Unity,unity,游戏引擎,Unity Dots,万人同屏

 当数量级庞大时,即使Job System + Burst编译再怎么开挂,主线程也会拖后腿的。

100万的压迫感,虽然已经成PPT了:

【Unity】万人同屏, 从入门到放弃之——自定义BatchRendererGroup合批渲染海量物体,Unity,unity,游戏引擎,Unity Dots,万人同屏

 难道万人同屏行业难题的门槛就这么被Unity Dots打下来了??

非也,上移动端测试:

同样1万个小人,PC端能达到惊人的600帧,而Android最强骁龙8 Gen2只有10多帧,而且工作线程数才5个; 当数量3000人时,手机端帧数46帧左右,相比传统方式没有任何提升!没错,没有任何提升。

【Unity】万人同屏, 从入门到放弃之——自定义BatchRendererGroup合批渲染海量物体,Unity,unity,游戏引擎,Unity Dots,万人同屏

Profiler中可以看到,瓶颈依然是GPU。 而Entities Graphics内部也是通过Batch Renderer Group接口实现,由此可以推断,被吹爆的Entities在移动端因该也是"水土不服":

【Unity】万人同屏, 从入门到放弃之——自定义BatchRendererGroup合批渲染海量物体,Unity,unity,游戏引擎,Unity Dots,万人同屏

 结论:

目前为止,我认为使用自定义BatchRendererGroup合批是PC端万人同屏的最优解了。

但是手机端性能瓶颈任重道远。手机端放弃!

最后附上本文BatchRendererGroup测试代码:文章来源地址https://www.toymoban.com/news/detail-714293.html

using System;
using TMPro;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using Unity.Jobs.LowLevel.Unsafe;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Rendering;

// The PackedMatrix is a convenience type that converts matrices into
// the format that Unity-provided SRP shaders expect.
struct PackedMatrix
{
    public float c0x;
    public float c0y;
    public float c0z;
    public float c1x;
    public float c1y;
    public float c1z;
    public float c2x;
    public float c2y;
    public float c2z;
    public float c3x;
    public float c3y;
    public float c3z;

    public PackedMatrix(Matrix4x4 m)
    {
        c0x = m.m00;
        c0y = m.m10;
        c0z = m.m20;
        c1x = m.m01;
        c1y = m.m11;
        c1z = m.m21;
        c2x = m.m02;
        c2y = m.m12;
        c2z = m.m22;
        c3x = m.m03;
        c3y = m.m13;
        c3z = m.m23;
    }

    public void SetData(Matrix4x4 m)
    {
        c0x = m.m00;
        c0y = m.m10;
        c0z = m.m20;
        c1x = m.m01;
        c1y = m.m11;
        c1z = m.m21;
        c2x = m.m02;
        c2y = m.m12;
        c2z = m.m22;
        c3x = m.m03;
        c3y = m.m13;
        c3z = m.m23;
    }
}
public class SimpleBRGExample : MonoBehaviour
{
    public Mesh mesh;
    public Material material;
    public TextMeshProUGUI text;
    public TextMeshProUGUI workerCountText;
    private BatchRendererGroup m_BRG;

    private GraphicsBuffer m_InstanceData;
    private BatchID m_BatchID;
    private BatchMeshID m_MeshID;
    private BatchMaterialID m_MaterialID;

    // Some helper constants to make calculations more convenient.
    private const int kSizeOfMatrix = sizeof(float) * 4 * 4;
    private const int kSizeOfPackedMatrix = sizeof(float) * 4 * 3;
    private const int kSizeOfFloat4 = sizeof(float) * 4;
    private const int kBytesPerInstance = (kSizeOfPackedMatrix * 2) + kSizeOfFloat4;
    private const int kExtraBytes = kSizeOfMatrix * 2;
    [SerializeField] private int kNumInstances = 20000;
    [SerializeField] private int m_RowCount = 200;
    private Matrix4x4[] matrices;
    private PackedMatrix[] objectToWorld;
    private PackedMatrix[] worldToObject;
    private Vector4[] colors;

    private void Start()
    {
        m_BRG = new BatchRendererGroup(this.OnPerformCulling, IntPtr.Zero);
        m_MeshID = m_BRG.RegisterMesh(mesh);
        m_MaterialID = m_BRG.RegisterMaterial(material);
        AllocateInstanceDateBuffer();
        PopulateInstanceDataBuffer();

        text.text = kNumInstances.ToString();
        random = new Unity.Mathematics.Random(1);
        m_TargetPoints = new float3[kNumInstances];
        var offset = new Vector3(m_RowCount, 0, Mathf.CeilToInt(kNumInstances / (float)m_RowCount)) * 0.5f;
        m_randomRange = new float4(-offset.x, offset.x, -offset.z, offset.z);
        for (int i = 0; i < m_TargetPoints.Length; i++)
        {
            var newTargetPos = new float3();
            newTargetPos.x = random.NextFloat(m_randomRange.x, m_randomRange.y);
            newTargetPos.z = random.NextFloat(m_randomRange.z, m_randomRange.w);
            m_TargetPoints[i] = newTargetPos;
        }
        
    }

    float3[] m_TargetPoints;
    Unity.Mathematics.Random random;
    Vector4 m_randomRange;
    private uint byteAddressObjectToWorld;
    private uint byteAddressWorldToObject;
    private uint byteAddressColor;

    private void Update()
    {
        NativeArray<Matrix4x4> tempMatrices = new NativeArray<Matrix4x4>(matrices, Allocator.TempJob);
        NativeArray<float3> tempTargetPoints = new NativeArray<float3>(m_TargetPoints, Allocator.TempJob);//worldToObject
        NativeArray<PackedMatrix> tempobjectToWorldArr = new NativeArray<PackedMatrix>(matrices.Length, Allocator.TempJob);
        NativeArray<PackedMatrix> tempWorldToObjectArr = new NativeArray<PackedMatrix>(matrices.Length, Allocator.TempJob);
        random = new Unity.Mathematics.Random((uint)Time.frameCount);
        var moveJob = new RandomMoveJob
        {
            matrices = tempMatrices,
            targetMovePoints = tempTargetPoints,
            random = random,
            m_DeltaTime = Time.deltaTime * 4f,
            randomPostionRange = m_randomRange,
            obj2WorldArr = tempobjectToWorldArr,
            world2ObjArr = tempWorldToObjectArr
        };
        var moveJobHandle = moveJob.Schedule(tempMatrices.Length, 64);
        moveJobHandle.Complete();
        matrices = moveJob.matrices.ToArray();
        m_TargetPoints = moveJob.targetMovePoints.ToArray();
        m_InstanceData.SetData(moveJob.obj2WorldArr, 0, (int)(byteAddressObjectToWorld / kSizeOfPackedMatrix), objectToWorld.Length);
        m_InstanceData.SetData(moveJob.world2ObjArr, 0, (int)(byteAddressWorldToObject / kSizeOfPackedMatrix), worldToObject.Length);
        tempMatrices.Dispose();
        tempTargetPoints.Dispose();
        tempobjectToWorldArr.Dispose();
        tempWorldToObjectArr.Dispose();

        workerCountText.text = $"JobWorkerCount:{JobsUtility.JobWorkerCount}";
    }

    private void AllocateInstanceDateBuffer()
    {
        m_InstanceData = new GraphicsBuffer(GraphicsBuffer.Target.Raw,
            BufferCountForInstances(kBytesPerInstance, kNumInstances, kExtraBytes),
            sizeof(int));
    }
    private void RefreshData()
    {
        m_InstanceData.SetData(objectToWorld, 0, (int)(byteAddressObjectToWorld / kSizeOfPackedMatrix), objectToWorld.Length);
        m_InstanceData.SetData(worldToObject, 0, (int)(byteAddressWorldToObject / kSizeOfPackedMatrix), worldToObject.Length);
    }
    private void PopulateInstanceDataBuffer()
    {
        // Place a zero matrix at the start of the instance data buffer, so loads from address 0 return zero.
        var zero = new Matrix4x4[1] { Matrix4x4.zero };

        // Create transform matrices for three example instances.
        matrices = new Matrix4x4[kNumInstances];
        // Convert the transform matrices into the packed format that shaders expects.
        objectToWorld = new PackedMatrix[kNumInstances];
        // Also create packed inverse matrices.
        worldToObject = new PackedMatrix[kNumInstances];
        // Make all instances have unique colors.
        colors = new Vector4[kNumInstances];

        var offset = new Vector3(m_RowCount, 0, Mathf.CeilToInt(kNumInstances / (float)m_RowCount)) * 0.5f;
        for (int i = 0; i < kNumInstances; i++)
        {
            matrices[i] = Matrix4x4.Translate(new Vector3(i % m_RowCount, 0, i / m_RowCount) - offset);
            objectToWorld[i] = new PackedMatrix(matrices[i]);
            worldToObject[i] = new PackedMatrix(matrices[0].inverse);
            colors[i] = UnityEngine.Random.ColorHSV();
        }

        // In this simple example, the instance data is placed into the buffer like this:
        // Offset | Description
        //      0 | 64 bytes of zeroes, so loads from address 0 return zeroes
        //     64 | 32 uninitialized bytes to make working with SetData easier, otherwise unnecessary
        //     96 | unity_ObjectToWorld, three packed float3x4 matrices
        //    240 | unity_WorldToObject, three packed float3x4 matrices
        //    384 | _BaseColor, three float4s

        // Calculates start addresses for the different instanced properties. unity_ObjectToWorld starts at 
        // address 96 instead of 64 which means 32 bits are left uninitialized. This is because the 
        // computeBufferStartIndex parameter requires the start offset to be divisible by the size of the source
        // array element type. In this case, it's the size of PackedMatrix, which is 48.
        byteAddressObjectToWorld = kSizeOfPackedMatrix * 2;
        byteAddressWorldToObject = byteAddressObjectToWorld + kSizeOfPackedMatrix * (uint)kNumInstances;
        byteAddressColor = byteAddressWorldToObject + kSizeOfPackedMatrix * (uint)kNumInstances;

        // Upload the instance data to the GraphicsBuffer so the shader can load them.
        m_InstanceData.SetData(zero, 0, 0, 1);
        m_InstanceData.SetData(objectToWorld, 0, (int)(byteAddressObjectToWorld / kSizeOfPackedMatrix), objectToWorld.Length);
        m_InstanceData.SetData(worldToObject, 0, (int)(byteAddressWorldToObject / kSizeOfPackedMatrix), worldToObject.Length);
        m_InstanceData.SetData(colors, 0, (int)(byteAddressColor / kSizeOfFloat4), colors.Length);

        // Set up metadata values to point to the instance data. Set the most significant bit 0x80000000 in each
        // which instructs the shader that the data is an array with one value per instance, indexed by the instance index.
        // Any metadata values that the shader uses and not set here will be zero. When such a value is used with
        // UNITY_ACCESS_DOTS_INSTANCED_PROP (i.e. without a default), the shader interprets the
        // 0x00000000 metadata value and loads from the start of the buffer. The start of the buffer which is
        // is a zero matrix so this sort of load is guaranteed to return zero, which is a reasonable default value.
        var metadata = new NativeArray<MetadataValue>(3, Allocator.Temp);
        metadata[0] = new MetadataValue { NameID = Shader.PropertyToID("unity_ObjectToWorld"), Value = 0x80000000 | byteAddressObjectToWorld, };
        metadata[1] = new MetadataValue { NameID = Shader.PropertyToID("unity_WorldToObject"), Value = 0x80000000 | byteAddressWorldToObject, };
        metadata[2] = new MetadataValue { NameID = Shader.PropertyToID("_BaseColor"), Value = 0x80000000 | byteAddressColor, };

        // Finally, create a batch for the instances, and make the batch use the GraphicsBuffer with the
        // instance data, as well as the metadata values that specify where the properties are. 
        m_BatchID = m_BRG.AddBatch(metadata, m_InstanceData.bufferHandle);
    }

    // Raw buffers are allocated in ints. This is a utility method that calculates
    // the required number of ints for the data.
    int BufferCountForInstances(int bytesPerInstance, int numInstances, int extraBytes = 0)
    {
        // Round byte counts to int multiples
        bytesPerInstance = (bytesPerInstance + sizeof(int) - 1) / sizeof(int) * sizeof(int);
        extraBytes = (extraBytes + sizeof(int) - 1) / sizeof(int) * sizeof(int);
        int totalBytes = bytesPerInstance * numInstances + extraBytes;
        return totalBytes / sizeof(int);
    }


    private void OnDisable()
    {
        m_BRG.Dispose();
    }

    public unsafe JobHandle OnPerformCulling(
        BatchRendererGroup rendererGroup,
        BatchCullingContext cullingContext,
        BatchCullingOutput cullingOutput,
        IntPtr userContext)
    {
        // UnsafeUtility.Malloc() requires an alignment, so use the largest integer type's alignment
        // which is a reasonable default.
        int alignment = UnsafeUtility.AlignOf<long>();

        // Acquire a pointer to the BatchCullingOutputDrawCommands struct so you can easily
        // modify it directly.
        var drawCommands = (BatchCullingOutputDrawCommands*)cullingOutput.drawCommands.GetUnsafePtr();
        // Allocate memory for the output arrays. In a more complicated implementation, you would calculate
        // the amount of memory to allocate dynamically based on what is visible.
        // This example assumes that all of the instances are visible and thus allocates
        // memory for each of them. The necessary allocations are as follows:
        // - a single draw command (which draws kNumInstances instances)
        // - a single draw range (which covers our single draw command)
        // - kNumInstances visible instance indices.
        // You must always allocate the arrays using Allocator.TempJob.
        drawCommands->drawCommands = (BatchDrawCommand*)UnsafeUtility.Malloc(UnsafeUtility.SizeOf<BatchDrawCommand>(), alignment, Allocator.TempJob);
        drawCommands->drawRanges = (BatchDrawRange*)UnsafeUtility.Malloc(UnsafeUtility.SizeOf<BatchDrawRange>(), alignment, Allocator.TempJob);
        drawCommands->visibleInstances = (int*)UnsafeUtility.Malloc(kNumInstances * sizeof(int), alignment, Allocator.TempJob);
        drawCommands->drawCommandPickingInstanceIDs = null;

        drawCommands->drawCommandCount = 1;
        drawCommands->drawRangeCount = 1;
        drawCommands->visibleInstanceCount = kNumInstances;

        // This example doens't use depth sorting, so it leaves instanceSortingPositions as null.
        drawCommands->instanceSortingPositions = null;
        drawCommands->instanceSortingPositionFloatCount = 0;

        // Configure the single draw command to draw kNumInstances instances
        // starting from offset 0 in the array, using the batch, material and mesh
        // IDs registered in the Start() method. It doesn't set any special flags.
        drawCommands->drawCommands[0].visibleOffset = 0;
        drawCommands->drawCommands[0].visibleCount = (uint)kNumInstances;
        drawCommands->drawCommands[0].batchID = m_BatchID;
        drawCommands->drawCommands[0].materialID = m_MaterialID;
        drawCommands->drawCommands[0].meshID = m_MeshID;
        drawCommands->drawCommands[0].submeshIndex = 0;
        drawCommands->drawCommands[0].splitVisibilityMask = 0xff;
        drawCommands->drawCommands[0].flags = 0;
        drawCommands->drawCommands[0].sortingPosition = 0;

        // Configure the single draw range to cover the single draw command which
        // is at offset 0.
        drawCommands->drawRanges[0].drawCommandsBegin = 0;
        drawCommands->drawRanges[0].drawCommandsCount = 1;

        // This example doesn't care about shadows or motion vectors, so it leaves everything
        // at the default zero values, except the renderingLayerMask which it sets to all ones
        // so Unity renders the instances regardless of mask settings.
        drawCommands->drawRanges[0].filterSettings = new BatchFilterSettings { renderingLayerMask = 0xffffffff, };

        // Finally, write the actual visible instance indices to the array. In a more complicated
        // implementation, this output would depend on what is visible, but this example
        // assumes that everything is visible.
        for (int i = 0; i < kNumInstances; ++i)
            drawCommands->visibleInstances[i] = i;

        // This simple example doesn't use jobs, so it returns an empty JobHandle.
        // Performance-sensitive applications are encouraged to use Burst jobs to implement
        // culling and draw command output. In this case, this function returns a
        // handle here that completes when the Burst jobs finish.
        return new JobHandle();
    }
}
[BurstCompile]
partial struct RandomMoveJob : IJobParallelFor
{
    [ReadOnly]
    public Unity.Mathematics.Random random;
    [ReadOnly]
    public float4 randomPostionRange;
    [ReadOnly]
    public float m_DeltaTime;

    public NativeArray<Matrix4x4> matrices;
    public NativeArray<float3> targetMovePoints;
    public NativeArray<PackedMatrix> obj2WorldArr;
    public NativeArray<PackedMatrix> world2ObjArr;
    [BurstCompile]
    public void Execute(int index)
    {
        float3 curPos = matrices[index].GetPosition();
        float3 dir = targetMovePoints[index] - curPos;
        if (Unity.Mathematics.math.lengthsq(dir) < 0.4f)
        {
            var newTargetPos = targetMovePoints[index];
            newTargetPos.x = random.NextFloat(randomPostionRange.x, randomPostionRange.y);
            newTargetPos.z = random.NextFloat(randomPostionRange.z, randomPostionRange.w);
            targetMovePoints[index] = newTargetPos;
        }

        dir = math.normalizesafe(targetMovePoints[index] - curPos, Vector3.forward);
        curPos += dir * m_DeltaTime;// math.lerp(curPos, targetMovePoints[index], m_DeltaTime);

        var mat = matrices[index];
        mat.SetTRS(curPos, Quaternion.LookRotation(dir), Vector3.one);
        matrices[index] = mat;
        var item = obj2WorldArr[index];
        item.SetData(mat);
        obj2WorldArr[index] = item;

        item = world2ObjArr[index];
        item.SetData(mat.inverse);
        world2ObjArr[index] = item;
    }
}

到了这里,关于【Unity】万人同屏, 从入门到放弃之——自定义BatchRendererGroup合批渲染海量物体的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Unity BatchRendererGroup 在低端设备上也实现高帧率

    在这篇文章中,我们描述了一个小型射击游戏样本,它可以动画和渲染几个交互式对象。许多演示只针对高端pc,但这里的目标是在使用GLES 3.0的廉价手机上实现高帧率。这个例子使用了 BatchRendererGroup, Burst 编译器和c#作业系统。它运行在Unity 2022.3中,不需要实体或实体。图形

    2024年02月20日
    浏览(50)
  • Unity 多相机 同屏显示

    一 首先了解: 相机和Canvas 的渲染先后关系 什么是相机的 渲染顺序 ? 答:简单理解就是 用毛刷  刷墙面,先刷的,会被后刷的 挡住 。 列如:相机01: 先渲染的大海,相机02:后渲染的比基尼美女。那么呈现的效果就是:比基尼美女站在大海前面。 毛刷理解方式:一面空

    2024年02月16日
    浏览(24)
  • Unity 万人寻路(dots版本)

    时间:2023-7-29 北京下雨,公司停电,回家早了,更新一下。 寻路模块,还是前前后后,打磨了2个月,中间穿插着搞很多其他的功能,有些螺旋迭代的味道。 进入正题: 设计团队,会从游戏性角度考虑,提出很多需求。 1,单位之间的规避,模型不能穿插 2,建筑物规避,模

    2024年02月08日
    浏览(30)
  • 微软放弃WPF了?自定义控件库有前途

            自 Microsoft 于 2006 年将 WPF作为 .NET 框架的一部分引入以来,该平台在 Windows 开发人员中越来越受欢迎。令我惊讶的是,到2015年为止.NET 4.6版本升级后,WPF再也没有版本升级过。最近我一直在寻找 WPF 主要版本及其迄今为止的进展。令我惊讶的是,在互联网没有资源

    2024年02月05日
    浏览(60)
  • Docker从入门到放弃

    看完我这里,就彻底入门了,如果对你有帮助,欢迎点赞+收藏❤️+评论噢~ 按部就班,先安装.. 在CentOS 7上安装Docker主要涉及添加Docker的官方仓库,然后从该仓库安装Docker CE(社区版) 1.安装所需的包: yum-utils 提供 yum-config-manager 工具,而 device-mapper-persistent-data 和 lvm2 是

    2024年04月14日
    浏览(28)
  • 性能测试从入门到放弃(一)

    性能测试是观察系统在给定的运行环境下,测试场景中系统的性能表现是否符与预期目标一致,评判是否存在性能缺陷,根据结果识别性能瓶颈。 运行环境:硬件服务器、操作系统、网络、数据库、web服务器、应用服务器等都为系统的运行环境。 测试场景:用户如何使用系统

    2023年04月09日
    浏览(33)
  • 【Docker】 Docker 入门到放弃

    Docker 容器生命周期 // 查看镜像 // pull 下载镜像 // 查看所有的镜像 // images 查看镜像 ID // search 搜索镜像 // rmi -f 删除镜像 // tag 标记本地镜像 // save 保存镜像 // load 加载镜像 // events 从服务器获取实时事件 注意 注意: 先了有镜像 – 才能创建容器 ** pull 下载镜像 ** run 创建容

    2024年01月23日
    浏览(25)
  • 大数据入门到放弃第一天:linux的入门

            虚拟机(Virtual Machine,简称VM)是一种在物理计算机上模拟运行的软件实体。它通过虚拟化技术,将一台物理计算机划分为多个虚拟的逻辑计算环境,每个环境都可以独立运行操作系统和应用程序。         虚拟机使得一台物理计算机可以同时运行多个操作系

    2024年02月05日
    浏览(31)
  • 【pyspark从入门到放弃】DataFrame

    pyspark支持通过pypip、conda下载,或者手动下载。 笔者通过 pip install 命令从pypip下载并配置安装了3.5.0版本的Spark。 使用spark的第一步就是拿到一个 SparkSession 对象。最简单的方法是 即,直接使用默认参数创建实例。也可以做一些配置,比如 创建DataFrame DataFrame 是类似 pandas 库中

    2024年01月16日
    浏览(29)
  • 深度学习从入门到不想放弃-6

       这节要讲完距离基础部分就真完事了,不继续在基础中求得基础了,我发现也没人看        书接前文深度学习从入门到不想放弃-5 (qq.com)        前文书写到要合理的设计特征是什么概念,我们再拿两个例子复习一下       比如一个卖车网站,上节我们讲过对物体识别

    2024年01月19日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包