当前位置:   article > 正文

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

unity batch renderer group

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代码如下:

  1. [BurstCompile]
  2. partial struct RandomMoveJob : IJobParallelFor
  3. {
  4. [ReadOnly]
  5. public Unity.Mathematics.Random random;
  6. [ReadOnly]
  7. public float4 randomPostionRange;
  8. [ReadOnly]
  9. public float m_DeltaTime;
  10. public NativeArray<Matrix4x4> matrices;
  11. public NativeArray<float3> targetMovePoints;
  12. public NativeArray<PackedMatrix> obj2WorldArr;
  13. public NativeArray<PackedMatrix> world2ObjArr;
  14. [BurstCompile]
  15. public void Execute(int index)
  16. {
  17. float3 curPos = matrices[index].GetPosition();
  18. float3 dir = targetMovePoints[index] - curPos;
  19. if (Unity.Mathematics.math.lengthsq(dir) < 0.4f)
  20. {
  21. var newTargetPos = targetMovePoints[index];
  22. newTargetPos.x = random.NextFloat(randomPostionRange.x, randomPostionRange.y);
  23. newTargetPos.z = random.NextFloat(randomPostionRange.z, randomPostionRange.w);
  24. targetMovePoints[index] = newTargetPos;
  25. }
  26. dir = math.normalizesafe(targetMovePoints[index] - curPos, Vector3.forward);
  27. curPos += dir * m_DeltaTime;// math.lerp(curPos, targetMovePoints[index], m_DeltaTime);
  28. var mat = matrices[index];
  29. mat.SetTRS(curPos, Quaternion.LookRotation(dir), Vector3.one);
  30. matrices[index] = mat;
  31. var item = obj2WorldArr[index];
  32. item.SetData(mat);
  33. obj2WorldArr[index] = item;
  34. item = world2ObjArr[index];
  35. item.SetData(mat.inverse);
  36. world2ObjArr[index] = item;
  37. }
  38. }

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

  1. private void Update()
  2. {
  3. NativeArray<Matrix4x4> tempMatrices = new NativeArray<Matrix4x4>(matrices, Allocator.TempJob);
  4. NativeArray<float3> tempTargetPoints = new NativeArray<float3>(m_TargetPoints, Allocator.TempJob);//worldToObject
  5. NativeArray<PackedMatrix> tempobjectToWorldArr = new NativeArray<PackedMatrix>(matrices.Length, Allocator.TempJob);
  6. NativeArray<PackedMatrix> tempWorldToObjectArr = new NativeArray<PackedMatrix>(matrices.Length, Allocator.TempJob);
  7. random = new Unity.Mathematics.Random((uint)Time.frameCount);
  8. var moveJob = new RandomMoveJob
  9. {
  10. matrices = tempMatrices,
  11. targetMovePoints = tempTargetPoints,
  12. random = random,
  13. m_DeltaTime = Time.deltaTime * 4f,
  14. randomPostionRange = m_randomRange,
  15. obj2WorldArr = tempobjectToWorldArr,
  16. world2ObjArr = tempWorldToObjectArr
  17. };
  18. var moveJobHandle = moveJob.Schedule(tempMatrices.Length, 64);
  19. moveJobHandle.Complete();
  20. matrices = moveJob.matrices.ToArray();
  21. m_TargetPoints = moveJob.targetMovePoints.ToArray();
  22. m_InstanceData.SetData(moveJob.obj2WorldArr, 0, (int)(byteAddressObjectToWorld / kSizeOfPackedMatrix), objectToWorld.Length);
  23. m_InstanceData.SetData(moveJob.world2ObjArr, 0, (int)(byteAddressWorldToObject / kSizeOfPackedMatrix), worldToObject.Length);
  24. tempMatrices.Dispose();
  25. tempTargetPoints.Dispose();
  26. tempobjectToWorldArr.Dispose();
  27. tempWorldToObjectArr.Dispose();
  28. }

Okay,跑起来看看:

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

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

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

看看性能分析:

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

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

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

非也,上移动端测试:

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

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

 结论:

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

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

