获取场景 Mesh 数据开发指南
LCC Unity SDK 在重建 LCC 数据时,会同时生成用于碰撞的 Mesh 数据。除了用于碰撞检测,SDK 还提供了将这份 Mesh 数据提取出来的能力,可将其转换为 Unity 的 Mesh 对象并可视化,便于导出、烘焙 NavMesh 等场景。
下面通过一个完整的示例,介绍同步获取全量 Mesh 数据、以及按需获取指定范围 Mesh 数据的步骤。
环境设置
在 Unity 中创建一个全新的场景,Unity 默认会创建 Main Camera 和 Directional Light。
设置 Main Camera
在 Hierarchy 窗口中选择 Main Camera,在 Inspector 窗口中按下图设置其属性。

设置 Main Camera 的属性
创建 LCCManager
在 Hierarchy 窗口空白处单击鼠标右键,在弹出的菜单中选择 Create Empty,创建一个空游戏对象,命名为 LCCManager,将其 Transform 归一,并通过 Add Component 为其添加 LCCManager 脚本。

创建 LCCManager 游戏对象并添加 LCCManager 脚本
创建 LCCRenderer
在 Hierarchy 窗口空白处单击鼠标右键,选择 Create Empty,创建一个空游戏对象,命名为 LCCRenderer,将其 Transform 设置为 Rotation.x = -90、Scale.x = -1。

创建 LCCRenderer 并设置 Transform(Rotation.x = -90、Scale.x = -1)
注意:
LCCRenderer 游戏对象的 Transform 设置(Rotation.x = -90、Scale.x = -1)是与 LCC 数据坐标系的对齐。关于坐标系对齐的详细说明,参见 Unity SDK 简介 中的「LCC 数据坐标系」。
数据前置条件
获取 Mesh 数据生效的前提是场景带有对应的碰撞 Mesh 数据(由 LCC Studio 重建时生成)。此外,该能力有以下约束:
- 仅 PC / Mac 平台,且仅支持本地数据源(HTTP 数据源不支持)。
- 只能在主线程调用。
- 原生格式数据(PLY / SOG / SPZ / splat / ksplat)不携带碰撞 Mesh 数据,调用相关方法会返回
null。
早期版本重建的数据可能不包含碰撞数据,此时调用 GetMeshes() 会返回 null。
同步获取全量 Mesh 数据
同步获取全量 Mesh 数据指一次性读取场景中所有碰撞 Mesh 分块的几何数据,通过 Renderer.GetMeshes() 方法实现。
新建一个 C# 脚本,命名为 Meshes.cs,代码如下:
using System.Collections.Generic;
using LCCCore;
using UnityEngine;
using UnityEngine.Rendering;
public class Meshes : MonoBehaviour
{
public LCCManager m_manager;
public string m_FilePath;
private LCCCore.Renderer m_renderer;
private bool m_isLoaded = false;
private Camera m_camera;
private const string kContainerName = "LCCVisualizedMeshes";
void Start()
{
m_camera = Camera.main;
m_isLoaded = false;
m_manager.SetPlatformType(PlatformType.PC);
m_renderer = m_manager.GetRender(this.transform);
m_renderer.Load(m_FilePath,
p => Debug.Log($"[Meshes] load progress: {p:P1}"),
() =>
{
Debug.Log($"[Meshes] file loaded: {m_FilePath}");
m_isLoaded = true;
m_manager.SetAutoFov(false);
},
e => Debug.LogError($"[Meshes] file load error: {e}")
);
}
public void onGetAllMeshes()
{
if (!m_isLoaded || m_renderer == null)
{
Debug.LogWarning("[Meshes] 数据尚未加载完成,无法获取 Mesh。");
return;
}
var meshes = m_renderer.GetMeshes();
if (meshes == null || meshes.Count == 0)
{
Debug.LogWarning("[Meshes] 未获取到 Mesh(数据可能不含碰撞体)。");
return;
}
VisualizeMeshes(meshes);
}
private void VisualizeMeshes(List<LCCMeshBlock> meshes)
{
// 清理上一次的可视化结果,避免重复堆积
Transform root = m_renderer.Transform;
Transform old = root.Find(kContainerName);
if (old != null) Destroy(old.gameObject);
var container = new GameObject(kContainerName).transform;
container.SetParent(root, false);
Material material = CreateVisualMaterial();
for (int i = 0; i < meshes.Count; i++)
{
LCCMeshBlock block = meshes[i];
if (block.vertices == null || block.vertices.Length == 0) continue;
if (block.triangles == null || block.triangles.Length < 3) continue;
var go = new GameObject(string.IsNullOrEmpty(block.id) ? $"MeshBlock_{i}" : block.id);
go.transform.SetParent(container, false);
var mesh = new Mesh { name = go.name };
// 单块顶点数可能超过 65535,必须使用 32 位索引,否则几何错乱
mesh.indexFormat = IndexFormat.UInt32;
mesh.vertices = block.vertices;
mesh.triangles = block.triangles;
mesh.RecalculateNormals();
mesh.RecalculateBounds();
var mf = go.AddComponent<MeshFilter>();
mf.sharedMesh = mesh;
var mr = go.AddComponent<MeshRenderer>();
mr.sharedMaterial = material;
}
Debug.Log($"[Meshes] 已为 {meshes.Count} 个 Mesh 分块创建 GameObject 并可视化。");
}
private Material CreateVisualMaterial()
{
Shader shader = Shader.Find("Universal Render Pipeline/Lit")
?? Shader.Find("Standard")
?? Shader.Find("Diffuse");
if (shader == null) return null;
var mat = new Material(shader) { color = new Color(0.7f, 0.7f, 0.7f, 1f) };
// 碰撞网格三角形绕序不保证一致,关闭背面剔除以完整显示
mat.SetFloat("_Cull", (float)CullMode.Off);
return mat;
}
}
说明:
GetMeshes()是同步重操作:会同步读盘、解析并分配内存。大场景下,如一份 262 块的数据,可能需读取大量磁盘数据、分配数GB至数十GB托管内存,请调用一次并自行缓存,切勿每帧调用。vertices/boundsLocal位于物体本地坐标系,因此把可视化 GameObject 挂到Renderer.Transform下即可自动对齐。- 单块顶点数可能超过 65535(实测最大 390,656),自建
Mesh前必须设置mesh.indexFormat = IndexFormat.UInt32,否则几何会错乱。- 碰撞网格三角形绕序不保证一致,示例中关闭了背面剔除(
_Cull = Off)以便完整显示。
将脚本挂载到 LCCRenderer 游戏对象上,并在 Inspector 中把 LCCManager 游戏对象拖到 Manager 属性、填写 File Path。
然后创建一个 Button,将 onGetAllMeshes() 方法关联到其 onClick 事件。
运行后点击 Button,即可看到场景中的全量碰撞 Mesh。

