Unity学习之网格Mesh、过滤器MeshFilter、渲染器MeshRenderer_unity mesh
在 Unity 中,Mesh
、MeshFilter
和MeshRenderer
是构建 3D 模型渲染的核心组件,今天详细介绍它们之间的关系和工作原理。
一、基本概念介绍
1. 网格Mesh
Mesh在Unity中是一个核心的组件,被称为网格组件,它主要用于表示3D几何体的数据结构。Mesh由顶点、三角形面以及可选的材质等组成,这些元素共同构建了3D模型的基础。
Mesh是几何数据的源头,可通过代码动态生成或导入 3D 建模软件(如 Blender、Maya)创建的模型。
从构成上来看,Mesh主要包括但不限于下列内容:
顶点坐标数组;
顶点在UV坐标系中的位置信息数组;
三角形顶点索引数组
核心属性:
vertices
:顶点坐标数组(Vector3 [])。
triangles
:三角形索引数组(int []),定义顶点如何连接成三角形。
normals
:法线向量数组,用于计算光照。
uv
:UV 坐标数组,用于纹理映射。
colors
:顶点颜色数组(Color [])。
mesh.SetVertices(vertices); // 设置顶点位置mesh.SetTriangles(triangles, 0); // 设置三角形索引mesh.RecalculateNormals(); // 重新计算法线mesh.RecalculateBounds(); // 重新计算边界框
2. 网格过滤器MeshFilter
Unity中的 MeshFilter 是一个组件,它用于从资源中获取网格(Mesh)并将其传递给网格渲染器( MeshRenderer ),以便在屏幕上渲染该网格。MeshFilter 本身并不直接参与渲染过程,而是作为一个中介,将Mesh数据传递给 MeshRenderer 。也就是说,GameObject 并不是直接引用了Mesh,因为Mesh并不是组件。一个游戏对象只能通过 MeshFilter 这类的组件来使用 Mesh 。
核心属性:
mesh
:指向当前使用的Mesh
对象。
sharedMesh
:多个对象共享同一个Mesh
实例(修改会影响所有对象)。
3. 网格渲染器MeshRenderer
MeshRenderer是Unity中的一个组件,属于UnityEngine命名空间下的一个类,它继承自Renderer类。MeshRenderer的主要作用是渲染由MeshFilter或TextMesh插入的网格。它根据物体的Transform组件的定义位置,从网格过滤器(Mesh Filter)获取几何形状,并在该位置进行渲染。
核心属性:
material
:当前使用的材质(运行时修改会创建副本)。
sharedMaterial
:共享材质(修改会影响所有使用该材质的对象)。
shadowCastingMode
:阴影投射模式(如 Cast Shadows、Receive Shadows)。lightProbeUsage
:光照探针使用方式。
二、三者之间关系
1. 三者的核心作用
2. 工作原理与关系
Mesh:是几何数据的源头,可通过代码动态生成或导入 3D 建模软件(如 Blender、Maya)创建的模型。
MeshFilter:持有对Mesh
的引用,将网格数据传递给MeshRenderer
。
MeshRenderer:使用MeshFilter
提供的数据,结合材质(Material)和着色器(Shader),将模型渲染到屏
3. 核心交互流程
创建 / 导入 Mesh:
通过代码动态生成(如程序化地形、动态网格变形)。
从外部 3D 软件导入(FBX、OBJ 等格式),Unity 会自动将其转换为Mesh
。
关联 MeshFilter:
在 GameObject 上添加MeshFilter
组件。
通过MeshFilter.mesh
属性指定使用的Mesh
对象。
配置 MeshRenderer:
在同一 GameObject 上添加MeshRenderer
组件。
设置MeshRenderer.material
属性,指定渲染时使用的材质(包含纹理、着色器等信息)。
渲染到屏幕:
Unity 的渲染管线读取MeshRenderer
的配置和MeshFilter
的网格数据,最终将模型显示在屏幕上。
三、程序动态生成网格
要在代码中创建一个网格,我们首先需要创建一个Mesh对象,并定义了它的顶点坐标 vertices ,三角形面索引 triangles ,UV坐标 uvs (UV坐标通常用于纹理映射,可选),以及法线信息 normals(可选)。代码示例如下:
public class TestCreateMeshBehaviour : MonoBehaviour{ private Mesh mesh; private MeshFilter meshFilter; public Material mat; void Start() { // 创建一个新的网格对象 mesh = new Mesh(); // 设置网格的顶点 Vector3[] vertices = new Vector3[] { new Vector3(0, 0, 0), new Vector3(0, 0, 10), new Vector3(10, 0, 10), new Vector3(10, 0, 0) }; mesh.vertices = vertices; // 设置网格的三角形(每个三角形由三个顶点的索引构成) int[] triangles = new int[] { 0, 1, 2, 2, 3, 0 }; mesh.triangles = triangles; // 设置网格的法线(可选,但通常用于光照计算) Vector3[] normals = new Vector3[] { Vector3.up, Vector3.up, Vector3.up, Vector3.up }; mesh.normals = normals; // 设置网格的UV坐标(可选,但通常用于纹理映射) Vector2[] uvs = new Vector2[] { new Vector2(0, 0), new Vector2(1, 0), new Vector2(1, 1), new Vector2(0, 1) }; mesh.uv = uvs; // 创建一个网格过滤器组件,并将网格分配给它 meshFilter = GetComponent(); if (!meshFilter) { meshFilter = gameObject.AddComponent(); } meshFilter.mesh = mesh; // 创建一个网格渲染器组件,以便在场景中显示网格 MeshRenderer meshRenderer = GetComponent(); if (!meshRenderer) { meshRenderer = gameObject.AddComponent(); } meshRenderer.material = mat; }}
1. 顶点信息
Mesh的组成离不开点和面,我们想要创建一个网格首先要提供的就是顶点信息。下面代码中提供了四个顶点,形成了一个边长为10的正方形。
// 设置网格的顶点Vector3[] vertices = new Vector3[]{ new Vector3(0, 0, 0), new Vector3(0, 0, 10), new Vector3(10, 0, 10), new Vector3(10, 0, 0)};mesh.vertices = vertices;
2. 三角形信息
想要形成正常的网格,我们还需要将刚才提供的点连起来,形成一个个面。这时候就需要提供第二类信息,三角形信息。代码如下:
// 设置网格的三角形(每个三角形由三个顶点的索引构成)int[] triangles = new int[]{ 0, 1, 2,// 第一个面 2, 3, 0// 第二个面};mesh.triangles = triangles;
3. 顶点顺序与三角形朝向
顺时针与逆时针:
Unity使用逆时针(counter-clockwise, CCW)的顶点顺序来表示三角形的正面。这意味着,如果你从三角形的外部观察(外部指的是三角形面向观察者的方向,也可以理解为从这个三角形的背面看三角形),此时顶点的顺序应该是逆时针的。如果顶点顺序是顺时针的,那么三角形可能会被视为背面,并可能被背面剔除(如果启用了该功能)。
当我们谈论三角形的顶点顺序和朝向时,我们关心的是三角形面相对于观察者的方向。具体来说,如果我们按照逆时针顺序(从三角形的一个顶点开始,沿着边缘到下一个顶点,再到下一个,最后回到起始顶点)来定义三角形的顶点,那么当观察者站在三角形的“外部”时,这个面会朝向观察者,我们称之为“正面”或“前向面”。相反,如果顶点顺序是顺时针的,那么当观察者站在三角形的“外部”时,这个面会背向观察者,我们称之为“背面”或“后向面”。
法线方向:
三角形的法线方向是由顶点的顺序决定的。逆时针顺序会产生指向观察者的法线,而顺时针顺序则会产生背向观察者的法线。确保你的法线方向与你想要的光照和阴影效果一致。
四、网格应用案例
1. 网格合并优化(减少 Draw Call)
// 将多个小网格合并为一个大网格MeshFilter[] meshFilters = GetComponentsInChildren();CombineInstance[] combine = new CombineInstance[meshFilters.Length];for (int i = 0; i < meshFilters.Length; i++) { combine[i].mesh = meshFilters[i].sharedMesh; combine[i].transform = meshFilters[i].transform.localToWorldMatrix;}Mesh finalMesh = new Mesh();finalMesh.CombineMeshes(combine);GetComponent().mesh = finalMesh;
2. 特殊效果实现(如流体、粒子系统)
通过动态修改Mesh
的顶点位置和 UV 坐标,实现水流、布料飘动等效果。
以下是一个通过动态修改 Mesh 顶点位置和 UV 坐标实现简单水流效果的 Unity 代码案例:
using UnityEngine;public class WaterFlowEffect : MonoBehaviour{ // 水流效果参数 [Header(\"波浪参数\")] public float waveHeight = 0.5f; // 波浪高度 public float waveFrequency = 1.0f; // 波浪频率 public float waveSpeed = 1.0f; // 波浪移动速度 [Header(\"UV滚动参数\")] public float uvScrollX = 0.1f; // UV水平滚动速度 public float uvScrollY = 0.0f; // UV垂直滚动速度 private Mesh mesh; private Vector3[] originalVertices; // 原始顶点位置 private Vector2[] originalUVs; // 原始UV坐标 private Vector3[] currentVertices; // 当前顶点位置 private Vector2[] currentUVs; // 当前UV坐标 private void Start() { // 获取MeshFilter组件并引用其Mesh MeshFilter meshFilter = GetComponent(); if (meshFilter == null) { Debug.LogError(\"对象上缺少MeshFilter组件\"); return; } mesh = meshFilter.mesh; // 保存原始顶点和UV数据 originalVertices = mesh.vertices; originalUVs = mesh.uv; // 创建当前数据副本用于修改 currentVertices = new Vector3[originalVertices.Length]; currentUVs = new Vector2[originalUVs.Length]; // 复制原始数据到当前数据 System.Array.Copy(originalVertices, currentVertices, originalVertices.Length); System.Array.Copy(originalUVs, currentUVs, originalUVs.Length); } private void Update() { // 更新波浪动画 UpdateWaveAnimation(); // 更新UV滚动 UpdateUVScroll(); // 应用修改到Mesh ApplyMeshChanges(); } private void UpdateWaveAnimation() { float time = Time.time * waveSpeed; for (int i = 0; i < currentVertices.Length; i++) { Vector3 vertex = originalVertices[i]; // 使用正弦函数计算波浪高度 float xCoord = vertex.x * waveFrequency + time; float zCoord = vertex.z * waveFrequency + time; // 波浪公式:y = A * sin(kx + ωt) + B * sin(ky + ωt) float waveY = waveHeight * Mathf.Sin(xCoord) + waveHeight * 0.5f * Mathf.Sin(zCoord + Mathf.PI/2); // 应用波浪效果到顶点Y坐标 currentVertices[i] = new Vector3(vertex.x, vertex.y + waveY, vertex.z); } } private void UpdateUVScroll() { float scrollX = Time.time * uvScrollX; float scrollY = Time.time * uvScrollY; for (int i = 0; i < currentUVs.Length; i++) { // 滚动UV坐标 currentUVs[i] = new Vector2( originalUVs[i].x + scrollX, originalUVs[i].y + scrollY ); } } private void ApplyMeshChanges() { // 应用修改后的顶点和UV到Mesh mesh.vertices = currentVertices; mesh.uv = currentUVs; // 重新计算法线以确保光照正确 mesh.RecalculateNormals(); // 重新计算边界以确保碰撞正确 mesh.RecalculateBounds(); }}
五、性能优化建议
1. 减少网格复杂度
避免不必要的高精度模型,使用 LOD(Level of Detail)技术根据距离降低网格复杂度。
LOD(Level of Detail)技术是优化网格复杂度、提升性能的核心手段之一。通过根据相机与物体的距离动态切换不同精度的模型,可以在保证视觉效果的同时显著降低 GPU 负载。
核心概念:
多级模型:为同一物体创建多个不同精度的模型(LOD0-LOD4),精度逐级降低。
距离触发:根据相机与物体的距离自动切换模型。
过渡效果:切换时可应用淡入淡出或交叉融合,避免视觉跳变。
具体使用:
step1. 创建多级模型:
LOD0:原始高精度模型(如 10k 面)。
LOD1:中度简化(如 5k 面)。
LOD2:高度简化(如 1k 面)。
LOD3:极低精度(如 200 面)或广告牌(Billboard)。
step2. 添加 LOD Group 组件:
GameObject obj = GameObject.Find(\"TargetObject\");LODGroup lodGroup = obj.AddComponent();
step3. 配置 LOD 级别:
// 获取或创建各级LOD模型Renderer[] lodRenderers = new Renderer[4];lodRenderers[0] = GetComponent(); // LOD0lodRenderers[1] = CreateLOD1Renderer(); // 自定义方法创建LOD1// ...同理创建LOD2和LOD3// 设置LOD转换阈值(0-1范围,表示屏幕空间占比)LOD[] lods = new LOD[4];lods[0] = new LOD(0.5f, new Renderer[] { lodRenderers[0] });lods[1] = new LOD(0.2f, new Renderer[] { lodRenderers[1] });lods[2] = new LOD(0.05f, new Renderer[] { lodRenderers[2] });lods[3] = new LOD(0.01f, new Renderer[] { lodRenderers[3] });lodGroup.SetLODs(lods);lodGroup.RecalculateBounds();
最后. 调整过渡效果:
// 启用淡入淡出过渡lodGroup.fadeMode = LODFadeMode.CrossFade;lodGroup.animateCrossFade = true;
2. 合并静态网格
使用 Unity 的 Static Batching 或代码手动合并静态物体的网格,减少 Draw Call。
在 Unity 中,** 静态网格合并(Static Batching)** 是降低 Draw Call、提升渲染性能的重要技术。通过将多个静态物体的网格合并为一个,可显著减少 GPU 指令数,尤其适用于场景中大量重复的静态元素(如建筑、地形、植被等)。
核心机制
预处理合并:Unity 在运行前将标记为Static
且满足条件的物体合并为一个或多个大网格。
共享材质:合并的物体必须使用相同材质(或材质实例)。
内存换性能:合并后占用更多内存,但减少了 Draw Call。
下面介绍如何手动网格合并(代码实现):
适用场景
动态生成的物体(如程序化地形)。
不同材质的物体需要合并(需分割为子网格)。
精细控制合并过程。
代码示例
using UnityEngine;public class MeshCombiner : MonoBehaviour{ [ContextMenu(\"合并选中物体\")] public void CombineSelectedMeshes() { // 获取选中的所有物体 GameObject[] selectedObjects = Selection.gameObjects; if (selectedObjects.Length == 0) return; // 创建合并后的父物体 GameObject combinedObject = new GameObject(\"CombinedMesh\"); combinedObject.transform.position = Vector3.zero; // 创建合并后的MeshFilter和MeshRenderer MeshFilter meshFilter = combinedObject.AddComponent(); MeshRenderer meshRenderer = combinedObject.AddComponent(); // 材质字典(材质 → 子网格索引) Dictionary materialToSubmesh = new Dictionary(); List materials = new List(); List combineInstances = new List(); // 遍历所有选中物体 foreach (GameObject obj in selectedObjects) { MeshFilter filter = obj.GetComponent(); MeshRenderer renderer = obj.GetComponent(); if (filter == null || renderer == null) continue; // 处理每个子网格 for (int i = 0; i < filter.sharedMesh.subMeshCount; i++) { Material material = renderer.sharedMaterials[i]; int submeshIndex; // 如果材质已存在,使用现有子网格索引 if (materialToSubmesh.TryGetValue(material, out submeshIndex)) { // ... } else { // 新增材质和子网格 submeshIndex = materials.Count; materialToSubmesh[material] = submeshIndex; materials.Add(material); } // 添加到合并列表 CombineInstance combineInstance = new CombineInstance(); combineInstance.mesh = filter.sharedMesh; combineInstance.subMeshIndex = i; combineInstance.transform = obj.transform.localToWorldMatrix; combineInstances.Add(combineInstance); } // 禁用原物体 obj.SetActive(false); } // 创建合并后的网格 Mesh combinedMesh = new Mesh(); combinedMesh.CombineMeshes(combineInstances.ToArray(), false, false); // 应用合并后的网格和材质 meshFilter.mesh = combinedMesh; meshRenderer.materials = materials.ToArray(); // 设置为静态以启用进一步批处理 combinedObject.isStatic = true; }}
3. 共享材质与网格
尽量使用sharedMaterial
和sharedMesh
,避免创建重复资源。
在 Unity 中,** 共享材质(sharedMaterial
)和共享网格(sharedMesh
)** 是优化性能、减少内存占用的重要手段,尤其适用于大量重复物体的场景(如植被、建筑部件、粒子效果等)。