最后附上本文BatchRendererGroup测试代码:

  1. using System;
  2. using TMPro;
  3. using Unity.Burst;
  4. using Unity.Collections;
  5. using Unity.Collections.LowLevel.Unsafe;
  6. using Unity.Jobs;
  7. using Unity.Jobs.LowLevel.Unsafe;
  8. using Unity.Mathematics;
  9. using UnityEngine;
  10. using UnityEngine.Rendering;
  11. // The PackedMatrix is a convenience type that converts matrices into
  12. // the format that Unity-provided SRP shaders expect.
  13. struct PackedMatrix
  14. {
  15. public float c0x;
  16. public float c0y;
  17. public float c0z;
  18. public float c1x;
  19. public float c1y;
  20. public float c1z;
  21. public float c2x;
  22. public float c2y;
  23. public float c2z;
  24. public float c3x;
  25. public float c3y;
  26. public float c3z;
  27. public PackedMatrix(Matrix4x4 m)
  28. {
  29. c0x = m.m00;
  30. c0y = m.m10;
  31. c0z = m.m20;
  32. c1x = m.m01;
  33. c1y = m.m11;
  34. c1z = m.m21;
  35. c2x = m.m02;
  36. c2y = m.m12;
  37. c2z = m.m22;
  38. c3x = m.m03;
  39. c3y = m.m13;
  40. c3z = m.m23;
  41. }
  42. public void SetData(Matrix4x4 m)
  43. {
  44. c0x = m.m00;
  45. c0y = m.m10;
  46. c0z = m.m20;
  47. c1x = m.m01;
  48. c1y = m.m11;
  49. c1z = m.m21;
  50. c2x = m.m02;
  51. c2y = m.m12;
  52. c2z = m.m22;
  53. c3x = m.m03;
  54. c3y = m.m13;
  55. c3z = m.m23;
  56. }
  57. }
  58. public class SimpleBRGExample : MonoBehaviour
  59. {
  60. public Mesh mesh;
  61. public Material material;
  62. public TextMeshProUGUI text;
  63. public TextMeshProUGUI workerCountText;
  64. private BatchRendererGroup m_BRG;
  65. private GraphicsBuffer m_InstanceData;
  66. private BatchID m_BatchID;
  67. private BatchMeshID m_MeshID;
  68. private BatchMaterialID m_MaterialID;
  69. // Some helper constants to make calculations more convenient.
  70. private const int kSizeOfMatrix = sizeof(float) * 4 * 4;
  71. private const int kSizeOfPackedMatrix = sizeof(float) * 4 * 3;
  72. private const int kSizeOfFloat4 = sizeof(float) * 4;
  73. private const int kBytesPerInstance = (kSizeOfPackedMatrix * 2) + kSizeOfFloat4;
  74. private const int kExtraBytes = kSizeOfMatrix * 2;
  75. [SerializeField] private int kNumInstances = 20000;
  76. [SerializeField] private int m_RowCount = 200;
  77. private Matrix4x4[] matrices;
  78. private PackedMatrix[] objectToWorld;
  79. private PackedMatrix[] worldToObject;
  80. private Vector4[] colors;
  81. private void Start()
  82. {
  83. m_BRG = new BatchRendererGroup(this.OnPerformCulling, IntPtr.Zero);
  84. m_MeshID = m_BRG.RegisterMesh(mesh);
  85. m_MaterialID = m_BRG.RegisterMaterial(material);
  86. AllocateInstanceDateBuffer();
  87. PopulateInstanceDataBuffer();
  88. text.text = kNumInstances.ToString();
  89. random = new Unity.Mathematics.Random(1);
  90. m_TargetPoints = new float3[kNumInstances];
  91. var offset = new Vector3(m_RowCount, 0, Mathf.CeilToInt(kNumInstances / (float)m_RowCount)) * 0.5f;
  92. m_randomRange = new float4(-offset.x, offset.x, -offset.z, offset.z);
  93. for (int i = 0; i < m_TargetPoints.Length; i++)
  94. {
  95. var newTargetPos = new float3();
  96. newTargetPos.x = random.NextFloat(m_randomRange.x, m_randomRange.y);
  97. newTargetPos.z = random.NextFloat(m_randomRange.z, m_randomRange.w);
  98. m_TargetPoints[i] = newTargetPos;
  99. }
  100. }
  101. float3[] m_TargetPoints;
  102. Unity.Mathematics.Random random;
  103. Vector4 m_randomRange;
  104. private uint byteAddressObjectToWorld;
  105. private uint byteAddressWorldToObject;
  106. private uint byteAddressColor;
  107. private void Update()
  108. {
  109. NativeArray<Matrix4x4> tempMatrices = new NativeArray<Matrix4x4>(matrices, Allocator.TempJob);
  110. NativeArray<float3> tempTargetPoints = new NativeArray<float3>(m_TargetPoints, Allocator.TempJob);//worldToObject
  111. NativeArray<PackedMatrix> tempobjectToWorldArr = new NativeArray<PackedMatrix>(matrices.Length, Allocator.TempJob);
  112. NativeArray<PackedMatrix> tempWorldToObjectArr = new NativeArray<PackedMatrix>(matrices.Length, Allocator.TempJob);
  113. random = new Unity.Mathematics.Random((uint)Time.frameCount);
  114. var moveJob = new RandomMoveJob
  115. {
  116. matrices = tempMatrices,
  117. targetMovePoints = tempTargetPoints,
  118. random = random,
  119. m_DeltaTime = Time.deltaTime * 4f,
  120. randomPostionRange = m_randomRange,
  121. obj2WorldArr = tempobjectToWorldArr,
  122. world2ObjArr = tempWorldToObjectArr
  123. };
  124. var moveJobHandle = moveJob.Schedule(tempMatrices.Length, 64);
  125. moveJobHandle.Complete();
  126. matrices = moveJob.matrices.ToArray();
  127. m_TargetPoints = moveJob.targetMovePoints.ToArray();
  128. m_InstanceData.SetData(moveJob.obj2WorldArr, 0, (int)(byteAddressObjectToWorld / kSizeOfPackedMatrix), objectToWorld.Length);
  129. m_InstanceData.SetData(moveJob.world2ObjArr, 0, (int)(byteAddressWorldToObject / kSizeOfPackedMatrix), worldToObject.Length);
  130. tempMatrices.Dispose();
  131. tempTargetPoints.Dispose();
  132. tempobjectToWorldArr.Dispose();
  133. tempWorldToObjectArr.Dispose();
  134. workerCountText.text = $"JobWorkerCount:{JobsUtility.JobWorkerCount}";
  135. }
  136. private void AllocateInstanceDateBuffer()
  137. {
  138. m_InstanceData = new GraphicsBuffer(GraphicsBuffer.Target.Raw,
  139. BufferCountForInstances(kBytesPerInstance, kNumInstances, kExtraBytes),
  140. sizeof(int));
  141. }
  142. private void RefreshData()
  143. {
  144. m_InstanceData.SetData(objectToWorld, 0, (int)(byteAddressObjectToWorld / kSizeOfPackedMatrix), objectToWorld.Length);
  145. m_InstanceData.SetData(worldToObject, 0, (int)(byteAddressWorldToObject / kSizeOfPackedMatrix), worldToObject.Length);
  146. }
  147. private void PopulateInstanceDataBuffer()
  148. {
  149. // Place a zero matrix at the start of the instance data buffer, so loads from address 0 return zero.
  150. var zero = new Matrix4x4[1] { Matrix4x4.zero };
  151. // Create transform matrices for three example instances.
  152. matrices = new Matrix4x4[kNumInstances];
  153. // Convert the transform matrices into the packed format that shaders expects.
  154. objectToWorld = new PackedMatrix[kNumInstances];
  155. // Also create packed inverse matrices.
  156. worldToObject = new PackedMatrix[kNumInstances];
  157. // Make all instances have unique colors.
  158. colors = new Vector4[kNumInstances];
  159. var offset = new Vector3(m_RowCount, 0, Mathf.CeilToInt(kNumInstances / (float)m_RowCount)) * 0.5f;
  160. for (int i = 0; i < kNumInstances; i++)
  161. {
  162. matrices[i] = Matrix4x4.Translate(new Vector3(i % m_RowCount, 0, i / m_RowCount) - offset);
  163. objectToWorld[i] = new PackedMatrix(matrices[i]);
  164. worldToObject[i] = new PackedMatrix(matrices[0].inverse);
  165. colors[i] = UnityEngine.Random.ColorHSV();
  166. }
  167. // In this simple example, the instance data is placed into the buffer like this:
  168. // Offset | Description
  169. // 0 | 64 bytes of zeroes, so loads from address 0 return zeroes
  170. // 64 | 32 uninitialized bytes to make working with SetData easier, otherwise unnecessary
  171. // 96 | unity_ObjectToWorld, three packed float3x4 matrices
  172. // 240 | unity_WorldToObject, three packed float3x4 matrices
  173. // 384 | _BaseColor, three float4s
  174. // Calculates start addresses for the different instanced properties. unity_ObjectToWorld starts at
  175. // address 96 instead of 64 which means 32 bits are left uninitialized. This is because the
  176. // computeBufferStartIndex parameter requires the start offset to be divisible by the size of the source
  177. // array element type. In this case, it's the size of PackedMatrix, which is 48.
  178. byteAddressObjectToWorld = kSizeOfPackedMatrix * 2;
  179. byteAddressWorldToObject = byteAddressObjectToWorld + kSizeOfPackedMatrix * (uint)kNumInstances;
  180. byteAddressColor = byteAddressWorldToObject + kSizeOfPackedMatrix * (uint)kNumInstances;
  181. // Upload the instance data to the GraphicsBuffer so the shader can load them.
  182. m_InstanceData.SetData(zero, 0, 0, 1);
  183. m_InstanceData.SetData(objectToWorld, 0, (int)(byteAddressObjectToWorld / kSizeOfPackedMatrix), objectToWorld.Length);
  184. m_InstanceData.SetData(worldToObject, 0, (int)(byteAddressWorldToObject / kSizeOfPackedMatrix), worldToObject.Length);
  185. m_InstanceData.SetData(colors, 0, (int)(byteAddressColor / kSizeOfFloat4), colors.Length);
  186. // Set up metadata values to point to the instance data. Set the most significant bit 0x80000000 in each
  187. // which instructs the shader that the data is an array with one value per instance, indexed by the instance index.
  188. // Any metadata values that the shader uses and not set here will be zero. When such a value is used with
  189. // UNITY_ACCESS_DOTS_INSTANCED_PROP (i.e. without a default), the shader interprets the
  190. // 0x00000000 metadata value and loads from the start of the buffer. The start of the buffer which is
  191. // is a zero matrix so this sort of load is guaranteed to return zero, which is a reasonable default value.
  192. var metadata = new NativeArray<MetadataValue>(3, Allocator.Temp);
  193. metadata[0] = new MetadataValue { NameID = Shader.PropertyToID("unity_ObjectToWorld"), Value = 0x80000000 | byteAddressObjectToWorld, };
  194. metadata[1] = new MetadataValue { NameID = Shader.PropertyToID("unity_WorldToObject"), Value = 0x80000000 | byteAddressWorldToObject, };
  195. metadata[2] = new MetadataValue { NameID = Shader.PropertyToID("_BaseColor"), Value = 0x80000000 | byteAddressColor, };
  196. // Finally, create a batch for the instances, and make the batch use the GraphicsBuffer with the
  197. // instance data, as well as the metadata values that specify where the properties are.
  198. m_BatchID = m_BRG.AddBatch(metadata, m_InstanceData.bufferHandle);
  199. }
  200. // Raw buffers are allocated in ints. This is a utility method that calculates
  201. // the required number of ints for the data.
  202. int BufferCountForInstances(int bytesPerInstance, int numInstances, int extraBytes = 0)
  203. {
  204. // Round byte counts to int multiples
  205. bytesPerInstance = (bytesPerInstance + sizeof(int) - 1) / sizeof(int) * sizeof(int);
  206. extraBytes = (extraBytes + sizeof(int) - 1) / sizeof(int) * sizeof(int);
  207. int totalBytes = bytesPerInstance * numInstances + extraBytes;
  208. return totalBytes / sizeof(int);
  209. }
  210. private void OnDisable()
  211. {
  212. m_BRG.Dispose();
  213. }
  214. public unsafe JobHandle OnPerformCulling(
  215. BatchRendererGroup rendererGroup,
  216. BatchCullingContext cullingContext,
  217. BatchCullingOutput cullingOutput,
  218. IntPtr userContext)
  219. {
  220. // UnsafeUtility.Malloc() requires an alignment, so use the largest integer type's alignment
  221. // which is a reasonable default.
  222. int alignment = UnsafeUtility.AlignOf<long>();
  223. // Acquire a pointer to the BatchCullingOutputDrawCommands struct so you can easily
  224. // modify it directly.
  225. var drawCommands = (BatchCullingOutputDrawCommands*)cullingOutput.drawCommands.GetUnsafePtr();
  226. // Allocate memory for the output arrays. In a more complicated implementation, you would calculate
  227. // the amount of memory to allocate dynamically based on what is visible.
  228. // This example assumes that all of the instances are visible and thus allocates
  229. // memory for each of them. The necessary allocations are as follows:
  230. // - a single draw command (which draws kNumInstances instances)
  231. // - a single draw range (which covers our single draw command)
  232. // - kNumInstances visible instance indices.
  233. // You must always allocate the arrays using Allocator.TempJob.
  234. drawCommands->drawCommands = (BatchDrawCommand*)UnsafeUtility.Malloc(UnsafeUtility.SizeOf<BatchDrawCommand>(), alignment, Allocator.TempJob);
  235. drawCommands->drawRanges = (BatchDrawRange*)UnsafeUtility.Malloc(UnsafeUtility.SizeOf<BatchDrawRange>(), alignment, Allocator.TempJob);
  236. drawCommands->visibleInstances = (int*)UnsafeUtility.Malloc(kNumInstances * sizeof(int), alignment, Allocator.TempJob);
  237. drawCommands->drawCommandPickingInstanceIDs = null;
  238. drawCommands->drawCommandCount = 1;
  239. drawCommands->drawRangeCount = 1;
  240. drawCommands->visibleInstanceCount = kNumInstances;
  241. // This example doens't use depth sorting, so it leaves instanceSortingPositions as null.
  242. drawCommands->instanceSortingPositions = null;
  243. drawCommands->instanceSortingPositionFloatCount = 0;
  244. // Configure the single draw command to draw kNumInstances instances
  245. // starting from offset 0 in the array, using the batch, material and mesh
  246. // IDs registered in the Start() method. It doesn't set any special flags.
  247. drawCommands->drawCommands[0].visibleOffset = 0;
  248. drawCommands->drawCommands[0].visibleCount = (uint)kNumInstances;
  249. drawCommands->drawCommands[0].batchID = m_BatchID;
  250. drawCommands->drawCommands[0].materialID = m_MaterialID;
  251. drawCommands->drawCommands[0].meshID = m_MeshID;
  252. drawCommands->drawCommands[0].submeshIndex = 0;
  253. drawCommands->drawCommands[0].splitVisibilityMask = 0xff;
  254. drawCommands->drawCommands[0].flags = 0;
  255. drawCommands->drawCommands[0].sortingPosition = 0;
  256. // Configure the single draw range to cover the single draw command which
  257. // is at offset 0.
  258. drawCommands->drawRanges[0].drawCommandsBegin = 0;
  259. drawCommands->drawRanges[0].drawCommandsCount = 1;
  260. // This example doesn't care about shadows or motion vectors, so it leaves everything
  261. // at the default zero values, except the renderingLayerMask which it sets to all ones
  262. // so Unity renders the instances regardless of mask settings.
  263. drawCommands->drawRanges[0].filterSettings = new BatchFilterSettings { renderingLayerMask = 0xffffffff, };
  264. // Finally, write the actual visible instance indices to the array. In a more complicated
  265. // implementation, this output would depend on what is visible, but this example
  266. // assumes that everything is visible.
  267. for (int i = 0; i < kNumInstances; ++i)
  268. drawCommands->visibleInstances[i] = i;
  269. // This simple example doesn't use jobs, so it returns an empty JobHandle.
  270. // Performance-sensitive applications are encouraged to use Burst jobs to implement
  271. // culling and draw command output. In this case, this function returns a
  272. // handle here that completes when the Burst jobs finish.
  273. return new JobHandle();
  274. }
  275. }
  276. [BurstCompile]
  277. partial struct RandomMoveJob : IJobParallelFor
  278. {
  279. [ReadOnly]
  280. public Unity.Mathematics.Random random;
  281. [ReadOnly]
  282. public float4 randomPostionRange;
  283. [ReadOnly]
  284. public float m_DeltaTime;
  285. public NativeArray<Matrix4x4> matrices;
  286. public NativeArray<float3> targetMovePoints;
  287. public NativeArray<PackedMatrix> obj2WorldArr;
  288. public NativeArray<PackedMatrix> world2ObjArr;
  289. [BurstCompile]
  290. public void Execute(int index)
  291. {
  292. float3 curPos = matrices[index].GetPosition();
  293. float3 dir = targetMovePoints[index] - curPos;
  294. if (Unity.Mathematics.math.lengthsq(dir) < 0.4f)
  295. {
  296. var newTargetPos = targetMovePoints[index];
  297. newTargetPos.x = random.NextFloat(randomPostionRange.x, randomPostionRange.y);
  298. newTargetPos.z = random.NextFloat(randomPostionRange.z, randomPostionRange.w);
  299. targetMovePoints[index] = newTargetPos;
  300. }
  301. dir = math.normalizesafe(targetMovePoints[index] - curPos, Vector3.forward);
  302. curPos += dir * m_DeltaTime;// math.lerp(curPos, targetMovePoints[index], m_DeltaTime);
  303. var mat = matrices[index];
  304. mat.SetTRS(curPos, Quaternion.LookRotation(dir), Vector3.one);
  305. matrices[index] = mat;
  306. var item = obj2WorldArr[index];
  307. item.SetData(mat);
  308. obj2WorldArr[index] = item;
  309. item = world2ObjArr[index];
  310. item.SetData(mat.inverse);
  311. world2ObjArr[index] = item;
  312. }
  313. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/86204
推荐阅读
相关标签
  

闽ICP备14008679号