点击按钮后可视化全量碰撞 Mesh 数据
按需获取 Mesh 数据
按需获取指只读取指定的部分Mesh数据,而不是一次性读取全部的Mesh数据,避免造成大的IO和内存开销,下面以读取相机周围一定范围内的 Mesh 分块为例。
先通过 GetMeshBlockInfos() 拿到所有分块的元信息(含世界空间包围盒),在世界坐标系下计算渲染相机中心点到分块 BBOX 表面的距离,筛选出距离小于 100 m 的分块 id,再用带参的 GetMeshes(ids) 只读取这些分块。
在 Meshes.cs 中新增以下方法:
public void onRequireMeshes()
{
if (!m_isLoaded || m_renderer == null)
{
Debug.LogWarning("[Meshes] 数据尚未加载完成,无法获取 Mesh。");
return;
}
// 1. 零 I/O 读取全部分块元信息,按相机到 BBOX 表面距离筛选 100 m 内的分块
var infos = m_renderer.GetMeshBlockInfos();
if (infos == null || infos.Count == 0)
{
Debug.LogWarning("[Meshes] 未获取到 Mesh 信息(数据可能不含碰撞体)。");
return;
}
Vector3 camPos = m_camera.transform.position;
var ids = new List<string>();
foreach (var info in infos)
{
// 相机位于 BBOX 内部时 ClosestPoint 返回相机自身、距离为 0,同样会被选中
if (calculateMinDistanceToAABB(info.boundsWorld, camPos) < 100f)
ids.Add(info.id);
}
if (ids.Count == 0)
{
Debug.LogWarning("[Meshes] 相机周围 100m 内没有 Mesh 分块。");
return;
}
// 2. 按 id 只读取筛选出的分块并可视化
var meshes = m_renderer.GetMeshes(ids);
if (meshes == null || meshes.Count == 0)
{
Debug.LogWarning("[Meshes] 按 id 获取 Mesh 失败。");
return;
}
VisualizeMeshes(meshes);
Debug.Log($"[Meshes] 相机周围 100m 内选中 {ids.Count} 个分块,可视化 {meshes.Count} 个。");
}
// 计算点到 AABB 表面的最近距离(点位于 AABB 内部时返回 0)
private static float calculateMinDistanceToAABB(Bounds _aabb, Vector3 _point)
{
Vector3 p = _aabb.ClosestPoint(_point);
return Vector3.Distance(p, _point);
}
说明:
GetMeshBlockInfos()是零 I/O、零解析操作,只读内存中已有的元信息,可放心调用。- 距离筛选用的是
boundsWorld(世界空间包围盒),相机中心点取Camera.transform.position。- 使用
Bounds.ClosestPoint计算点到 AABB 表面的最近距离:当相机位于 BBOX 内部时,ClosestPoint返回相机自身、距离为 0,该分块同样会被选中。- 示例中的筛选半径
100f(单位:米)可按需调整。
创建一个 Button,将 onRequireMeshes() 关联到其 onClick 事件。
运行后点击按钮,即可看到相机周围 100 m 范围内的 Mesh 分块。

