要把 LingBot-Depth 2.0 的深度数据接进 Unity,核心不是把 SDK 跑起来,而是把该 SDK 的深度缓冲转换成 Unity 的 Texture 和 Shader 能理解的格式。下面按初始化、Texture 转换、Shader 消费三步走,给出可落地的集成骨架。无论 SDK 通过回调、轮询还是共享内存输出数据,最终都要落到这两个问题上:数据何时到达、数据以什么格式排布。
集成 LingBot-Depth 2.0 时,建议先确认 SDK 以什么回调方式送出深度数组(如 ushort[] 或 IntPtr),再决定 Texture 格式和坐标翻转方式。深度图在 Unity 中通常用 Texture2D 配合 R16 或 RGBA32 存储,Shader 中通过自定义纹理采样。先做单相机深度显示,再谈配准和多相机融合。
初始化与数据回调
在 Unity 中初始化深度 SDK,通常放在 MonoBehaviour 的 Awake 或 Start 里。优先确认三件事:SDK 是否要求特定线程、是否自动开启深度流、回调数据是同步返回还是事件驱动。以常见的回调式 SDK 为例,先拿到设备句柄,再注册深度帧回调。注意回调线程不是主线程,不能在回调里直接操作 Unity 对象,需要把数据拷贝到临时缓冲区,再用主线程的 Update 或 Coroutine 更新 Texture。
public class DepthSource : MonoBehaviour
{
IntPtr device = IntPtr.Zero;
ushort[] depthBuffer;
Texture2D depthTexture;
bool hasNewFrame;
void Start()
{
// 替换为实际 SDK 初始化入口
device = LingBotDepth.Init(0);
LingBotDepth.RegisterDepthFrame(device, OnDepthFrame);
depthBuffer = new ushort[640 * 480]; // 按 SDK 实际分辨率改
depthTexture = new Texture2D(640, 480, TextureFormat.R16, false);
}
void OnDepthFrame(IntPtr data, int width, int height)
{
// 回调节点上拷贝数据,不直接 new 或上传 GPU
System.Runtime.InteropServices.Marshal.Copy(data, depthBuffer, 0, depthBuffer.Length);
hasNewFrame = true;
}
void Update()
{
if (!hasNewFrame) return;
hasNewFrame = false;
depthTexture.LoadRawTextureData(depthBuffer);
depthTexture.Apply();
}
}这段代码假设 SDK 返回的是连续 ushort 数组,且分辨率固定。如果深度图尺寸会动态变化,需要在回调里判断 width、height,并在主线程重新创建 Texture2D。注意 LoadRawTextureData 要求 byte 数组长度与 Texture 格式匹配,R16 正好是 2 字节/像素。
深度值到 Texture 的编码与上传
深度值通常是毫米单位的 ushort,直接转 R16 比较自然。但 R16 在部分移动平台或某些渲染管线下不受支持,这时可以把高低字节拆进 RGBA32 的 R 和 G 通道,Shader 里再合并。下面是两种编码方式的选择。如果只是调试,可以用 Color32 数组手动填充,但性能不如 LoadRawTextureData。
void Update()
{
// 方式一:R16(推荐)
depthTexture.LoadRawTextureData(depthBuffer);
depthTexture.Apply();
// 方式二:RGBA32 兜底
// Color32[] pixels = new Color32[depthBuffer.Length];
// for (int i = 0; i < depthBuffer.Length; i++)
// {
// pixels[i] = new Color32(
// (byte)(depthBuffer[i] & 0xFF),
// (byte)(depthBuffer[i] >> 8),
// 0,
// 255);
// }
// depthTexture.SetPixels32(pixels);
// depthTexture.Apply();
}R16 方案的优点是 Shader 里直接取 .r 作为归一化深度,RGBA32 方案则在 Shader 中需要 decode:depth = (r + g*256)/65535。两种方案都要注意平台字节序,但实际深度值在内存中就是小端,所以上面的拆法是对的。
在 Shader 中消费深度信息
深度纹理传入 Shader 后,需要按实际用途处理。下面是一个调试用 Shader,把深度映射成红绿渐变,方便直接看到大概距离。对深度感知类应用,采样后最好转换为线性距离再做后续计算。
Shader "Unlit/DepthViewer"
{
Properties
{
_DepthTex ("Depth Texture", 2D) = "white" {}
_Near ("Near Distance (m)", Float) = 0.1
_Far ("Far Distance (m)", Float) = 5.0
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _DepthTex;
float _Near;
float _Far;
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; };
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 如果使用 R16,深度值已经被归一化到 0-1
float d = tex2D(_DepthTex, i.uv).r;
// 映射到实际距离范围,便于显示或参与逻辑
float dist = lerp(_Near, _Far, d);
float t = saturate((dist - _Near) / (_Far - _Near));
return fixed4(t, 1.0 - t, 0.0, 1.0);
}
ENDCG
}
}
}如果使用 RGBA32 编码,需要把上面 frag 第一行改成:float raw = tex2D(_DepthTex, i.uv).r + tex2D(_DepthTex, i.uv).g * 256.0; float d = raw / 65535.0; 具体写在哪里,取决于你的图像到底是 raw 还是已经归一化成了 0-1。先跑通调试 Shader,再用 OnRenderImage 或屏幕后处理进一步验证。
验证与常见问题
完成基础通路后,建议做三样检查:一、在屏幕上显示 depthTexture,看深度值是否随物体远近单调变化;二、移动物体,确认深度纹理刷新率符合预期;三、用 Debug.Log 打印深度数组的最大最小值,确认没有溢出或全零。
- 深度图全黑或全白: 通常不是没数据,而是深度范围没有映射到 0-1。先输出原始值范围,再选择归一化参数。
- 深度图和 RGB 图错位: 先确认两路图像的分辨率、FOV 是否一致,再检查 Y 轴是否需要翻转。如果错位是整体平移,可能来自传感器硬件配准,这不在本文集成范围内。
另外注意,某些 SDK 在设备挂起或插拔后不会自动恢复,需要在 OnApplicationPause 或 OnDisable 里做反初始化。集成时不要把深度数据直接和 MainCamera 的 depthTexture 混淆,那是引擎自己渲染的深度,不是传感器深度。
常见问题
深度数据回调频率不稳定怎么办? 先检查设备是否工作在固定帧率模式,或者是否因为 USB 带宽被色彩流挤占。可以试着降低分辨率或关闭 RGB 流,单独跑深度流。
在移动端 R16 不可用? 改用 RGBA32 编码,或者用 ComputeShader 把 ushort 转成四个 byte。也可以用 GraphicsFormat.R16_UInt,但需要确认目标平台支持情况。