Unity中的静态合批动态合批GPU Instance 以及SRP Batching
Posted 流浪打工人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity中的静态合批动态合批GPU Instance 以及SRP Batching相关的知识,希望对你有一定的参考价值。
文章目录
Unity中的静态合批、动态合批、GPU Instance 以及SRP Batching
四种合批简介
GPU instancing
GPU instancing: 对同一网格,同时渲染多个副本时使用,底层调用的是多实例渲染接口,例如OpenGL的glDrawArraysInstanced接口。GPU实例对于绘制场景中多次出现的几何图形(例如,树或灌木丛)非常有用。首先使用GPU Instance,需要材质着色器支持 GPU 实例化,接着就可以在 Project 窗口中选择材质,最后在 Inspector 中勾选 Enable Instancing 复选框。
特点:
- 使用 GPU 实例化可使用少量绘制调用一次绘制(或渲染)同一网格的多个副本。
- GPU 实例化在每次绘制调用时仅渲染相同的网格,但每个实例可以具有不同的参数(例如,颜色或比例)以增加变化并减少外观上的重复。
- GPU 实例化可以降低每个场景使用的绘制调用数量。可以显著提高项目的渲染性能。
使用GPU Instance的限制条件:
-
Unity 自动选取要实例化的网格渲染器组件和 Graphics.DrawMesh 调用。请注意,不支持 SkinnedMeshRenderer(骨骼蒙皮渲染)。
-
Unity 仅在单个 GPU实例化绘制调用中,批量处理那些共享相同网格和相同材质的游戏对象。使用少量网格和材质可以提高实例化效率。要创建变体,请修改着色器脚本为每个实例添加数据,下述shaderlab代码为官方示例代码。
-
还可以使用 Graphics.DrawMeshInstanced 和 Graphics.DrawMeshInstancedIndirect 调用来通过脚本执行 GPU 实例化。
使用实例化渲染实例代码
Shader "Custom/InstancedColorSurfaceShader"
Properties
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white"
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
SubShader
Tags "RenderType"="Opaque"
LOD 200
CGPROGRAM
// 基于物理的标准光照模型,并对所有光照类型启用阴影
#pragma surface surf Standard fullforwardshadows
// 使用 Shader Model 3.0 目标
#pragma target 3.0
sampler2D _MainTex;
struct Input
float2 uv_MainTex;
;
half _Glossiness;
half _Metallic;
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)
void surf (Input IN, inout SurfaceOutputStandard o)
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
ENDCG
FallBack "Diffuse"
然后可以在C#脚本中给对应的对象设置Color属性
MaterialPropertyBlock props = new MaterialPropertyBlock();
MeshRenderer renderer;
foreach (GameObject obj in objects)
float r = Random.Range(0.0f, 1.0f);
float g = Random.Range(0.0f, 1.0f);
float b = Random.Range(0.0f, 1.0f);
props.SetColor("_Color", new Color(r, g, b));
renderer = obj.GetComponent<MeshRenderer>();
renderer.SetPropertyBlock(props);
更多关于Unity GPU Instance的内容,请点击这里查看官方文档
static Batching
- static Batching:静态合批,一般合批的对象是场景中不能动的物体,静态物体,并且在inspector界面勾选上static选项;Unity预先组合静态GameObjects的网格,然后将组合的数据发送到GPU,但单独渲染组合中的每个网格,静态合批不会减小drawcall,只是减少了渲染状态的改变次数。Unity仍然可以单独筛选网格,但每次绘制调用都不会占用大量资源,因为数据的状态永远不会改变。在Unity开启Static Batching,依次点击 Edit > Project Settings > Playe->Other Settings->enable Static Batching。对于Unity使用静态合批的具体要求可以参照官方文档
- 支持静态合批的Unity渲染管线:默认渲染管线、URP(通用渲染管线)、HDRP、SRP。
- 在运行时,通过脚本使用静态合批:Unity提供了运行时,通过脚本使用静态合批口:StaticBatchingUtility.Combine,这是一个静态方法,通过这种方式调用静态合批,就不需要在编辑器的inspector界面勾选上Static选项。
- static Batching缺点与限制:使用静态合批会增加内存!,因为使用静态合批,需要在内存中存储合成的几何体数据;如果多个GameObject使用了相同的网格数据,Unity会为每一个GameObject创建一个网格副本,这就意味着相同的几个数据有可能会出现多次!所以如果内存过大,就尽量避免使用静态合批。对于每一次静态合批,顶点数的限制时64000,如果超过这个值,Unity就会再次创建另外一个批次(Batch)。
Dynamic batching
- Dynamic batching :动态合批,一般处理的是动态物体;在CPU上变换网格顶点,对共享相同配置的顶点进行分组,并在一次绘制调用中渲染它们。如果顶点存储相同数量和类型的属性,则它们共享相同的配置。例如,位置和法线。 在Unity开启动态合批,依次点击Edit > Project Settings > Player->Other Settings->enable Dynamic Batching。对于Unity使用动态合批的具体要求可以参照官方文档
- 特点:由于动态合批是通过将多个符合要求的GameObject的顶点一起转换到世界空间,提升性能;相当于是作用在CPU,所以只有在顶点的转换过程比一个绘制调用(Draw Call)过程消耗更低,这种策略才是一个合理的选择。
- 支持是动态合批的Unity渲染管线:默认渲染管线、URP、SRP。
- Dynamic batching的限制:动态合批对于顶点属性的数量有限制,不能超过900个顶点属性,例如,材质的shader使用了位置、法线、UV坐标,那么顶点数就是900 / 3 = 300,如果使用了vertex position, vertex normal, UV0, UV1, 和 vertex tangent,那么顶点数量的限制就是 900 / 5 = 180;
SRP Batcher
- **SRP Batcher:**如果项目使用脚本化渲染管道(SRP),使用SRP批处理,可以减少渲染状态的切换,这样会同样可以提升渲染新能,因为满足srp管线的材质,在显存中都有一个CBuffer,只要材质的参数没变,就不用每一帧都提交和设置材质的渲染状态 ,按照官网上上面的文档描述,使用SRP Batcher,其中一个要求是:材质可以不同,但是材质使用的Shader变体必须一致,这个和上面的三种合批方式很不一样。
在SRP Batcher中,GameObject的内置引擎属性(transform等)是有专门的提交路径(下图实线箭头所示),和材质的提交是分开的(下图虚线所示),这样做的好处是,我们每帧都更新一些必要的属性,例如位置,大小等信息,而材质就可以以增量改变的方式提交给GPU,而恰好材质的提交(渲染状态的改变)是非常印象效率的,在SRP Batcher管线中,每一种材质在GPU内存中都有一个CBuffer存放对应的参数,只要材质的参数没有发生变化,那么在每一帧中就不必从CPU提交材质到GPU。从而减少CPU的消耗,提升渲染效率,官网上的示意图如下:
图集的作用
在上述的合批中,多数都是要求使用相同的材质(meterail),而贴图也是属于材质的一种属性,如果两个材质仅仅贴图不一样,这也会导致材质不一样,就不印象合批,所以把多张贴图打包为图集,这样就可以是的材质引用同一份贴图,使得合批得以进行。
不同合批的优先级
- SRP Batcher and static batching
- GPU instancing
- Dynamic batching
SRP Batcher和static batching可以共存,如果一个GameObject使用了static batching,Unity就会禁用GPU instancing ,即使GameObject使用的是GPU instancing Shader; 如果一个GameObject使用了GPU instancing , Unity就会禁用Dynamic batching;
UGUI中的mask组件,会增加draw call分析:
Stencil 状态
Stencil 状态即模板测试,通过模板缓冲来实现特定的效果,在 Unity 中,Mask 组件就是通过该功能实现,一个 Mask 组件及其控制的渲染节点,需要至少三次 Draw call。第一次开启模板测试并调用一次 Draw call,刷新模板缓冲。第二次绘制对需要通过模板测试的区域进行设置。第三次再进行实际的子节点内容绘制,绘制结束再关闭模板测试。因此使用 Mask 组件就无法与其他相邻节点进行批次处理,但是 Mask 组件内部的连续节点在满足合并规则的情况下还是会进行合批。
Stencil 使用的最佳实践
如果界面内使用大量 Mask 组件会带来 Draw call 的剧增,因此应该尽量减少 Mask 组件的使用。如有使用 Mask 组件的节点,应该尽量不要穿插在连续并且可以进行批次合并的节点层级内,这样也可以尽量规避 Mask 打断本可以合并批次的一系列连续节点。
游戏开发进阶Unity网格探险之旅(Mesh | 动态合批 | 骨骼动画 | 蒙皮 )
一、前言
嗨,大家好,我是新发。
有同学私信我让我写一篇Unity
网格相关的教程,
那我就带大家来一次Unity
的网格探险之旅吧~
二、Hello Mesh
我背着旅行背包走在Unity
的场景中,突然眼前出现了一棵树,
我走近一看,这棵树身上挂着MeshFilter
和MeshRenderer
组件,根据Unity
探险手册记载,这个MeshFilter
是网格过滤器,它会引用一个网格资源,我顺腾摸瓜,找到了对应的网格,
实在太美了,我久久伫立,这就是网格啊!
正当我欣赏着网格三角形时,突然世界暗了下来,眼前出现了一团火,
我又拿出了Unity
探险手册,啊,这一定就是粒子系统了!它可以动态生成网格。
天外传来一阵打字声,场景中出现了一行看起来像文字的网格,作为一个具有多年Hello World
经验的程序员,我看出了第一个单词应该是Hello
,第二个单词…我知道了,
是Hello Mesh
!
(此处为震撼人心的入场音乐)
三、萌新初识Mesh
1、引擎内置的Mesh
网格的英文名是Mesh
,Unity
萌新最先接触的网格应该就是引擎内置的Cube
(正方体)、Capsule
(胶囊体)、Cylinder
(圆柱体)、Plane
(平面)、Sphere
(球体)、Quad
(四边形),如下
事实上,我们在Unity
场景中,所有能被渲染出来的物体都会带有网格,比如3D
模型、粒子特效、UI
、文字等等。
2、Mesh是什么
从概念上讲,网格是图形硬件用来绘制复杂内容的构造。它至少包含一组定义3D
空间中点的顶点,以及一组连接这些点的三角形,实际上还包含法线、顶点颜色纹理坐标等信息,这些三角形构成了网格所代表的任何表面。
我们可以看下Unity
的Mesh
类,Mesh
的属性和方法很多,我这里列举几个比较常用的,如下
// 顶点坐标数组
public Vector3[] vertices { get; set; }
// 法线向量数组
public Vector3[] normals { get; set; }
// 顶点颜色数组
public Color[] colors { get; set; }
// 三角形序列数组,每三个数字为一组
public int[] triangles { get; set; }
// uv坐标数组
public Vector2[] uv { get; set; }
// 重新计算法线,在修改完顶点后,通常会更新法线来反映新的变化,注意,法线是根据共享的顶点计算出来的。
public void RecalculateNormals();
// 从法线和纹理坐标重新计算网格的切线。修改网格的顶点和法线之后,如果网格使用引用法线贴图的着色器进行渲染,则切线需要更新。
public void RecalculateTangents();
// 重新计算从网格包围体的顶点, 在修改顶点后需要这个函数以确保包围体是正确的,赋值三角形将自动重新计算这个包围体。
public void RecalculateBounds();
画个图,方便大家有个直观印象,
三、Mesh的创建方式
1、第三方建模软件
建模本质上就是建网格,我们可以事先通过第三方建模软件来创建模型网格,
常见的建模软件比如
3DS MAX
官网:https://www.autodesk.com/products/3ds-max/overview
MAYA
官网:https://www.autodesk.com/products/maya/overview
blender
官网:https://www.blender.org/
2、Unity建模插件:ProBuilder
Unity
官方提供了一个可以用来创建和自定义几何体的工具ProBuilder
,我们可以在Unity
的Package Manager
中下载到这个插件,
使用ProBuilder
我们可以直接在Unity
中创建或编辑简单的几何体,不用通过第三方建模软件,提升了效率,方便快速搭建场景原型,
3、程序动态生成网格
网格也可以是程序动态生成的,比如粒子系统的网格就是动态生成的,
又比如文字,也是程序动态生成网格,
文章后面我还会手把手教你如何使用纯代码来构建网格,这里先不急着写代码,我们继续探寻网格的秘密先~
四、Unity中如何显示网格
在Unity
中,我们要显示一个网格,需要用到两个组件:MeshFilter
和MeshRenderer
。
注:你也可以直接使用
SkinnedMeshRenderer
组件,与MeshFilter
和MeshRenderer
的区别我下文会讲。
1、MeshFilter:网格过滤器
MeshFilter
是网格过滤器,我们需要通过它设置引用的网格资源,比如这里引用的是一个Cube
(正方体)网格。
我们可以看下MeshFilter.cs
的源码,
[RequireComponent(typeof(Transform))]
[NativeHeader("Runtime/Graphics/Mesh/MeshFilter.h")]
public sealed partial class MeshFilter : Component
{
[RequiredByNativeCode] // MeshFilter is used in the VR Splash screen.
private void DontStripMeshFilter() {}
extern public Mesh sharedMesh { get; set; }
extern public Mesh mesh {[NativeName("GetInstantiatedMeshFromScript")] get; [NativeName("SetInstantiatedMesh")] set; }
}
MeshFilter
只有两个属性:mesh
和sharedMesh
,
我们查看Unity
的官方手册,看看mesh
与sharedMesh
的区别:https://docs.unity3d.com/ScriptReference/MeshFilter.html
我来解读一下,mesh
访问的是一个Mesh
资源的实例(副本),这意味着我们修改这个mesh
并不会修改到原始资源本身,改的只是Mesh
的实例(副本)。
而sharedMesh
是原始资源的引用,如果修改了sharedMesh
,比如修改顶点坐标,那么原始资源也会被修改。
画成图大概是这样子:
这里我顺手写个随机修改Mesh
顶点坐标的脚本,如下,将下面这个RandoMeshmVertices
脚本挂到MeshFilter
组件所在的物体上即可,
// RandoMeshmVertices.cs
// 随机修改Mesh顶点坐标
using UnityEngine;
public class RandoMeshmVertices: MonoBehaviour
{
// Mesh的实例
MeshFilter meshFilter;
// 顶点的原始坐标
Vector3[] originalVertices;
void Start()
{
meshFilter = GetComponent<MeshFilter>();
originalVertices = meshFilter.mesh.vertices;
}
void Update()
{
// 随机修改顶点坐标
Vector3[] vertices = meshFilter.mesh.vertices;
for (int i = 0, len = originalVertices.Length; i < len; ++i)
{
var v = originalVertices[i];
vertices[i] = v + Random.Range(-0.1F, 0.1F) * Vector3.one;
}
meshFilter.mesh.vertices = vertices;
meshFilter.mesh.RecalculateNormals();
}
}
运行效果如下,网格顶点坐标发生了随机偏移,
关于mesh
属性的访问需要特别注意一下,我们先看看Unity
官方手册的说明,https://docs.unity3d.com/ScriptReference/MeshFilter-mesh.html
翻译一下就是,如果一个Mesh
资源已经被分配给MeshFilter
的mesh
属性,那么当我们在代码中第一次访问mesh
属性时才正真创建了Mesh
的实例;再次访问mesh
属性时则直接返回这个实例,并且一旦mesh
属性被访问,则与原始共享网格的链接会丢失,此时sharedMesh
变成mesh
的别名,如果我们想避免这种自动生成Mesh
实例,可以使用sharedMesh
代替。
写成伪代码的话大致是这样子:
public class MeshFilter ...
{
...
private Mesh _mesh;
public Mesh mesh
{
get
{
if (_mesh == null)
{
_mesh = new Mesh();
Copy(sharedMeh, _mesh);
}
return _mesh;
}
}
...
}
还有,如果我们访问了mesh
属性而导致自动创建了Mesh
实例,则需要在代码中主动调用Resources.UnloadUnusedAssets
来销毁没有引用的Mesh
实例,建议是在场景切换时调用Resources.UnloadUnusedAssets
。
2、MeshRenderer:网格渲染器
MeshRenderer
,顾名思义,网格渲染器。我们依旧先来看看官方手册的介绍:https://docs.unity3d.com/Manual/class-MeshRenderer.html
翻译过来就是MeshRenderer
会从MeshFilter
那里拿到网格数据并在所在物体的位置处将其渲染出来。
如果没有MeshRenderer
,我们就看不见网格了,如下
另外,我们还需要在MeshRenderer
的Materials
中指定一个材质球,这样才能正常显示,否则模型表面就是紫色的。
3、SkinnedMeshRenderer:蒙皮网格渲染器
SkinnedMeshRenderer
是蒙皮网格渲染器,可能有小伙伴就会问了,上面使用MeshFilter
和MeshRenderer
已经可以显示模型网格了,为什么又弄了一个SkinnedMeshRenderer
呢?
看下Unity
官方手册的介绍:https://docs.unity3d.com/Manual/class-SkinnedMeshRenderer.html
可以看到SkinnedMeshRenderer
其实是针对带 骨骼动画 的模型的渲染的。
3.1 骨骼动画
为什么需要做骨骼动画呢? 就好比我们人一样,我们的骨骼会随着我们肌肉的伸缩而动,骨骼又可以带动它管辖的身体部位发生形变和移动,骨骼还会影响它所连接的其他骨骼一起发生联动。对应到模型动作上,想想一个简单的举手动作要牵涉到多少网格顶点的移动,如果没有骨骼,那动画师要每帧挨个网格顶点进行调整,即使动画做出来了,这个动画也不能复用到其他模型上,因为不同模型的顶点信息都不一样,这么低效的动画制作肯定是不行的,于是,就有了骨骼动画。
骨骼动画的原理 就是将模型分为骨骼(Bone
)和蒙皮(Mesh
)两个部分,骨骼可分为多层父子骨骼,每个骨骼都附加到周围网格的一些顶点上,在动画关键帧数据的驱动下,计算出各个父子骨骼的位置,基于骨骼的控制通过顶点混合动态计算出蒙皮网格的顶点。
动画师可以在MAYA
软件上给模型绑定骨骼,绑定骨骼不是本文的重点,这里就不展讲开具体操作了,感兴趣的同学可以自行百科学习,我这里附一个传送门:《maya骨骼绑定入门教程》
制作好导出为fbx
格式,
将fbx
文件导入到Unity
中,选中它,
在Inspector
视图中点击Rig
按钮,
我们可以看到动画类型Animation Type
有None
、Legacy
、Generic
和Humanoid
四个,
具体选项可以参见Unity
官方手册:https://docs.unity3d.com/Manual/FBXImporter-Rig.html
我这里演示一下人形骨骼动画,选择Humanoid
类型,Avatar Definition
选择Create From This Model
,然后点击Configure
,
在Inspector
视图中我们就可以看到对应的骨骼绑定信息了,
如下,绿色的线段就是一根根骨骼,
我们调整一根骨骼,对应的网格也会跟着一起动,如下
这样做出来的人形动画是可以进行复用了,有请妹子上场,
骨骼动画资源的话,我在之前的文章中也介绍过一个宝藏网站Mixamo
:https://www.mixamo.com/,上面有很多做好的人形骨骼动画,
看,是不是挺好玩的,
我们可以把它的动作直接复用到我们自己的人形模型上,效果如下:
具体是怎么绑定动画的,我在之前这篇文章中有讲,感兴趣可以看看:https://linxinfa.blog.csdn.net/article/details/118054075
3.2 SkinnedMeshRenderer组件
骨骼动画可以正常播放,要归功于SkinnedMeshRenderer
组件,制作好骨骼动画的fbx
文件导入Unity
中,Unity
会自动帮我们挂上SkinnedMeshRenderer
组件,
其中几个重要的属性我讲一下,
Bounds
:骨骼数据;
Mesh
:要渲染的网格;
Root Bone
:根骨骼,其他骨骼都是相对根骨骼移动的;
BlendShapes
:一般用于制作表情融合,我之前写过一篇文章讲过BlendShapes
:https://linxinfa.blog.csdn.net/article/details/116666936
我们再来看看SkinnedMeshRenderer
脚本的属性和方法:
需要讲的应该就是这个BakeMesh
方法了,下面我就单独拎出来讲下BakeMesh
。
3.2 使用BakeMesh进行优化
假设现在场景中有100
只皮卡丘,每只皮卡丘的网格、贴图、动作相同,
如果每只皮卡丘身上都挂SkinnedMeshRenderer
,那就是100
个SkinnedMeshRenderer
在计算蒙皮,
由于SkinnedMeshRenderer
是根据骨骼动画动态计算网格顶点坐标,这个运算开销还是不小的,有没有办法优化呢?
SkinnedMeshRenderer
提供了一个BakeMesh
方法,可以将一个蒙皮动画的某个时间点上的动作,Bake
成一个不带蒙皮的Mesh
,我们统一使用这个Mesh
来显示其余的皮卡丘,这样就可以大大减少了SkinnedMeshRenderer
的计算了,
画成图大概是这样子:
不过,上面这种方案的局限性是每只皮卡丘的动画是相同的,如果突然某一只皮卡丘要播放与其他皮卡丘不同的动画,那就不行了。
另一种Bake
方案可以是这样:
对皮卡丘的每个动画进行遍历采样,把采样到的Mesh
存到数组中,因为这里要Bake
很多网格,比较耗时,建议在加载场景时时就完成采样过程;后面要播放某个动画时直接从这个Mesh
数组中获取Mesh
来显示,此时直接使用MeshFilter
加MeshRenderer
的方式来显示网格就好了。
贴个BakeMesh
的示例脚本:
using UnityEngine;
using System.Collections.Generic;
/// <summary>
/// Bake Mesh 示例
/// </summary>
public class BakeMeshTest : MonoBehaviour
{
[SerializeField]
Animation m_animation;
[SerializeField]
SkinnedMeshRenderer m_skinnedMeshRenderer;
[SerializeField]
string m_clipToBake = "Idle";
List<Mesh> m_bakedMeshList = new List<Mesh>();
/// <summary>
/// 采样帧数
/// </summary>
[SerializeField]
int m_numFramesToBake = 30;
void Start()
{
// 获取要Bake的动画片段
AnimationState clipState = m_animation[m_clipToBake];
if (clipState == null)
{
Debug.LogError(string.Format("Unable to get clip '{0}'", m_clipToBake), this);
return;
}
// 开始播放动画
m_animation.Play(m_clipToBake, PlayMode.StopAll);
// 设置动画初始时间戳
clipState.time = 0.0f;
// 采样帧间隔
float deltaTime = clipState.length / (float)(m_numFramesToBake - 1);
for (int frameIndex = 0; frameIndex < m_numFramesToBake; ++frameIndex)
{
string frameName = string.Format("BakedFrame{0}", frameIndex);
// 创建Mesh
Mesh frameMesh = new Mesh();
frameMesh.name = frameName;
// 动画采样
m_animation.Sample();
// 执行BakeMesh
m_skinnedMeshRenderer.BakeMesh(frameMesh);
m_bakedMeshList.Add(frameMesh);
// 设置动画时间戳
clipState.time += deltaTime;
}
// 停止播放动画
m_animation.Stop();
}
}
需要提醒的是,这个方案是利用空间换时间,如果模型顶点数据特别多或动画时长特别长的时候,这时就会遇到内存瓶颈。
五、纯代码动态创建网格
一般情况下,网格是事先制作好的资源,但也有一些特殊的需求需要在代码中动态创建网格。
比如我之前写的一篇牙齿碎了的文章:《Unity 2D图片任意形状破碎碎裂效果,以此纪念我的牙光荣牺牲》
现在我来教大家如何使用代码从零创建网格并将网格渲染出来,下文我以创建一个正方形网格为例进行讲解。
1、创建Mesh对象
第一步最简单,就是直接new
一个Mesh
,
var mesh = new Mesh();
2、顶点坐标
首先分析一下,一个四边形有四个顶点,假设正方形边长为1
,四个点的坐标如下,
写成代码就是这样:
// 构建顶点坐标
var vertices = new List<Vector3>();
vertices.Add(new Vector3(-0.5f, -0.5f, 0));
vertices.Add(new Vector3(-0.5f, 0.5f, 0));
vertices.Add(new Vector3(0.5f, 0.5f, 0));
vertices.Add(new Vector3(0.5f, -0.5f, 0));
// 将顶点坐标设置给Mesh
mesh.SetVertices(vertices);
3、UV坐标
UV
坐标就是纹理贴图坐标,它将纹理上每一个点精确对应到模型物体的表面上,注意U
和V
的取值范围是0~1
。
UV
坐标系原点在左下角,U
轴是水平轴,V
轴是竖直轴,如下:
对应到我们的上面那个正方向网格的话,四个点的UV
坐标如下:
写成代码就是这样:
// 构建UV坐标
var uvs = new List<Vector2>();
uvs.Add(new Vector2(0, 0));
uvs.Add(new Vector2(0, 1));
uvs.Add(new Vector2(1, 1));
uvs.Add(new Vector2(1, 0));
// 将UV坐标设置给Mesh
mesh.SetUVs(0, uvs);
4、三角形序列
网格需要切分成三角形,我们可以这样切分,
当然也可以这样切分,
两种切分方法对应不同的三角形序列,假设 法线方向 是垂直于屏幕从内指向屏幕外的话,第一种切分方式的三角形序列如下:
注:法线的方向就决定了表面正面,如果你的材质是单面渲染的话,那么只有从正面看才能看到网格被渲染。
即三角形序列为:{ 0, 1, 2, 0, 2, 3 }
,注意序号是从0
开始的。
为什么是这样的顺序呢?我教大家一个技巧,伸出你的左手,竖起大拇指,像这样子,
大拇指指向法线的方向,那么此时你的其余四根手指头环绕的方向就是三角形的序号的顺序,三个序号为一组按顺序塞入数组中即可,即得到的数组就是:{ 0, 1, 2, 0, 2, 3 }
,当然,以下数组最终的效果都是等价的,只要顺序一致即可:
{ 0, 1, 2, 0, 2, 3 }
,
{ 1, 2, 0, 0, 2, 3 }
,
{ 0, 2, 3, 1, 2, 0 }
,
…
我们现在写成代码,
// 设置三角形序列
var triangles = new int[] { 0, 1, 2, 0, 2, 3 };
mesh.SetTriangles(triangles, 0);
5、重新计算法线和包围体
当我们设置或修改了顶点数据后,需要调用Mesh
的Recalculate
方法来重新计算一些必要的信息,比如重新计算法线、包围体,代码如下
// 重新计算法线,注意,法线是根据共享的顶点计算出来的。
mesh.RecalculateNormals();
// 重新计算包围体,在修改顶点后需要这个函数以确保包围体是正确的
mesh.RecalculateBounds();
// 从法线和纹理坐标重新计算网格的切线(如果网格使用引用法线贴图的着色器进行渲染,则切线需要更新)
// 因为我们这里不使用法线贴图,所以就不调用它了
// mesh.RecalculateTangents();
6、完整版代码
以上代码封装成GenQuadMesh.cs
脚本,完整代码如下:
// 使用代码生成四边形网格
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class GenQuadMesh : MonoBehaviour
{
public M以上是关于Unity中的静态合批动态合批GPU Instance 以及SRP Batching的主要内容,如果未能解决你的问题,请参考以下文章
游戏开发进阶Unity网格探险之旅(Mesh | 动态合批 | 骨骼动画 | 蒙皮 )