按需获取相机周围 100m 内的 Mesh 数据
关键步骤
获取并可视化场景 Mesh 数据的关键步骤:
- 完成
SetPlatformType等前置配置,并通过GetRender+Load加载 LCC 场景。 - 确认数据携带碰撞 Mesh 数据(原生格式或早期重建的数据可能不含)。
- 全量获取:调用
GetMeshes()一次性读取所有分块,并自行缓存结果,避免重复调用。 - 按需获取:先
GetMeshBlockInfos()拿分块世界包围盒,按相机距离筛选 id,再GetMeshes(ids)只读目标分块。 - 可视化时把 GameObject 挂到
Renderer.Transform下对齐本地坐标,并用IndexFormat.UInt32构建 Mesh。
获取 Mesh 相关 API
以下方法都挂在 Renderer 上,需要场景加载完成后调用,且数据需携带碰撞 Mesh 数据。
GetMeshBlockInfos
// 返回值:List<LCCMeshBlockInfo> → 碰撞 Mesh 分块元信息;无 Mesh 或不可用时返回 null
public List<LCCMeshBlockInfo> GetMeshBlockInfos()
返回全部碰撞 Mesh 分块的元信息(id、世界/本地包围盒、顶点数、面数)。零 I/O、零解析,只读内存中已有的 meta。
调用示例:
var infos = renderer.GetMeshBlockInfos();
foreach (var info in infos)
{
Debug.Log($"{info.id}: verts={info.vertexCount}, faces={info.faceCount}, bbox={info.boundsWorld.size}");
}
说明:
- 仅 PC / Mac 平台 + 本地数据源;只能主线程调用。
- 典型用法:先用它拿各块世界 BBox,挑出需要的 id,再调
GetMeshes(ids)只读那几块。 - 原生格式(PLY / SOG / SPZ / splat / ksplat)无 Mesh,返回
null(属正常情况,不打日志)。
GetMeshes
// 返回值:List<LCCMeshBlock> → 全部碰撞 Mesh 几何数据;不可用时返回 null
public List<LCCMeshBlock> GetMeshes()
同步返回全部碰撞 Mesh 分块的几何数据(顶点 + 三角形索引,本地坐标系)。这是重操作(同步读盘 + 解析 + 分配),请调用一次并自行缓存,切勿每帧调用。
调用示例:
var meshes = renderer.GetMeshes();
foreach (var block in meshes)
{
// block.vertices / block.triangles 在物体本地坐标系
}
说明:
- 仅 PC / Mac 平台 + 本地数据源;只能主线程调用。
- 大场景请改用
GetMeshBlockInfos()挑 id +GetMeshes(ids),避免一次性分配海量托管内存。 - 单块顶点数可超 65535,自建 Mesh 前必须设
mesh.indexFormat = IndexFormat.UInt32。
GetMeshes(按 id)
// 返回值:List<LCCMeshBlock> → 指定 id 的碰撞 Mesh 几何数据;无命中时返回 null
public List<LCCMeshBlock> GetMeshes(IEnumerable<string> _ids)
同步返回指定 id 的碰撞 Mesh 分块几何数据(本地坐标系)。id 由 GetMeshBlockInfos() 提供。
| 参数 | 类型 | 说明 |
|---|---|---|
_ids | IEnumerable<string> | 目标分块 id 集合(由 GetMeshBlockInfos() 提供) |
调用示例:
var ids = new List<string> { "block-0", "block-3" };
var meshes = renderer.GetMeshes(ids);
说明:
- 未知 id 跳过并告警;重复 id 去重(不重复读盘);返回顺序为 ids 的遍历顺序(去重后)。
- 入参为
null、为空、或无一命中时返回null并告警。 - 其余约束同
GetMeshes()。
LCCMeshBlockInfo 数据结构
public struct LCCMeshBlockInfo {
public string id; // 节点 id,与 LCCMeshBlock.id 对应
public Bounds boundsWorld; // 世界空间 AABB,用于挑选 id
public Bounds boundsLocal; // 本地空间 AABB
public int vertexCount; // 顶点数
public int faceCount; // 面数
}
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 节点 id,与 LCCMeshBlock.id 对应 |
boundsWorld | Bounds | 世界空间 AABB,用于挑选 id |
boundsLocal | Bounds | 本地空间 AABB |
vertexCount | int | 顶点数 |
faceCount | int | 面数 |
LCCMeshBlock 数据结构
public struct LCCMeshBlock {
public string id; // 节点 id
public Vector3[] vertices; // 顶点,本地坐标系
public int[] triangles; // 三角形索引,每 3 个一组
public Bounds boundsLocal; // 与 vertices 同空间的包围盒
}
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 节点 id |
vertices | Vector3[] | 顶点数组,物体本地坐标系 |
triangles | int[] | 三角形索引,每 3 个一组 |
boundsLocal | Bounds | 与 vertices 同空间的包围盒 |
注意:
vertices/boundsLocal都在物体本地坐标系。要在 Unity 中可视化,把承载 Mesh 的 GameObject 挂到Renderer.Transform之下即可自动对齐。单块顶点数可超过 65535,自建 Mesh 前必须设置mesh.indexFormat = IndexFormat.UInt32。