一、概述

由于目前开发的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运行的结果是完全一致的。 这里主要是展示原理, 具体的实现就不公开了。