资讯动态

Unity相机视锥体与任意平面求交:获取世界坐标可视四边形

发布时间:2026/10/18 18:55:46 来源:尧图企业网站定制
1. 这个需求到底在解决什么问题——从实际开发场景说起“Unity 获得相机在某一平面上可视范围的四个角的世界坐标”——光看标题很多刚接触渲染或UI适配的同学可能第一反应是“这不就是算个投影矩形吗调个Camera.WorldToScreenPoint不就完了”但真正在项目里踩过坑的人会立刻皱眉WorldToScreenPoint返回的是屏幕像素坐标而我们要的是“在某个自定义平面上”的世界坐标。这个“某个平面”可能是地面、UI画布、AR锚点平面、动态生成的网格表面甚至是倾斜的斜坡或旋转后的建筑立面。它不一定是XY平面也不一定过原点更不一定与相机朝向垂直。我最早遇到这个需求是在做一个工业巡检AR系统时。客户要求把设备故障热区用3D Box标记实时投影到平板屏幕上并在屏幕底部叠加一个“当前可见区域提示条”显示“您当前视野覆盖了A区全部、B区左侧2/3、C区右上角”。这里的关键词是“当前视野覆盖”——不是相机视锥体在世界空间的8个顶点而是视锥体与指定平面这里是工厂地面相交后形成的四边形区域。这个四边形必须是精确的世界坐标才能和已有的GIS坐标系对齐否则热区标注就会漂移几十厘米现场工程师根本没法用。后来在做VR大屏协同系统时又碰一次多人共享同一块虚拟巨幕每个人视角不同需要实时计算“我的相机能看到巨幕的哪一块区域”并高亮显示。这里的“巨幕”是一个带Transform的Quad Mesh它的法线方向、位置、缩放全都不固定。这时候再用ScreenToWorldPoint硬凑结果要么报NaN要么坐标飞到天上去——因为ScreenToWorldPoint默认假设平面是Z0且正交于相机而我们的巨幕是任意姿态的。所以这个标题背后本质是一个空间几何求交问题将相机的视锥体frustum与一个给定的无限平面plane做截面运算得到一个凸四边形通常是梯形或平行四边形并求出其四个顶点在世界坐标系下的精确位置。它横跨了图形学基础射线-平面求交、Unity底层API理解Camera.CalculateFrustumCorners、以及工程鲁棒性如何处理相机正对平面、平面与视锥体无交、平面被裁剪等边界情况。这不是一个“查文档就能抄”的小技巧而是一套需要手推公式、验证逻辑、反复调试的空间思维闭环。核心关键词“Unity”“相机”“平面”“可视范围”“世界坐标”已经框定了技术栈和目标。它不涉及Shader编写不依赖URP/HDRP管线定制纯C#脚本层可解它不追求实时性能极限每帧计算但要求单次调用结果100%准确它面向的是中高级Unity开发者尤其是做AR/VR、仿真可视化、3D UI、游戏地图系统的人。如果你正在写一个“让UI始终贴合地面”的脚本或者要实现“镜头推进时自动缩放地图视野”又或者在做“基于视觉的机器人路径规划”——那这篇内容就是为你量身写的实操手册。2. 为什么不能直接用Camera.WorldToScreenPoint或ScreenToWorldPoint这个问题我被问过至少二十次每次我都先拉出一张草图用白板笔画出相机、视锥体、目标平面三者的关系。很多人卡住不是因为不会写代码而是对Unity坐标系转换的本质存在根本性误解。我们来一层层剥开。2.1 ScreenToWorldPoint 的“幻觉”陷阱Camera.ScreenToWorldPoint(Vector3 screenPos)这个API看似万能但它有一个极其关键的前提条件你传入的screenPos必须对应一个真实存在于该平面plane上的点。而这个“平面”是谁是Camera组件上设置的nearClipPlane近裁剪面吗不是。是farClipPlane远裁剪面也不是。它是你显式指定的一个Z深度值即screenPos.z参数。看官方文档原文“The z position is in world units from the camera.” 意思是screenPos.z代表的是从相机位置沿视线方向向前多少单位距离的那个平面。这个平面永远与相机视线垂直且位置由z值决定。举个例子// 假设相机在(0,0,-5)朝向Z轴正方向 Vector3 screenCenter new Vector3(Screen.width/2, Screen.height/2, 10f); // z10 Vector3 worldPos Camera.main.ScreenToWorldPoint(screenCenter); // worldPos 就是相机位置 10单位Z方向的点即 (0,0,5)问题来了你的目标平面是地面Y0而ScreenToWorldPoint算出来的是Z10那个垂直平面。这两个平面大概率不重合甚至可能平行比如地面是XY平面ScreenToWorldPoint算的是XZ平面那结果自然毫无意义。我见过最典型的错误代码是这样写的// ❌ 危险这是新手高频错误 Vector3 topLeft Camera.main.ScreenToWorldPoint(new Vector3(0, Screen.height, 0)); Vector3 topRight Camera.main.ScreenToWorldPoint(new Vector3(Screen.width, Screen.height, 0)); // ... 然后以为这四个点就在地面上这里z0意味着“在相机位置”结果四个点全挤在相机原点附近形成一个极小的四边形完全不是视野范围。这就是没理解z参数的物理意义导致的灾难。2.2 WorldToScreenPoint 的“单向性”局限Camera.WorldToScreenPoint(Vector3 worldPos)是ScreenToWorldPoint的逆过程它把世界坐标转成屏幕像素坐标。但它解决不了我们的需求因为我们根本不知道“平面上的哪四个点”对应屏幕的四个角。你想找的是“屏幕左上角像素它在地面上的落点在哪里”但WorldToScreenPoint只能告诉你“地面上某个已知点它在屏幕上显示在哪”。这是鸡生蛋还是蛋生鸡的问题。更致命的是精度问题。假设你暴力遍历地面网格的顶点用WorldToScreenPoint判断哪些点落在屏幕内再取边界点——这不仅性能爆炸每帧几万次矩阵乘法而且结果是离散的、不精确的。地面可能有缝隙你算出来的“角点”其实是两个三角面片的交界处坐标抖动严重。而我们需要的是数学上严格的交线顶点是连续空间里的解析解。2.3 正确的解题思路从视锥体出发而非屏幕坐标所有绕不开的正确路径都必须回到视锥体frustum本身。Unity的相机视锥体是一个四棱台frustum由6个平面围成近面、远面、左面、右面、上面、下面。我们要求的“可视范围四边形”本质上就是这个四棱台与目标平面的交集。而两个凸多面体的交集其边界必然是若干条线段这些线段的端点就是视锥体的12条棱边与目标平面的交点中位于视锥体内部的那些点。所以标准解法分三步获取视锥体的8个顶点在世界坐标系下提取构成视锥体的12条棱边每条棱是两个顶点的连线对每条棱边计算它与目标平面的交点筛选出交点在棱边线段范围内t∈[0,1]且在视锥体6个裁剪面内的点最终得到4个有效交点。这个思路听起来复杂但Unity提供了Camera.CalculateFrustumCorners这个高效API它直接返回近裁剪面和远裁剪面的4个角点共8个点省去了自己推导视锥体顶点的麻烦。而射线-平面求交是图形学最基础的公式一行代码就能搞定。这才是既准确又高效的正道。提示CalculateFrustumCorners返回的是相对于相机局部坐标的点必须用camera.transform.TransformPoint()转到世界坐标。漏掉这一步所有坐标都会错位。3. 核心实现四步精准计算附完整可运行代码现在进入实操环节。我会把整个计算流程拆成四个原子步骤每一步都解释原理、给出代码、说明注意事项。你可以直接复制粘贴进项目稍作修改就能用。3.1 第一步定义目标平面Plane平面在数学上由一个法向量normal和一个点point唯一确定。Unity提供了Plane结构体构造函数new Plane(Vector3 normal, Vector3 point)正是为此设计。但要注意法向量必须是单位向量normalized否则后续求交计算会出错。// ✅ 推荐方式用Transform直接构建平面 public Transform targetPlaneTransform; // 拖拽你的地面/巨幕/任意平面的Transform private Plane targetPlane; void UpdatePlane() { if (targetPlaneTransform null) return; // 平面法向量取Transform的上方向Y轴点取Transform位置 Vector3 planeNormal targetPlaneTransform.up; Vector3 planePoint targetPlaneTransform.position; // 关键必须归一化 planeNormal planeNormal.normalized; targetPlane new Plane(planeNormal, planePoint); }为什么用Transform.up而不是transform.forward因为绝大多数“可视范围”需求针对的是“水平面”如地面、桌面Y轴向上最符合直觉。但如果你的平面是竖直的如墙面那就用transform.forward或transform.right。核心原则是法向量方向决定了“哪一侧”是平面的正面影响求交结果的符号。如果发现算出来的点全在平面另一侧把法向量取反即可-planeNormal。注意不要用MeshRenderer.bounds或Collider.bounds来代替PlaneBounds是轴对齐包围盒AABB它无法表达任意旋转平面的姿态。曾经有同事用BoxCollider.center当planePoint结果相机一旋转交点就飘走——因为center是本地坐标没转到世界空间。3.2 第二步获取相机视锥体的8个顶点世界坐标这是最关键的一步也是最容易出错的一步。Camera.CalculateFrustumCorners返回的是相机局部空间的坐标必须手动转换。很多人在这里栽跟头写出的坐标全是乱的。// ✅ 完整、安全的获取方式 public Camera targetCamera; private Vector3[] frustumCorners new Vector3[8]; // 存储8个顶点 void CalculateFrustumCornersWorld() { if (targetCamera null) return; // Step 1: 在相机局部空间计算8个角点 // 注意参数是Vector2表示近/远裁剪面的宽高一半half-extents // 这里用ViewportToWorldPoint是错的必须用CalculateFrustumCorners var nearCorners new Vector3[4]; var farCorners new Vector3[4]; // 获取近裁剪面4个角局部坐标 targetCamera.CalculateFrustumCorners( new Rect(0, 0, 1, 1), // 全屏范围 targetCamera.nearClipPlane, Camera.MonoOrStereoscopicEye.Mono, nearCorners ); // 获取远裁剪面4个角局部坐标 targetCamera.CalculateFrustumCorners( new Rect(0, 0, 1, 1), targetCamera.farClipPlane, Camera.MonoOrStereoscopicEye.Mono, farCorners ); // Step 2: 将8个点全部转换到世界坐标 for (int i 0; i 4; i) { frustumCorners[i] targetCamera.transform.TransformPoint(nearCorners[i]); frustumCorners[i 4] targetCamera.transform.TransformPoint(farCorners[i]); } }这里有几个魔鬼细节Rect(0,0,1,1)表示“整个视口”不是像素尺寸。Unity的视口坐标是归一化的0~1所以不用除以Screen.width。Camera.MonoOrStereoscopicEye.Mono是必须的即使你用的是VR相机。Stereo模式会返回左右眼分别的角点增加复杂度而我们的需求是“单目视野范围”Mono足够。TransformPoint()是核心它执行position rotation * localPoint scale * localPointscale部分通常忽略。漏掉这一步所有点都在相机原点附近打转。3.3 第三步提取12条棱边并计算与平面的交点视锥体有8个顶点连接方式是近面4点构成四边形远面4点构成四边形再用4条边连接对应角点近左上→远左上近左下→远左下...。总共44412条棱。// ✅ 高效、无冗余的棱边枚举 private ListVector3 intersectionPoints new ListVector3(); void CalculateIntersections() { intersectionPoints.Clear(); // 定义12条棱边的索引对起点索引终点索引 int[][] frustumEdges { // 近裁剪面四边形顺时针左下、右下、右上、左上 new int[]{0, 1}, new int[]{1, 2}, new int[]{2, 3}, new int[]{3, 0}, // 远裁剪面四边形同样顺序 new int[]{4, 5}, new int[]{5, 6}, new int[]{6, 7}, new int[]{7, 4}, // 连接近远面的4条边对应角点 new int[]{0, 4}, new int[]{1, 5}, new int[]{2, 6}, new int[]{3, 7} }; foreach (var edge in frustumEdges) { Vector3 start frustumCorners[edge[0]]; Vector3 end frustumCorners[edge[1]]; // 射线-平面求交P start t * (end - start) // 代入平面方程normal · (P - point) 0 // 解得t normal · (point - start) / normal · (end - start) Vector3 rayDir end - start; float denominator Vector3.Dot(targetPlane.normal, rayDir); // 如果分母为0说明射线与平面平行无交点或无限交点 if (Mathf.Abs(denominator) 1e-6f) continue; float t Vector3.Dot(targetPlane.normal, targetPlane.point - start) / denominator; // t必须在[0,1]之间表示交点在线段上而非射线延长线上 if (t 0f t 1f) { Vector3 intersection start t * rayDir; // 额外校验交点是否在视锥体内可选提高鲁棒性 if (IsPointInFrustum(intersection)) { intersectionPoints.Add(intersection); } } } }射线-平面求交公式是图形学基石这里不做推导只强调两个实践要点分母判零Mathf.Abs(denominator) 1e-6f是必须的。浮点数计算总有误差不能用0。我曾因漏掉这行导致相机正对平面时程序卡死除零异常。t值范围t∈[0,1]确保交点在线段上。如果t0交点在起点之外t1在终点之外。这两种情况都不属于“视锥体棱边与平面的交”必须剔除。3.4 第四步筛选并排序4个有效交点形成四边形理论上12条棱边最多产生12个交点但视锥体与一个平面相交结果只能是0、1、2、3或4个点退化情况如相切得2点相交于一条线得无数点但我们只关心凸四边形。正常情况下我们期望得到恰好4个点。但浮点误差可能导致出现3个或5个点所以需要筛选。// ✅ 稳健的4点筛选与排序 public Vector3[] GetVisibleQuadCorners() { // 先确保前面三步已执行 UpdatePlane(); CalculateFrustumCornersWorld(); CalculateIntersections(); // Step 1: 去重浮点误差可能导致重复点 var uniquePoints new ListVector3(); foreach (var p in intersectionPoints) { bool isDuplicate false; foreach (var existing in uniquePoints) { if (Vector3.Distance(p, existing) 0.01f) { isDuplicate true; break; } } if (!isDuplicate) uniquePoints.Add(p); } // Step 2: 如果点数不是4尝试修复常见于相机几乎平行于平面 if (uniquePoints.Count ! 4) { // 启用备用方案用视锥体6个裁剪面与目标平面求交更稳定但稍慢 return CalculateByPlaneClipping(); } // Step 3: 对4个点按顺时针/逆时针排序形成凸四边形 // 方法以四边形中心为原点计算每个点的极角atan2 Vector3 center Vector3.zero; foreach (var p in uniquePoints) center p; center / uniquePoints.Count; uniquePoints.Sort((a, b) { float angleA Mathf.Atan2(a.z - center.z, a.x - center.x); float angleB Mathf.Atan2(b.z - center.z, b.x - center.x); return angleA.CompareTo(angleB); }); return uniquePoints.ToArray(); } // ✅ 备用方案用6个裁剪面求交当棱边法失效时 private Vector3[] CalculateByPlaneClipping() { // 获取相机6个裁剪面世界坐标系下 var planes GeometryUtility.CalculateFrustumPlanes(targetCamera); // 与目标平面两两求交线再求交线与视锥体的交点... // 此处省略因篇幅限制实际项目中建议封装为独立方法 return new Vector3[4]; // 占位符 }排序是最后一步也是让结果“看起来合理”的关键。如果不排序4个点是随机顺序你画出来的四边形可能是自相交的蝴蝶结。用极角排序Atan2是最直观的方法它把点按围绕中心的角度排列天然形成凸包。注意这里用了X-Z平面的Atan2因为大多数平面地面的法向量是Y轴所以投影到XZ平面最稳定。如果你的平面是竖直的法向量是X或Z就要换成对应的另外两个轴。实操心得我在某次调试中发现当相机非常接近地面nearClipPlane0.1时近裁剪面的4个点几乎共面导致某些棱边求交t值不稳定。解决方案是在CalculateFrustumCorners前临时增大nearClipPlane到0.3计算完再改回去。这是一个小技巧但能避免90%的边缘崩溃。4. 工程级避坑指南12个真实踩过的坑与独家解决方案这部分是价值千金的经验总结。不是教科书里的理论而是我在三个商业项目中累计调试超过200小时记录下来的血泪教训。每一个都附带可立即复用的解决方案。4.1 坑1相机旋转后交点坐标“漂移”——TransformPoint漏了Scale现象相机挂在一个空物体下该物体有非1的Scale比如Scale(2,1,1)计算出的四边形比实际视野大一倍。原因TransformPoint()内部会应用Scale变换。如果相机Transform的Scale不是(1,1,1)CalculateFrustumCorners返回的局部坐标会被错误缩放。解决方案强制重置相机Scale或手动剥离Scale影响。// ✅ 安全做法在CalculateFrustumCorners前临时清除Scale影响 Vector3 originalScale targetCamera.transform.localScale; targetCamera.transform.localScale Vector3.one; // 临时设为1 targetCamera.CalculateFrustumCorners(...); targetCamera.transform.localScale originalScale; // 恢复4.2 坑2目标平面是MeshRenderer但Bounds.center不是平面中心现象把meshRenderer.bounds.center当planePoint结果交点总在Mesh中心而非平面几何中心。原因Bounds.center是包围盒中心对于非对称Mesh如L形地板它根本不在平面上。解决方案用Mesh的顶点数据计算真实平面中心。// ✅ 计算Mesh顶点的平均位置适用于平面Mesh MeshFilter mf targetPlaneTransform.GetComponentMeshFilter(); if (mf ! null mf.sharedMesh ! null) { Vector3 sum Vector3.zero; foreach (var v in mf.sharedMesh.vertices) { sum targetPlaneTransform.TransformPoint(v); } planePoint sum / mf.sharedMesh.vertices.Length; }4.3 坑3VR/AR项目中StereoEye导致结果双份现象在Oculus Quest项目中CalculateFrustumCorners返回8个点但实际得到16个交点。原因Camera.MonoOrStereoscopicEye.Stereo会为左右眼各算一套角点。解决方案统一用Mono并在VR中通过XRDisplaySubsystem获取主眼相机。// ✅ VR安全写法 Camera mainCam Camera.current ! null ? Camera.current : targetCamera; mainCam.CalculateFrustumCorners(..., Camera.MonoOrStereoscopicEye.Mono, ...);4.4 坑4相机Near/Far Clip Plane设置过大导致远裁剪面角点精度丢失现象farClipPlane10000计算出的远角点坐标有明显抖动小数点后3位乱跳。原因浮点数在大数值区间精度急剧下降。10000.0001f和10000f在float32下可能相等。解决方案动态设置farClipPlane或用double精度计算需自定义。// ✅ 动态Far Clip根据目标平面距离调整 float maxDistance Vector3.Distance(targetCamera.transform.position, targetPlane.point); targetCamera.farClipPlane Mathf.Max(10f, maxDistance * 1.5f);4.5 坑5平面法向量未归一化导致求交t值计算错误现象t值总是大于1或小于0交点全被过滤掉。原因Plane构造时normal未normalizedVector3.Dot(normal, rayDir)结果放大了normal.magnitude倍。解决方案构造Plane时强制归一化加断言保护。// ✅ 铁律永远归一化 planeNormal planeNormal.normalized; targetPlane new Plane(planeNormal, planePoint); Debug.Assert(Mathf.Abs(planeNormal.magnitude - 1f) 0.001f, Plane normal must be normalized!);4.6 坑6相机处于目标平面“背面”交点全在负t侧现象相机在地面下方Y-1目标平面是Y0但一个交点都算不出来。原因t值为负被if (t 0f)过滤掉了。但物理上相机在平面下方视野应该与平面相交。解决方案检查相机与平面的相对位置动态调整求交逻辑。// ✅ 智能t值范围根据相机在平面哪一侧决定t的有效区间 float distToPlane targetPlane.GetDistanceToPoint(targetCamera.transform.position); if (distToPlane 0) { // 相机在平面正面t∈[0,1] if (t 0f t 1f) AddPoint(); } else { // 相机在平面背面t∈[-1,0]交点在相机后方 if (t -1f t 0f) AddPoint(); }4.7 坑7UI Canvas使用Screen Space - Overlay模式无法参与世界坐标计算现象想把“可视范围”投影到UI上但Canvas是Overlay模式没有世界坐标。解决方案切换Canvas Render Mode为Screen Space - Camera或World Space并指定一个相机。4.8 坑8动态生成的平面如Raycast创建的地板Plane未及时更新现象地板是运行时Instantiate的但targetPlane还是旧的交点错位。解决方案在生成平面后立即调用UpdatePlane()并用OnBecameVisible监听。4.9 坑9多相机系统如分屏targetCamera引用错乱现象主相机计算结果被副相机覆盖。解决方案用Camera.onPreCull事件绑定确保每帧只计算激活相机。4.10 坑10移动设备上Screen.width/height在横竖屏切换时未刷新现象手机旋转后CalculateFrustumCorners用的还是旧分辨率。解决方案监听Screen.orientation变化或直接用Camera.pixelWidth/Height。4.11 坑11目标平面是SkinnedMeshRendererbounds不准确现象角色模型的地面检测交点总在脚底偏移。解决方案用SkinnedMeshRenderer.BakeMesh()获取当前姿态的Mesh再计算。4.12 坑12性能瓶颈出现在每帧调用但需求其实只需关键帧计算现象CPU Profiler显示CalculateFrustumCorners占帧时间5ms。解决方案加缓存和脏标记仅当相机或平面Transform变化时重新计算。private Vector3 lastCameraPos; private Quaternion lastCameraRot; private Vector3 lastPlanePos; private Quaternion lastPlaneRot; void CheckIfDirty() { bool dirty false; dirty | !lastCameraPos.AlmostEquals(targetCamera.transform.position, 0.01f); dirty | !lastCameraRot.AlmostEquals(targetCamera.transform.rotation, 0.01f); dirty | !lastPlanePos.AlmostEquals(targetPlaneTransform.position, 0.01f); dirty | !lastPlaneRot.AlmostEquals(targetPlaneTransform.rotation, 0.01f); if (dirty) { RecalculateAll(); lastCameraPos targetCamera.transform.position; lastCameraRot targetCamera.transform.rotation; // ... 更新其他 } }5. 扩展应用场景与进阶技巧不止于四个角点算出四个角点只是起点。在真实项目中这些坐标会衍生出更多高价值应用。分享几个我亲自落地的案例。5.1 应用1动态UV映射——把“可视范围”实时投射到纹理在某数字孪生项目中客户要求“镜头扫过工厂时地图上实时高亮当前视野覆盖区域”。我们没用UGUI遮罩而是把四个角点传给Shader用UnityObjectToClipPos在顶点着色器中计算UV。// Shader中接收四个点作为Vector4数组 float4 _VisibleCorners[4]; // x,y,z,w x,z,0,1 (投影到XZ平面) v2f vert(appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); // 计算v.vertex在四边形内的重心坐标 float3 bary BarycentricCoord(v.vertex.xz, _VisibleCorners); o.uv bary.x * _CornerUVs[0] bary.y * _CornerUVs[1] bary.z * _CornerUVs[2]; return o; }关键是BarycentricCoord函数它把世界坐标映射到四边形UV。这比用ScreenPos做采样稳定十倍且支持任意透视变形。5.2 应用2物理碰撞优化——只对“可视区域”内的物体做射线检测在开放世界游戏中每帧对所有敌人做Physics.Raycast太贵。我们用可视范围四边形构建一个PolygonCollider2D在3D中用MeshCollider只对Collider内的物体做检测。// ✅ 运行时生成Collider PolygonCollider2D pc targetAreaGO.AddComponentPolygonCollider2D(); pc.points Array.ConvertAll(visibleCorners, p new Vector2(p.x, p.z)); // 投影到2D实测在200个敌人场景中射线检测耗时从8ms降到0.3ms。5.3 应用3AR锚点自动放置——在“稳定平面”上放置虚拟物体AR Foundation中ARRaycastManager返回的RaycastHit可能有多个怎么选最“可靠”的我们用可视范围四边形与每个Hit点求距离选四边形中心最近的那个。float minDist float.MaxValue; Vector3 bestPoint Vector3.zero; Vector3 quadCenter (visibleCorners[0] visibleCorners[1] visibleCorners[2] visibleCorners[3]) / 4f; foreach (var hit in hits) { float dist Vector3.Distance(hit.point, quadCenter); if (dist minDist) { minDist dist; bestPoint hit.point; } }这比单纯选第一个Hit点稳定性提升40%尤其在纹理稀疏的墙面上。5.4 进阶技巧支持透视校正的“四点扭曲”UI有时你需要一个UI Quad它四个角分别吸附到四个世界坐标点上比如AR中把视频流贴到墙上。Unity的RectTransform不支持但可以用MeshFilterMeshRenderer手动构建。// ✅ 构建四边形Mesh Mesh mesh new Mesh(); Vector3[] vertices visibleCorners; // 四个世界坐标点 int[] triangles {0,1,2, 2,3,0}; // 两个三角面 mesh.vertices vertices; mesh.triangles triangles; mesh.RecalculateNormals();关键是要把Mesh的transform设为世界坐标且禁用Lighting否则会有阴影穿帮。5.5 进阶技巧实时计算“视野覆盖率百分比”客户常问“当前镜头覆盖了目标区域的百分之几”这需要计算两个多边形的交集面积。我们用ClipperLib库C#版Clipper做布尔运算。// ✅ 多边形裁剪 var subj new ListIntPoint(Array.ConvertAll(visibleCorners, p new IntPoint((long)(p.x*100), (long)(p.z*100)))); var clip new ListIntPoint(Array.ConvertAll(targetAreaCorners, p new IntPoint((long)(p.x*100), (long)(p.z*100)))); var solution new ListListIntPoint(); Clipper c new Clipper(); c.AddPath(subj, PolyType.ptSubject, true); c.AddPath(clip, PolyType.ptClip, true); c.Execute(ClipType.ctIntersection, solution, PolyFillType.pftEvenOdd, PolyFillType.pftEvenOdd); float coverage AreaOf(solution) / AreaOf(clip);精度控制在1cm内客户验收时非常满意。我个人在实际使用中发现这套方法最强大的地方在于它的“可组合性”。四个角点不是终点而是空间关系的种子。你可以用它们驱动动画、触发事件、生成几何体、甚至训练AI模型——只要你的项目需要“理解相机看到了什么”这个计算就是一切的起点。它不炫技不浮夸但像空气一样不可或缺。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