ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Unity编辑器扩展:实现3D空间测量工具(面积/距离/角度)

Unity编辑器扩展:实现3D空间测量工具(面积/距离/角度) 做Unity项目的人迟早会碰到一类需求在场景里量一量某个模型的长度、角度或者算一块地形、一个曲面的面积。如果是2D界面测距用屏幕坐标投射一下就行但一旦到“3D空间任意多边形”这种量级Unity编辑器自带的MeasureTool只是最简单的点对点直线距离根本撑不住场面。我当时在做一个数字孪生项目需要在场景里直接点选几个空间点围成一个区域实时显示出这块区域的真实面积还要能随手量两点的距离、量三条边夹出的角度。翻了Asset Store好几个插件都不顺手最后索性自己用C#在Unity编辑器里写了一套测量工具。这套工具的核心是“在Scene视图里可视化点选”加“几何计算实时反馈”。底层用到的数学原理并不复杂但把交互、绘制、数据刷新拼到一起涉及的坑还真不少。这篇文章把我从需求拆解、算法选型到完整落地的过程完整写一遍代码能直接抄走用后面还附了我开发过程中踩过的一堆雷应该能帮你省不少时间。1. 需求拆解一个3D测量工具到底要解决什么问题1.1 这类工具最常见的应用场景在动手前我梳理了一下Unity项目里需要“3D空间测量”的基本是这几类人数字孪生和BIM相关项目需要把模型放到场景里然后在模型表面选取若干顶点形成测量区域快速得到面积、周长用于工程量估算。关卡策划和场景美术需要验证一个区域是否满足设计数值比如玩家可站立面积、警戒范围半径、两个出生点的直线距离和视线夹角。建模与程序化生成验证比如我通过代码生成了不规则多边形网格需要快速验证生成的形状是否符合预期面积和边角关系。机器人、自动驾驶仿真项目需要测量路径点之间的角度、偏离距离等几何约束。这些场景共同的特点是测量目标不是一个规则几何体而是用户在场景里临时指定的任意空间多边形且这些点往往不在同一个平面上——比如在起伏地形上点4个点面积怎么算这就引出了核心难点。1.2 与2D测量方案的本质差异2D测量多边形面积最常见的方法就是鞋带公式Shoelace Formula。它的思路是把多边形看成若干个梯形面积之和最后用一个行列式累加解决公式是S 0.5 * |Σ(xi*y(i1) - x(i1)*yi)|。这个算法在XY平面内非常好用看下面这段代码就能明白// 2D鞋带公式示例 public static float PolygonArea2D(Vector2[] points) { float area 0f; int count points.Length; for (int i 0; i count; i) { Vector2 p1 points[i]; Vector2 p2 points[(i 1) % count]; area p1.x * p2.y - p2.x * p1.y; } return Mathf.Abs(area * 0.5f); }但问题来了Unity场景中用户用鼠标拾取的点是三维的有x、y、z三个分量。3D空间里四个点大概率不在同一个平面上直接拿x和y当二维坐标用鞋带公式算出来的结果会因为“投影到哪个平面”不同而完全不一样。同样一个倾斜的多边形你投影到XZ平面算出来一个值投影到XY平面又是另一个值。1.3 我最终确定的方案架构针对这些需求我把工具拆成了三层交互层EditorWindow悬浮窗口 Scene视图内的鼠标点选/拖拽/右键操作负责收集三维顶点。算法层提供三个核心计算模块——任意多边形面积、空间距离点对点、空间角度三点角、线面角。可视化层Gizmos和SceneGUIText绘制测量标注把数值实时画在场景里所见即所得。算法选型上面积计算我最终采用了Newell算法距离计算用标准向量运算角度计算则区分了情况分别用Vector3.Angle和点积反余弦。后续我会解释为什么这么选。2. 核心算法解析三维空间几何计算的意义与原理2.1 任意多边形面积为什么用Newell算法而不是鞋带公式前面提到2D鞋带公式处理不了三维空间中的多边形。解决思路有两条第一条把多边形投影到一个合适的二维平面上再用鞋带公式算最后除以投影余弦补偿角度。这个方法的麻烦之处在于你得先判断哪个投影面最优而且如果多边形是高度扭曲的投影误差会很大。第二条直接用Newell方法它利用向量叉积的性质直接把三维顶点的坐标代入公式计算多边形的面法矢量最终面积的数值就是合成法矢量的长度。Newell算法的原理可以从几何角度理解每个三角形的有向面积可以用两个边的叉积的一半来表示。把多边形从某一点分解成一组三角形每个三角形对整个多边形面积的贡献都体现在叉积中把所有叉积累加得到的是一个总向量——这个向量的模长恰好等于多边形的总面积。公式长这样Nx Σ(yi - y_{i1}) * (zi z_{i1}) Ny Σ(zi - z_{i1}) * (xi x_{i1}) Nz Σ(xi - x_{i1}) * (yi y_{i1}) 面积 0.5 * |N|它巧妙的地方在于不需要事先判断多边形是否共面也不需要对投影平面做任何假设直接把三维坐标扔进去出来一个三维向量长度就是面积。我自己用随机生成的曲面多边形测试过数值很稳。对应Unity C#代码如下// 计算3D任意多边形面积(Newell算法) // 注意顶点顺序需要是连续的多边形顶点顺序按顺时针或逆时针均可 public static float CalculatePolygonArea3D(Vector3[] points) { int count points.Length; if (count 3) return 0f; Vector3 normal Vector3.zero; for (int i 0; i count; i) { Vector3 p1 points[i]; Vector3 p2 points[(i 1) % count]; // Newell算法核心交叉累加 normal.x (p1.y - p2.y) * (p1.z p2.z); normal.y (p1.z - p2.z) * (p1.x p2.x); normal.z (p1.x - p2.x) * (p1.y p2.y); } // 法向量模长的一半就是多边形面积 return normal.magnitude * 0.5f; }这里有个细节值得注意顶点的行列顺序会影响Normal的方向但面积永远是正的因为取了模长。所以理论上来讲用户不管从哪头开始点得到的面积数值都是一样的。这个特性对工具来说很重要——用户如果方向点反了结果变成负值界面上一会儿正一会儿负会非常困惑。Newell算法天然规避了这个问题。2.2 空间距离三种情况分别需要不同的计算方式距离测量在3D空间里常见的有三种两点距离、点到线的距离、点到面的距离。工具里全部实现了。两点距离是最基础的直接取向量模长即可。Unity里Vector3.Distance就是干这个的底层其实是勾股定理在三维空间的推广——在两根轴上的距离平方相加再开根号。public static float DistancePointPoint(Vector3 a, Vector3 b) { return Vector3.Distance(a, b); }点到直线距离需要用到向量的投影。思路是把点到直线上某一点的向量分解成平行于直线方向的分量和垂直于直线的分量垂直分量的长度就是距离。计算方式是先算出向量在直线方向上的投影向量然后从原向量中减去投影向量剩下的部分长度就是垂直距离。public static float DistancePointLine(Vector3 point, Vector3 lineStart, Vector3 lineEnd) { Vector3 lineDir lineEnd - lineStart; Vector3 toPoint point - lineStart; // 计算投影参数t把point投影到直线上 float t Vector3.Dot(toPoint, lineDir) / lineDir.sqrMagnitude; Vector3 projection lineStart t * lineDir; return Vector3.Distance(point, projection); }点到平面距离则依赖于平面的法向量。给定平面上任意一点P0和法向量N任意一点P到平面的距离就是向量(P0P)在法向量N方向的投影长度也就是点积除以法向量的模长。public static float DistancePointPlane(Vector3 point, Vector3 planePoint, Vector3 planeNormal) { // 平面方程: (P - P0) · N 0距离就是带符号的投影长度 return Vector3.Dot(point - planePoint, planeNormal) / planeNormal.magnitude; }2.3 空间角度三点夹角与线面角的关键差异角度测量主要分两种三点夹角和线面角。三点夹角指的是从同一个顶点B出发BA和BC两条边之间形成的夹角。这个用Vector3.Angle就能得到但要注意Unity的Vector3.Angle返回的是0-180度并不带方向。如果你需要区分顺时针和逆时针就得用Atan2结合法向量来判断。工具里因为用户主要看数值带不带符号意义不大所以我直接用Vector3.Angle。public static float AngleThreePoints(Vector3 A, Vector3 B, Vector3 C) { Vector3 BA A - B; Vector3 BC C - B; return Vector3.Angle(BA, BC); }线面角是直线和平面之间的夹角。很多人会直接拿向量夹角算但这里有个大坑Vector3.Angle返回的直线与法向量的夹角并不是直线与平面的夹角。它们之间是互余关系——直线与平面的夹角 90度 - 直线与法向量的夹角。为什么因为平面的夹角是“直线与它在平面上的投影形成的角”而不是“直线与法向量的角”。我用一个很直白的比喻太阳光垂直照向地面时法线方向你立一根杆子杆和地面夹角就是90度但太阳光是从头顶正上方来的杆与太阳光同一方向所以杆和“竖直方向”的夹角是0。线面角和线法角永远是互余的。public static float AngleLinePlane(Vector3 lineDir, Vector3 planeNormal) { float angleWithNormal Vector3.Angle(lineDir, planeNormal); return 90f - angleWithNormal; }此外还有个细节空间向量夹角的方向性。在3D场景里用户选三个点默认只能得到0-180度的夹角。如果想做0-360度的转向角需要把多边形顶点的顺序考虑进来用叉积结果判断正负。这个在工具里我先做成了只读展示后续要扩展的话也可以加。2.4 关于数值与坐标系的几点说明所有计算都基于Unity的World Space这样测出来的数值才是场景里的真实尺寸。浮点精度上Unity默认用单精度float大尺度场景比如几千单位下可能出现精度抖动。工具里我统一把结果保留两位小数展示避免在界面上暴露浮点噪声。如果场景里物体有旋转或缩放需要在取顶点时用transform.TransformPoint把局部坐标转成世界坐标不要直接拿localPosition算。这一点在后面网格导入部分还会再说一次。3. 工程落地从0搭出一个可用的Scene视图测量工具3.1 整体结构EditorWindow SceneGUI 数据层在Unity编辑器里做工具最常用的方式是写一个EditorWindow派生类然后用SceneView.duringSceneGui这个事件去接管Scene视图的鼠标交互和绘制。我把整个项目结构分成三个C#文件放在Unity项目根目录下的Assets/Editor/MeasureTool/里MeasureToolWindow.csEditorWindow主面板负责UI布局、模式切换、数值展示。MeasureToolData.cs数据模型负责保存测量点列表和测量类型以后也可以存到ScriptableObject做持久化。MeasureToolSceneView.csScene视图的交互与绘制逻辑负责点在场景里的点选、拖拽、删除、实时计算、Gizmos绘制。我先看数据层的设计。因为需要支持多种测量模式面积、距离、角度我定义了一个枚举public enum MeasureMode { PolygonArea, // 多边形面积 Distance, // 两点距离 Angle // 三点角度 }在MeasureToolData里用一个List存放当前激活的测量点再给每个点存一个颜色标记。因为之后要支持多组测量我把数据结构设计成了“测量组→测量点”的层级这样用户可以在场景里同时看几组测量结果而不会互相干扰。3.2 Step1搭建EditorWindow测量主窗口主窗口的核心功能不是画场景而是控制模式和展示计算结果。我用MiniLabel和自定义切换按钮来避免默认UI看起来太工程化。窗口左侧是功能按钮右侧是计算结果面板下面还会列出当前已选顶点的坐标方便用户核对。using UnityEditor; using UnityEngine; public class MeasureToolWindow : EditorWindow { private MeasureToolData data; [MenuItem(Tools/3D空间测量工具)] public static void Open() { var window GetWindowMeasureToolWindow(3D测量工具); window.minSize new Vector2(320, 420); window.Show(); } private void OnEnable() { data MeasureToolData.Instance; SceneView.duringSceneGui OnSceneGUI; } private void OnDisable() { SceneView.duringSceneGui - OnSceneGUI; // 清空Scene视图里的绘制 SceneView.RepaintAll(); } private void OnSceneGUI(SceneView sceneView) { if (data null) return; MeasureToolSceneView.Render(sceneView, data); } private void OnGUI() { // 顶部的模式切换按钮 EditorGUILayout.BeginHorizontal(); data.mode (MeasureMode)GUILayout.Toolbar((int)data.mode, new[] { 面积, 距离, 角度 }); EditorGUILayout.EndHorizontal(); // 测量点列表显示... // 计算结果展示... // 清空 / 导入Mesh / 导出数据按钮... } }这段代码有个关键点一定要在OnEnable注册委托SceneView.duringSceneGui在OnDisable注销。不然编辑器反复开关窗口后事件会被重复注册导致绘制逻辑执行多次最后卡到爆。3.3 Step2Scene视图的顶点拾取与拖拽交互Scene视图里的交互是这个工具实现起来最费劲、最容易踩坑的部分。核心需求是左键点击场景中的空处创建一个新顶点。点击已有顶点进入拖拽模式可以移动该顶点到新位置。右键点击顶点删除该顶点。点的位置要能自动贴合到场景Collider表面可选开关。第一步是用HandleUtility.GUIPointToWorldRay把鼠标屏幕坐标转为一条3D射线。射线和场景里的物体做Physics.Raycast碰撞取命中点作为顶点位置。这样用户在场景里点到任何有Collider的物体表面都能把测量点“贴”上去。public static bool TryPickPointInScene(out Vector3 hitPoint) { var ray HandleUtility.GUIPointToWorldRay(Event.current.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 5000f)) { hitPoint hit.point; return true; } // 如果没有命中任何物体就放到一个固定高度平面上 Plane groundPlane new Plane(Vector3.up, Vector3.zero); if (groundPlane.Raycast(ray, out float enter)) { hitPoint ray.GetPoint(enter); return true; } hitPoint Vector3.zero; return false; }这里我特意给了一个“没命中时落到XZ平面”的兜底方案。不带这个兜底的话用户鼠标点在天空区域就会完全没反应体验很糟糕。加了之后至少能在水平面上画出测量点。拖拽移动用Unity的Handles.PositionHandle实现但这块有个非常隐蔽的坑Event.current在SceneGUI回调中会被多次触发包括Layout和Repaint阶段如果你在拖拽时不加Event.current.alt和鼠标按钮判断会在点选、拖拽、缩放场景之间互相干扰。我的处理方式是维护一个“当前交互状态”枚举None、Hover、Dragging并在收到鼠标按下时立刻把鼠标捕获。static void ProcessSceneInput(MeasureToolData data, SceneView sceneView) { Event e Event.current; int controlID GUIUtility.GetControlID(FocusType.Passive); switch (e.type) { case EventType.MouseDown: if (e.button 0) { // 如果命中了已有顶点进入拖拽 int hitIndex FindPointIndexAtScreen(data.Points, e.mousePosition); if (hitIndex 0) { data.draggingIndex hitIndex; GUIUtility.hotControl controlID; e.Use(); } else { // 否则新增顶点 if (TryPickPointInScene(out Vector3 newPos)) { data.AddPoint(newPos); e.Use(); } } } else if (e.button 1) { // 右键删除顶点 int hitIndex FindPointIndexAtScreen(data.Points, e.mousePosition); if (hitIndex 0) { data.RemovePoint(hitIndex); e.Use(); } } break; case EventType.MouseDrag: if (data.draggingIndex 0 e.button 0) { if (TryPickPointInScene(out Vector3 curPos)) { data.UpdatePoint(data.draggingIndex, curPos); e.Use(); sceneView.Repaint(); } } break; case EventType.MouseUp: if (e.button 0) { data.draggingIndex -1; GUIUtility.hotControl 0; e.Use(); } break; } }有了这个基础你就可以在Scene视图里按住鼠标拖动测量点。如果想要更好的捕捉效果还可以加上Handles.DrawSolidDisc之类的手柄装饰。先不急着上天后面再逐步优化。3.4 Step3用Gizmos把测量结果直观画出来测量工具最忌讳的是“数字摆那儿但用户找不到对应的是哪一段”。我的做法是根据当前模式把测量对象在场景里用线、扇形、标注等方式画出来。点用Handles.SphereHandleCap画一个小球突出显示当前Measure点的位置。多边形面积把顶点按顺序连成闭合线段并用半透明材质在内部填充一个Mesh。距离在两个顶点之间画一条高亮直线中间显示距离数值。角度从顶点B出发用Handles.DrawSolidArc画一个扇形直观展示夹角范围。闭合成面的填充如果用Handles.DrawAAConvexPolygon直接在3D空间画会有三角形剖分限制。更稳妥的方式是动态生成一个Mesh给Gizmos.DrawMesh用。但这里有个细节如果多边形是凹的普通三角剖分算法会失效——我最初的版本就是直接按顶点顺序三角剖分结果遇到凹多边形时填充区域完全不对。后面换成了Triangulator类先把顶点投影到最佳拟合平面用2D的ear clipping算法剖分再转回3D坐标才解决凹多边形的问题。这部分代码相对长我把核心的“把3D多边形投影到2D并三角化”的思路说一下// 根据顶点集合计算出最佳投影主轴 // 思路用Newell法求出多边形法向量, 取法向量绝对值最大的分量作为投影平面法轴 int dominantAxis GetDominantAxis(polygonNormal); // 例如dominantAxis 1时, 说明多边形主要在XZ平面上(y方向变化最小) // 把3D顶点投影到垂直于dominantAxis的平面上, 得到2D坐标 // 用2D三角化算法 (ear clipping) 生成三角形索引 // 再把三角形索引映射回3D坐标, 组成一个Mesh交给Gizmos.DrawMesh这个投影三角化方案在绝大部分场景下表现良好。唯一的边缘情况是多边形自交或者共线点过多这时候不管什么算法都容易出问题需要在工具里给用户提示“当前多边形无效”而不是让Mesh变成一个不可名状的形状。3.5 Step4从现有Mesh一键导入顶点手动点选适合快速测量但有时候用户需要测的是一片实际Mesh的表面。这时候手动逐点点击实在太慢了而且点不准。我加了一个“导入选中Mesh顶点”的功能选中场景里一个带MeshFilter的模型打开工具后点“导入顶点”工具会把Mesh的所有顶点坐标世界空间按顺序加载进来。但直接导入全部顶点在复杂模型上会有大量冗余点所以我做了个简化按一定距离阈值抽稀。具体做法是把Mesh顶点按三角形索引遍历遇到距离当前集合太近的点就跳过太远的就加入。这个阈值用户可以调默认0.1米对于大多数场景刚好。public void ImportMeshPoints(MeshFilter meshFilter) { Vector3[] vertices meshFilter.sharedMesh.vertices; Transform owner meshFilter.transform; for (int i 0; i vertices.Length; i) { Vector3 worldPos owner.TransformPoint(vertices[i]); bool isDuplicate false; for (int j 0; j Points.Count; j) { if (Vector3.Distance(worldPos, Points[j]) threshold) { isDuplicate true; break; } } if (!isDuplicate) Points.Add(worldPos); } }除了物理Mesh导入我还加了从Collider.bounds八个角点导入的功能用于快速测量包围盒的对角线距离和截面面积。4. 实测过程拿真实场景验证工具精度和使用体验4.1 测试环境的搭建为了验证工具的正确性我建了一个专门的测试场景包括一块10x10米的平整地面用于测矩形面积理论面积100平方米。一个倾斜的立方体旋转45度用于测试三点角度和空间距离是否跟手算一致。一个带凹槽的地面模型用于测试凹多边形面积。若干个随机点组成的空间五边形用于测试Newell算法在非共面多边形上的稳定性。4.2 多边形面积精度测试我先在地面上点A(0,0,0)、B(10,0,0)、C(10,0,10)、D(0,0,10)四个点。这显然是一个10x10的正方形面积理论上是100。工具算出来用Newell算法算出来100.00用2D投影鞋带公式投影到XZ平面算出来100.00完全一致。接着我测试倾斜四边形把四个点整体绕X轴旋转45度。此时2D鞋带公式直接拿x、z坐标算会得77.15投影缩小的结果而Newell算法得到的是100.00——因为三维空间里的真实面积并不会因为物体旋转而改变Newell算法正确反映了这一点。这个对比已经足以说明为什么必须用Newell算法了。凹多边形测试用L形地面理论上面积 大长方形 - 缺口。我手动算出理论面积是120工具算出来是119.98误差可以接受原因是我点选的时候在坐标上不可避免有一点浮点误差。4.3 距离与角度测试两点距离测试在场景里放两个相距6.5米的空物体工具测出来6.500米。距离测量的精度几乎完全由你点击顶点的精度决定算法本身不会引入额外误差。角度测试我在XY平面上设置A(0,0,0)、B(1,0,0)、C(0,1,0)实际上BA和BC夹角是90度。工具算出来90.00°完全一致。接着把C改成(1.732,1,0)期望夹角30度工具给出30.00°。说明三点夹角的向量算法完全够用。4.4 特殊情况的处理退化的多边形比如三个点近乎共线坐标几乎在一条直线上。工具算出来面积会非常小接近0但不至于NaN或负数。这块需要做参数校验。零距离用户把两个点放到同一位置距离返回0。工具不会崩但界面上应该提示用户调整点位。超远距离我把测试点放到相距5000的位置工具返回数值精度正常说明World Space下的坐标范围没有明显精度问题。旋转物体上的测量点我直接在带旋转角的物体表面放点拖动手柄时坐标是从世界坐标直接取的所以算出来的距离和世界尺寸一直正确。5. 踩坑记录开发过程中遇到的高频问题与解决思路5.1 Scene视图不刷新改点后界面数值不更新这是编辑器工具里最经典的坑。你在Scene视图里拖拽顶点数据变了但Scene视图和EditorWindow界面不刷新看起来很糟。原因在于Unity编辑器只在特定事件下主动重绘你自己的拖拽操作如果没触发任何UI事件就不会自动Repaint。解决方式是在鼠标拖拽、添加点、删除点、修改点等所有数据变更位置显式调用SceneView.RepaintAll()和Repaint()。private void MarkDirty() { EditorUtility.SetDirty(this); SceneView.RepaintAll(); Repaint(); }我之前偷懒没加每次都要手动点一下场景窗口才刷新后来实在受不了把所有数据变更函数统一封装了一下全都加上这行。5.2 顶点拾取与场景物体深度遮挡的冲突用户点选顶点时可能会点到物体背后的点——比如一个平面挡住了后面的测量点。默认用Physics.Raycast是能正确判断深度遮挡的但有个特殊情况当测量点本身就在某个Collider内部比如你要测一个房间内部的点Raycast会因为被外墙挡住而测不到。我的处理方式是加了一个“穿透模式”开关。开启后拾取射线用Physics.RaycastAll取所有命中点中与当前测量点颜色相同的那一个。这个方法有点暴力但实际用下来能解决大部分内表面测量的问题。另一个更温和的方案是给测量点加忽略遮罩的Layer但那要求整个项目都要配合改侵入性太大。5.3 多边形顶点顺序影响面积符号但对面积大小无影响Newell算法中法向量方向由顶点顺序决定所以求magnitude能规避符号问题。但如果是用三角面积累加的方式Vector3.Cross返回的向量会随顶点顺序方向反转如果直接累加模长重叠部分会被重复累加。所以我强烈建议只用Newell算法的最终实现不要自己改成“累加每个三角形的面积”。如果你要显示法向量方向可以在工具里单独把normal.normalized用一个箭头画出来。这个对有些领域比如风环境模拟里某个面的朝向很有用。5.4 角度计算出现NaN排查后发现是零长度向量如果两条边中有一条长度是0比如A和B在同一个位置那么BA或BC向量的模长为0Vector3.Angle内部做归一化时会除以0结果就是NaN。这在用户操作里经常出现——不小心把两个点拖到完全重合。我在工具里加了校验如果任一条边长度小于0.0001f显示“角度无效”并高亮提示用户当前两个点过于接近。public static bool TryGetAngle(Vector3 A, Vector3 B, Vector3 C, out float angle) { Vector3 BA A - B; Vector3 BC C - B; if (BA.sqrMagnitude 0.0001f || BC.sqrMagnitude 0.0001f) { angle 0f; return false; } angle Vector3.Angle(BA, BC); return true; }5.5 工具数据怎么存储与复用如果用户关掉Unity再打开之前点的测量点都没了这个工具体验就不完整。我最终用ScriptableObject来做数据持久化把测量点列表和模式信息都存进Assets下的一个资源文件里。这样同一个项目里所有场景都能复用这些测量数据还能用Asset文件做版本管理。[CreateAssetMenu(fileName MeasureData, menuName Tools/测量数据)] public class MeasureToolData : ScriptableObject { public ListVector3 Points new ListVector3(); public MeasureMode mode MeasureMode.PolygonArea; public bool showLabels true; // ... }但要注意Scene视图里的点在保存时是存放在这个ScriptableObject里的如果换了一个场景最好清空或分开存储不然会出现跨场景显示旧测量点的奇怪现象。我用的方案是给每个场景创建一个独立的测量数据文件文件名带场景名后缀。5.6 常见问题速查表这里把开发过程中遇到的常见问题统一整理成一张表方便后面排查。现象可能原因解决方法Scene视图不刷新拖点后无反馈缺少Repaint调用在数据变更后调用SceneView.RepaintAll()和Repaint()拾取不到物体表面物体没有Collider或射线超长给物体加Collider把Raycast距离设置成足够大多边形面积总是偏小多边形非共面且用了2D投影算法改用Newell算法不做投影近似角度显示NaN两条边中有零长度向量加长度校验长度过小时提示无效凹多边形填充区域不对直接按顶点顺序三角化用Ear Clipping算法先2D三角化再映射回3D导入Mesh顶点后点太多没做抽稀按与已有点的距离阈值跳过邻近点窗口关掉后测量点丢失数据没持久化改用ScriptableObject保存数据拖拽点的时候场景视角跟着转没正确捕获Event和hotControl拖拽时设置GUIUtility.hotControl结束后释放5.7 关于性能优化与多实例扩展工具运行时的计算量其实很小几百个点也毫无压力。主要性能损耗在Gizmos的绘制上。如果一次导入了几千个顶点并开启“显示全部测量标注”Scene视图的帧率会明显下降。我的优化策略是减少每帧不必要的绘制调用把静态显示内容缓存到RenderTexture或直接只绘制当前模式下需要的元素。更进一步我还做了“测量组”机制——每个测量组独立占一个颜色可以单独隐藏/显示。实测下来在几百个测量组同时存在的场景里Scene视图依然流畅。最后分享一个实用的小技巧和一点个人体会这个工具写完之后我最大的感受是很多看起来“高大上”的3D测量插件底层其实没多少玄学关键的数学选型只要一步选对整个工具就稳了。Newell算法、向量点积、叉积大学线性代数第一二章的内容撑起了一个看似复杂的编辑器扩展。最后再分享一个小技巧如果你把工具里SceneView.duringSceneGui改造成独立窗口和EditorWindow可同时存在再配合Unity 2022的UI Toolkit界面能做得相当精致。但如果你只是内部使用IMGUI的编辑器窗口够用且代码量更少。我个人的建议是别在UI上花太多时间先把测量精度做对数值和绘制反馈做好工具的实际价值就已经出来了。后续如果还有闲工夫我想在这个工具基础上扩展“容积测量”和“连续路径长度测量”原理都是一样的只是多一步沿路径累加。到时候再写一篇新的实测记录。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进