系统采用 Unity(C#)与 Orbbec/Astra 深度相机,包含深度→网格、标定/投影、ROI 联动、Shader 参数中台、生态分布与交互、稳定性工程化等完整链路。
开发需求文档与思维导图:Sandbox: https://www.mubu.com/doc/EP5Q5NheCX
产品相关展示:https://github.com/xiaoyunhai0/ARSandBox
0) 运行环境
- 引擎:Unity(建议 2021+,内置或 URP 均可)
- 硬件:Orbbec/Astra(深度 + 彩色)
- UI/依赖:UGUI + TextMeshPro;参数持久化使用
PlayerPrefs
- 部署:Windows x64,双显示器(主屏控制 + 副屏投影)
1) 项目目标与成果
- 将 Astra 深度数据实时重建为 Unity 网格(Mesh),实现物理等比与投影对齐。
- 通过 ROI 双窗口联动(Mesh/Color)完成区域标定与像素级裁剪。
- 构建 Shader 参数中台,统一水陆阈值、等高线、主题/天气;生态/显示共享同一“世界解释”。
- 提供 副屏投影模板(正交相机 + 动态 RenderTexture 自适应)与一站式标定接口。
- 叠加 轻量生态系统(鱼/鸟/走兽/鲨/宝箱 + 捕鱼模式),表现可控、耦合低。
- 关键参数全链路持久化,按 SOP 快速布展与复现。
2) 系统结构
1 2 3 4 5 6 7 8 9
| Astra 深度/彩色帧 └─ AstraMeshController(深度→网格;等比/拉平/矫正/落盘) ├─ ProjectionCalibrationController(Mesh/投影相机 一站式标定) ├─ ROIManager + ROICornerDragger(ROI 双窗口角点联动) ├─ AstraDisplayUI(彩色裁剪、滤波切换、UI 联动) ├─ DynamicRT(RawImage ↔ RenderTexture 自适应) ├─ MaterialParamManager + SandboxSettingsUI(Shader 参数中台) ├─ WeatherManager(主题/天气与光照/雾效联动) └─ ModelSpawnerManager + Fish/Bird/Beast/Shark/Treasure/Fishing(生态)
|
3) 目录(核心脚本)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| AstraMeshController.cs ProjectionCalibrationController.cs DynamicRT.cs ROIManager.cs ROICornerDragger.cs AstraDisplayUI.cs MaterialParamManager.cs SandboxSettingsUI.cs WeatherManager.cs ModelSpawnerManager.cs FishController.cs BirdController.cs BeastController.cs SharkSpawner.cs SharkController.cs SharkSettingsUI.cs TreasureChestController.cs FishingManager.cs
|
4) 关键模块与源码
4.1 深度→网格与物理对齐(AstraMeshController.cs)
职责:深度帧→三角网/UV/顶点;1:1 等比;围栏拉平;全局偏移/旋转/基准抬升;参数持久化。
要点:xyScale/zScale + useRealWorldSize/realWorldSize;边界采样 + 平面拟合;PlayerPrefs 落盘。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #region Mesh Params Save/Load(网格参数落盘/恢复) public void SaveMeshParamsToPrefs() { PlayerPrefs.SetFloat("Mesh_HorizontalOffset", positionOffset.x); PlayerPrefs.SetFloat("Mesh_VerticalOffset", positionOffset.z); PlayerPrefs.SetFloat("Mesh_BaseOffset", baseOffset); PlayerPrefs.SetFloat("Mesh_RotationY", rotationOffset.y); PlayerPrefs.Save(); } public void LoadMeshParamsFromPrefs() { positionOffset.x = PlayerPrefs.GetFloat("Mesh_HorizontalOffset", 0f); positionOffset.z = PlayerPrefs.GetFloat("Mesh_VerticalOffset", 0f); baseOffset = PlayerPrefs.GetFloat("Mesh_BaseOffset", 0f); rotationOffset.y = PlayerPrefs.GetFloat("Mesh_RotationY", 0f); } #endregion
#region Fence Level(围栏拉平:边界采样 + 平面拟合) [ContextMenu("Fence Level Now")] public void FenceLevelCorrectionRequest() { fenceLevelCorrectionRequested = false; UpdateMeshVertices(); } private void FenceLevelCorrect(Vector3[] verts) { var borderPoints = new List<Vector3>(); Plane plane = FitPlaneToPoints(borderPoints); Quaternion deltaRot = Quaternion.FromToRotation(transform.up, plane.normal); transform.rotation = deltaRot * transform.rotation; transform.position += new Vector3(0f, baseOffset, 0f); } #endregion
#region Public Access(对外接口) public Vector3[] GetMeshVertices() => originalVertices; public void SetRotationOffsetY(float ry) => rotationOffset.y = ry; public float GetRotationOffsetY() => rotationOffset.y; #endregion
|
4.2 投影与多显示模板(ProjectionCalibrationController.cs、DynamicRT.cs)
职责:集中暴露 Mesh/相机几何参数;正交相机副屏输出;RawImage→RT 自适应,仅在尺寸变化时重建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #region One-Stop Calibration(标定入口) public void SetMeshScaleX(float v) => meshCtrl.realWorldSize.x = v; public void SetMeshScaleZ(float v) => meshCtrl.realWorldSize.y = v; public void SetMeshBaseOffset(float v) => meshCtrl.SetBaseOffset(v); public void SetMeshRotY(float v) => meshCtrl.SetRotationOffsetY(v);
public void SetCameraX(float v){ var p=projectorCamera.transform.position; p.x=v; projectorCamera.transform.position=p; } public void SetCameraY(float v){ var p=projectorCamera.transform.position; p.y=v; projectorCamera.transform.position=p; } public void SetCameraZ(float v){ var p=projectorCamera.transform.position; p.z=v; projectorCamera.transform.position=p; } public void SetCameraOrthoSize(float v) => projectorCamera.orthographicSize = v;
public void MeshFenceLevel() => meshCtrl.FenceLevelCorrectionRequest(); #endregion
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #region DynamicRT(按需重建 RT 并同步相机宽高比) void Update() => UpdateRenderTexture(); void UpdateRenderTexture() { var rect = rawImage.rectTransform.rect; int w = Mathf.Max(1, Mathf.RoundToInt(rect.width)); int h = Mathf.Max(1, Mathf.RoundToInt(rect.height)); if (rt == null || rt.width != w || rt.height != h) { if (rt != null) rt.Release(); rt = new RenderTexture(w, h, 16); renderCam.targetTexture = rt; rawImage.texture = rt; renderCam.aspect = (float)w / h; } } #endregion
|
4.3 ROI 双窗口联动与彩色裁剪(ROIManager.cs、ROICornerDragger.cs、AstraDisplayUI.cs)
职责:Mesh/Color 两窗口角点拖拽双向同步;彩色帧像素级裁剪;角点/ROI 落盘恢复;局部相机随 ROI 自动对齐。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #region ROI Save/Load(角点落盘与延迟恢复) public void SaveROIToPrefs() { for (int i = 0; i < 4; i++) { var pos = roiCornersMesh[i].anchoredPosition; PlayerPrefs.SetFloat($"ROI_Corner_{i}_X", pos.x); PlayerPrefs.SetFloat($"ROI_Corner_{i}_Y", pos.y); } PlayerPrefs.Save(); } IEnumerator DelayLoadROI() { bool wasActive = calibrationPanel.activeSelf; calibrationPanel.SetActive(true); yield return null; LoadROIFromPrefs(); UpdateAll(); calibrationPanel.SetActive(wasActive); } #endregion
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #region Corner Drag(角点拖拽与双向同步) public void OnBeginDrag(PointerEventData e) { RectTransformUtility.ScreenPointToLocalPointInRectangle( parentRawImageRect, e.position, e.pressEventCamera, out startLocal); startOffset = (Vector2)transform.localPosition - startLocal; } public void OnDrag(PointerEventData e) { RectTransformUtility.ScreenPointToLocalPointInRectangle( parentRawImageRect, e.position, e.pressEventCamera, out Vector2 local); Vector2 newPos = local + startOffset; var r = parentRawImageRect.rect; newPos.x = Mathf.Clamp(newPos.x, r.xMin, r.xMax); newPos.y = Mathf.Clamp(newPos.y, r.yMin, r.yMax); ((RectTransform)transform).anchoredPosition = newPos; roiManager.OnCornerMoved(cornerIndex, belongsToMeshWindow: true); } #endregion
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #region ROI Crop(像素级裁剪输出) private Texture2D _roiTex; void CropAndShowROI(Texture2D src, RectInt roi) { roi.x = Mathf.Clamp(roi.x, 0, Mathf.Max(0, src.width - 1)); roi.y = Mathf.Clamp(roi.y, 0, Mathf.Max(0, src.height - 1)); roi.width = Mathf.Clamp(roi.width, 1, src.width - roi.x); roi.height = Mathf.Clamp(roi.height, 1, src.height - roi.y); var pixels = src.GetPixels(roi.x, roi.y, roi.width, roi.height); if (_roiTex == null || _roiTex.width != roi.width || _roiTex.height != roi.height) _roiTex = new Texture2D(roi.width, roi.height, TextureFormat.RGBA32, false); _roiTex.SetPixels(pixels); _roiTex.Apply(false, false); roiPreviewRawImage.texture = _roiTex; } #endregion
#region UI Shortcut(滤波 σ 快切) void Update() { if (Input.GetKeyDown(KeyCode.Alpha5)) ToggleGaussianSigma(); } #endregion
|
4.4 Shader 参数中台与分层阈值(MaterialParamManager.cs、SandboxSettingsUI.cs)
职责:统一封装地形/火山/雪/等高线;语义化接口;UI 双向绑定与边界有序约束;阈值驱动生态与显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #region Terrain Params(地形分层/等高线) public void SetTerrainBorder0(float v) => SetFloat("terrain", "_Border0", v); public void SetTerrainBorder1(float v) => SetFloat("terrain", "_Border1", v); public void SetTerrainBorder2(float v) => SetFloat("terrain", "_Border2", v); public void SetTerrainBlendRange(float v) => SetFloat("terrain", "_BlendRange", v); public void SetContourSpacing(float v) => SetFloat("terrain", "_ContourSpacing", v); public void SetContourSmoothness(float v)=> SetFloat("terrain", "_ContourSmoothness", v); public void SetContourColor(Color c) => SetColor("terrain", "_ContourColor", c);
private readonly Dictionary<string, Material> matDict = new(); private void SetFloat(string name, string prop, float value) { if (matDict.TryGetValue(name, out var m) && m.HasProperty(prop)) m.SetFloat(prop, value); } private void SetColor(string name, string prop, Color value) { if (matDict.TryGetValue(name, out var m) && m.HasProperty(prop)) m.SetColor(prop, value); } #endregion
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #region UI Binding(滑条/输入框双向绑定 + 边界有序约束) void InitSliderAndInput(Slider s, TMP_InputField i, float min, float max, float def, Action<float> onChanged) { s.minValue = min; s.maxValue = max; s.SetValueWithoutNotify(def); i.text = def.ToString("F2"); s.onValueChanged.AddListener(v => { i.SetTextWithoutNotify(v.ToString("F2")); onChanged(v); Save(); }); i.onEndEdit.AddListener(t => { if (float.TryParse(t, out var v)) { v = Mathf.Clamp(v, min, max); s.SetValueWithoutNotify(v); onChanged(v); Save(); } }); } void EnforceBordersOrder() { float b0 = GetSlider(terrainBorder0Slider); float b1 = Mathf.Max(b0 + 0.01f, GetSlider(terrainBorder1Slider)); float b2 = Mathf.Max(b1 + 0.01f, GetSlider(terrainBorder2Slider)); terrainBorder1Slider.SetValueWithoutNotify(b1); terrainBorder2Slider.SetValueWithoutNotify(b2); } #endregion
|
4.5 生态分布与行为(ModelSpawnerManager.cs、SharkSpawner.cs、SharkController.cs 等)
职责:按阈值与网格生成 water/ground/fly 点集;行为巡航带最小位移/最大距离与越界预测;UI/快捷键批量控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #region Points Generation(水/地/空点集) public void RefreshAllModelPoints() { if (!IsMeshReady()) return; var mat = matMgr.terrainMaterial; float b0 = mat.GetFloat("_Border0"); float b2 = mat.GetFloat("_Border2"); float blend = mat.GetFloat("_BlendRange"); float waterThreshold = b2 - blend; var verts = meshCtrl.GetMeshVertices(); waterPoints = verts.Where(v => v.y >= waterThreshold).ToList(); groundPoints = verts.Where(v => v.y > b0 && v.y < b2).ToList(); flyPoints = verts.ToList(); } #endregion
|
1 2 3 4 5 6 7 8 9 10 11
| #region Shark Sync(下发水面高度与点集) void Update() { float b2 = matParamMgr.terrainMaterial.GetFloat("_Border2"); float blend = matParamMgr.terrainMaterial.GetFloat("_BlendRange"); SharkController.GlobalWaterBorder = b2 - blend; foreach (var s in sharks) s.GetComponent<SharkController>().waterPoints = waterPoints; } #endregion
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #region Shark Target Pick(目标选择:去抖/限距/越界预测) void PickNewTarget(bool isFirst = false) { if (waterPoints == null || waterPoints.Count == 0) { targetPos = transform.localPosition; return; } Vector3 cur = transform.localPosition; float maxSqr = maxTargetDistance * maxTargetDistance; float minSqr = minMoveDist * minMoveDist; var cand = new List<Vector3>(); foreach (var wp in waterPoints) { float d2 = (new Vector3(wp.x, GlobalWaterBorder, wp.z) - cur).sqrMagnitude; if (d2 > minSqr && d2 < maxSqr) cand.Add(wp); } var pick = cand.Count > 0 ? cand[Random.Range(0, cand.Count)] : waterPoints[Random.Range(0, waterPoints.Count)]; targetPos = new Vector3(pick.x, GlobalWaterBorder, pick.z); } #endregion
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #region Chest Appear/Hide(阈值触发显隐) public void ShowChest() { if (isAppeared) return; isAppeared = true; var pos = transform.position; pos.y = chestShowYOffset; transform.position = pos; SetChildrenActive(true); if (chestAnim) chestAnim.SetTrigger("Appear"); Invoke(nameof(HideChest), 5f); } #endregion
|
4.6 主题/天气与地图切换(WeatherManager.cs)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #region Weather/Map(天气/地图) public void DoClear() => SetWeather(WeatherType.Clear); public void DoRain() => SetWeather(WeatherType.Rain); public void DoSnow() => SetWeather(WeatherType.Snow); public void ShowMap0() => SwitchMap(0); public void ShowMap1() => SwitchMap(1);
public void SetWeather(WeatherType t) { } #endregion
|
5) 标定与演示 SOP
- 分层优先:在设置面板调
_Border0/_Border1/_Border2/_BlendRange,先稳定“水面阈值/地表层级”。
- ROI 校准:Mesh/Color 两窗口四角拖拽(双向同步),自动落盘;彩色帧按 ROI 像素级裁剪。
- 姿态/尺寸:用一站式接口调整 Mesh 偏移/旋转/等比尺寸与投影相机
OrthoSize;围栏歪斜用“一键拉平”。
- 主题/天气:切换 Clear/Rain/Snow/Volcano 与 Map0/Map1(光照/雾效联动)。
- 生态/互动:
F1/F2/F3 刷鱼/鸟/走兽,F5 刷新全部,F6 清空;F8 捕鱼模式;F7 调试显宝箱。
- 持久化:Mesh/ROI/Shader/UI 参数统一
PlayerPrefs 落盘,二次启动即用。
6) 快捷键速查
| 功能 |
快捷键 |
| 滤波 σ 快切 |
5 |
| 鱼 / 鸟 / 走兽 刷新 |
F1 / F2 / F3 |
| 全部刷新 / 清空 |
F5 / F6 |
| 鲨鱼 刷新 / 清空 |
3 / 4 |
| 捕鱼模式 |
F8 |
| 天气:雨 / 雪 / 晴 / 火山 |
R / S / C / V |
| 地图切换 |
1 / 2 |
| 宝箱显现(调试) |
F7 |
7) 稳定性与工程化
- 按需分配:RT/纹理尺寸变更才重建;实例池按目标数增删,减少
Instantiate/Destroy。
- 越界与抖动:最小位移/最大距离 + 下一帧越界预测,不合法即重选。
- UI 约束:
SetValueWithoutNotify 防事件回环;EnforceBordersOrder 保证边界有序。
- 日志可观测:聚焦刷新/清空/强制动作;发布构建统一降级日志级别。
- 异常防护:空引用/尺寸越界/ROI 出界均有 Clamp 与判空,避免运行期崩溃。
8) 独立负责与落地产出(Solo Ownership)
- 深度→网格:实时重建、等比标定、围栏拉平、全局矫正、参数持久化。
- 投影/多显示:正交相机模板、动态 RT 自适应、一站式标定接口。
- ROI 系统:双窗口角点联动、像素级裁剪、延迟恢复与落盘。
- Shader 中台:分层阈值与等高线/火山/雪等主题的语义化接口。
- 生态/互动:阈值驱动的水/地/空区域分布与行为(鱼/鸟/走兽/鲨/宝箱/捕鱼)。
- SOP 与文档:现场标定流程、快捷键体系、排障要点。
9) 常见问题(快速排查)
- ROI 初次进入错位:
DelayLoadROI() 先激活校准面板并等一帧再恢复角点;必要时调用 Canvas.ForceUpdateCanvases()。
- 投影画面拉伸/糊:确认
DynamicRT 正在按 RawImage 像素尺寸重建 RT,并同步 renderCam.aspect。
- 分层边界“打架”:UI 以
EnforceBordersOrder() 保证 Border0 < Border1 < Border2,回写材质前 Clamp。
- 鱼/鲨“卡位”或越界:增大
minMoveDist、收紧 maxTargetDistance,确保越界预测生效。
- 二次启动丢参数:检查
PlayerPrefs.Save() 是否调用、键名是否一致;必要时清理后重新保存。