AREngine远程同步调试
一、概述
由于目前开发的AR内容, 只能在手机上运行, 看不到代码的运行过程。 为了将运行过程中产生的中间数据传送到PC端,比如说预览流, 这里需要实现的一套信息传送机制, 能实现实时将数据从手机传递到PC。 这次实现基于Android端Java代码, PC端c#代码进行单向传递, 并最终在3D编辑器Unity实时显示的框架。
二. adb forward
adb forward的功能是建立一个转发,adb forward tcp:11111 tcp:22222的意思是,将PC端的11111端口收到的数据,转发给到手机中22222端口。但是光执行这个命令还不能转发数据,还需要完成两个步骤才能传数据。这两个步骤是:
(a)在手机端,建立一个端口为22222的server,并打开server到监听状态。
(b)在PC端,建立一个socket client端,连接到端口为11111的server上。
通过运行adb forward –list查看刚才的执行结果
adb forward --list
可以通过adb forward –remove tcp:11111删除建立的转发
adb forward --remove tcp:11111
在PC端,adb forward创建了一个监听本机11111端口的server。通过adb 转发的数据,需要先发到11111端口。这个11111端口是约定好的,你也可以改成其他端口。PC端的应用通过socket连接到11111端口,以准备发送数据。但是连接到11111端口之前,还需要在手机端启动端口为22222的server。
在PC端的应用开始连接之前,手机端要启动端口为22222的server(socket server)。手机中adb的daemon进程将连接到22222端口,这样PC端应用就可以连接PC端的11111端口了,连接上之后就可以从PC端的应用发送数据给手机端的应用,手机端的应用也可以发送数据给PC端的应用。
PC端的应用与手机端应用通信建立的过程:
(1)执行adb forward tcp:11111 tcp:22222
(2)启动手机端应用,建立端口为22222的server,并处于监听状态(LISTENING)
(3)启动PC端应用,连接端口为11111的server(adb创建的)
之后,就可以传输数据了。
PC端的应用与手机端应用之间传输数据的过程:
(1)PC端应用将数据发送给端口为11111的server(adb创建的)
(2)adb将数据转发给手机端adbd进程(通过USB传输)
(3)adb进程将数据发送给端口为22222的server(手机端应用创建的)
传递是双向的,第(1)和第(3)步是通过socket实现的,所以通过socket的读和写就完成了PC端应用和手机端应用的数据传递。
UDP 支持
adb forward不能转发UDP端口信息,只能是TCP…. adb forward tcp:6100 tcp:7100. 这样就将宿主机的6100端口映射到模拟器的7100端口上(将tcp改成udp,会一直提示绑定失败),也正因为如此我发现了转发端口的基本命令redir。
redir add < udp/tcp >:< pc端口 >:< 模拟器端口 >
如:
redir add udp:1096:1097
redir tcp:1096:1097
作用就是将PC的1096端口转发到android设备的1097端口,当然两个端口号可以相同,因为他们是在两个不同的设备上。但是有个缺点,就是不如adb forward灵活。
例如:redir add udp:5000:6000 这样所有在开发机上5000端口的udp通信都会被重定向到模拟器的6000端口上。
添加成功后,我们可以用redir list命令来列出已经添加的映射端口,redir del可以进行删除。
redir list
实践
基于adb forward tcp 建立的的端口转发机制, 作者实现了一个Demo, 利用c#的Socket的TCP,手机端进行监听, pc端直接进行连接。 代码已上传到github, 具体的实现就是在多线程里开启一个socket/TCP, 分别在手机上和pc上运行, 端口配置adb forward 转发的端口就。效果如下图:
如下面是pc侧运行代码:
sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
sock.NoDelay = true;
var ipaddress = IPAddress.Parse(ip);
var endpoint = new IPEndPoint(ipaddress, port);
sock.Connect(endpoint);
callback(" connect server success", TcpState.Connect);
thread = new Thread(Receive);
thread.IsBackground = true;
thread.Start();
手机侧的socket负责监听来自pc侧的连接请求, 相当与运行一个Server。 这里实现用到了两个线程,一个负责监听新的连接, 一个负责接收消息。 由于我们这里只是用来电脑和Android侧进行调试来用, 所以socket的监听队列长度设置为1.
public void BuildServer(string ip, int port)
{
socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
ProtocolType.Tcp);
var ipaddress = IPAddress.Parse(ip);
var endpoint = new IPEndPoint(ipaddress, port);
socketWatch.Bind(endpoint);
//将套接字的监听队列长度限制为1
socketWatch.Listen(1);
callback(" begin listening", TcpState.Connect);
thread = new Thread(WatchConnecting);
thread.IsBackground = true;
thread.Start();
}
private void WatchConnecting()
{
while (true)
try
{
sock = socketWatch.Accept();
callback(" connect:" + sock.RemoteEndPoint, TcpState.Connect);
state = TcpState.Connect;
rcvThread = new Thread(Receive);
rcvThread.IsBackground = true;
rcvThread.Start(sock);
threadRun = true;
}
catch (Exception ex)
{
Debug.LogError("connect error:" + ex);
}
}
除此之后, 我们还可以把基于华为 AREngine实现的预览流投射到Editor中。
当然要想实现上面的效果, 需要对 AREnegine的代码做少许的修改, 比如说手机产生的YUV数据传送到Editor中,将对应的数据转换成生成单通道的 Y-Texture 和 UV-Texture。 然后在Shader中分别采样yuv数据,最后在每个像素中转换成RGB输出到FrameBuffer。
需要注意的是 AREngine 里的像素并不是RGB 排列的, 也不是等距的yuv图像, 而是 YUV-420-888 格式的图像,这意味着 4个y通道分别对应着一个u通道和v通道。 转换成二维的图像, y 通道的图片分辨率即 uv 图片的分辨率的一半。
如下图, 在 huawei meta30 pro 预览流中使用的 640X480 的 y 通道输出到一张 只有 R 通道的 RenderTexture上
下图是 只有 一半分辨率的 uv 通道(输出到RT的rg两个通道)中 u通道显示:
从Y通道图片中可以看到图片是翻转的, 这里在shader中需要对 y—texture 采样的时候, 需要做这样翻转:
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex).yx;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float2 uv2 = float2(1, 1) - i.uv;
fixed4 ycol = tex2D(_YTex, uv2);
fixed4 uvcol = tex2D(_UVTex, uv2);
//...
}
上面是后置镜头的yuv图像,如果是前置camera传递的图像 又发生了变化, uv需要做如下翻转:
fixed4 frag(v2f i) : SV_Target
{
float2 uv2 = float2(i.uv.x, 1 - i.uv.y);
fixed4 ycol = tex2D(_YTex, uv2);
fixed4 uvcol = tex2D(_UVTex, uv2);
//...
}
综上, 我们shader里使用一个uniform 变量, 通过c#侧传递过的值统一动态的采样图像:
float4 uv_st;
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex).yx;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float2 uv2 = uv_st.xy + uv_st.zw * i.uv;
fixed4 ycol = tex2D(_YTex, uv2);
fixed4 uvcol = tex2D(_UVTex, uv2);
//...
}
c# 这样传值:
readonly Vector4 front_uv = new Vector4(0, 1, 1, -1);
readonly Vector4 back_uv = new Vector4(1, 1, -1, -1);
static readonly int UVSt = Shader.PropertyToID("uv_st");
protected override void OnInitial()
{
var uv_st = sceneState == SceneState.Face ?
front_uv :
back_uv;
material.SetVector(UVSt, uv_st);
}
除了预览流, 使用类似的方法, 还可以将SLAM驱动的相机姿态、点云信息传送到Editor, 可以看到更多的调试信息。
如上图, 这里将预览流、camera姿态、 点云位置、 平面检测信息动态传到Editor.
这里将AR的手势识别信息传递到Editor。
下图是Editor下重建人脸示例:
场景冲击那示例:
基于此原理, 那也可以同样做到把 平面信息、场景Mesh、 人脸识别、 骨骼信息、光照估计等相关的算法同步显示在Editor中。 如果你的框架写的好,除了AR的中间变量进行可视化输出,也可以把其他的事件产生的数据都可以在Editor表出来, 这样手机运行的结果和Editor运行的结果是完全一致的。 这里主要是展示原理, 具体的实现就不公开了。