ARCore Augmented Image+marker传送门

2022-08-04,,,

想要的效果就是将marker变成一个窗口,通过这个窗口可以看见里面的虚拟内容

做的东西有点类似于传送门,但与网上常见的AR传送门又有一点不同,网上常见的AR传送门其中“门”的位置通常是任意的,b站上有一个老哥的视频,可以看看AR传送门的效果。

【附源码】Unity+ARcore 实现传送门 小意思VR AR教程

 

我想做的这个“门”是和marker同样大小的,有点像官方的一个演示视频(官方的这个示例里从marker到虚拟物体的过渡做的很流畅)

还有之前看到的一个国外网友做的东西。

效果大概就是这个样子,最终做出的demo:

识别marker

ARCore组件中的Example下有一个名为增强图像(Augmented Image)的示例,展示了ARCore如何识别marker并在上面显示模型。初学记录一下做的过程和理解。

Augmented Image示例

代码主要有两部分AugmentedImageExampleController和AugmentedImageVisualizer。AugmentedImageVisualizer部分是ARCore检测到marker图像之后,负责在其上显示模型的组件。就像HelloAR中显示模型一样。

能够发现在AugmentedVisualizer脚本上留有左下角、右下角、左上角和右上角这四个部分的模型等待绑定。

Prefab目录下的AugmentedImageVisualizer上绑定了同名的script,并为其绑定了对应的四个模型。 在prefab的Inspector中我们看到的如下。

prefab与实际使用时看到的不一样是因为在Visualizer的script中对4部分的位置都分别做了调整。 

 然后在AugmentedImageExampleController中绑定了上面的prefab

初见遇到的坑

这些坑太蠢了,笑笑跳过去吧。

1、换成自己的模型之后,在Visualizer脚本中试图修改模型的缩放和位置,但是没有效果,模型显示的时候还是出现在初始位置且是初始的缩放比例。

最初的示例Augmented Image中,当改变marker的大小时,模型的位置和大小也会发生改变,也就是说,从一开始模型和marker的相对大小就是固定的,而脚本中有修改模型位置的代码。我的问题就在于,这个修改位置的代码不起作用。

是因为我在Inspector上脚本处绑定物体不对。我绑定的是模型文件夹下的,而不是prefab内的模型?二者是不同的,相互独立的,虽然它们大小坐标等等是一样的,但是在场景中显示的是prefab内的模型,我们修改的也应当是prefab的组成部分,也就是说之前修改的统统没有使用。

对!就是这里。

2、关于localscale缩放大小的问题,是我蠢逼了,要么在script中设置,要么在Inspector中设置,不要两头同时做。被自己蠢到了,计算了一晚上找不出到底哪里错了,还搁这研究坐标变换呢....

坐标系

就算不熟悉ARCore、unity的坐标系,通过Vector3的forward、back等6个向量加上prefab中四个模型和Visualizer脚本中的代码,我们也可以推出来坐标系的样子。如下图:

unity中的世界坐标系是左手坐标系,用左手做出一个手枪枪毙的动作,将中指指向掌心方向,大拇指代表X轴,食指代表Y轴,中指代表Z轴。

6个单位向量的xyz值

Vector3.forward  (0,0,1)

Vector3.left  (-1,0,0)

Vector3.up  (0,1,0)

back,right,down分别相反

着色器Shader

网上已经有很多这种“透明物体遮挡实体”的效果了,参考:

https://jingyan.baidu.com/article/9faa7231f98071473c28cb85.html

https://blog.csdn.net/u014361280/article/details/103690369

最近正在看《Unity Shader入门精要》这本,微信读书上就有,对入门算是很友好了。

为了实现透明且同时遮挡后面物体的效果,需要单独创建一个Shader,使用VS打开,修改其代码

Shader "Unlit/MaskShader"
{
    SubShader
    {
        Tags { "Queue" = "Geometry-10"}

		Lighting off

		ZTest LEqual 

		ZWrite On

		ColorMask 0
		Pass{}
	}
}

与传送门的不同

与传送门不同的是,要更改“门”的大小到和marker一致,这个很简单,因为ARCore能够估计marker的宽高并返回值。

public AugmentedImage Image;

可通过Image的ExtentX和ExtentZ属性来获得ARCore对增强图像尺寸的估计

C#中结构体struct的坑

C#中的结构体

localScale.x = 1.0f;

这样是不行的,要修改必须将localScale整个修改,可以搜索关键字“C#结构体”来了解一下这个问题。

localScale = new Vector3(x,y,z);

代码

Visualizer:

//-----------------------------------------------------------------------
// <copyright file="AugmentedImageVisualizer.cs" company="Google LLC">
//
// Copyright 2018 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// </copyright>
//-----------------------------------------------------------------------

namespace GoogleARCore.Examples.AugmentedImage
{
    using System;
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    using GoogleARCore;
    using GoogleARCoreInternal;
    using UnityEngine;

    /// <summary>
    /// Uses 4 frame corner objects to visualize an AugmentedImage.
    /// </summary>
    public class MyVisualizer : MonoBehaviour
    {
        /// <summary>
        /// The AugmentedImage to visualize.
        /// </summary>
        //增强图像Image
        public AugmentedImage Image;

        //box变成marker的几倍
        //必须为大于等于1的值,最好大于等于2
        public float box_scale = 3.0f;
        //box_depth为box的深度
        //注意box要下降的深度应当为box_depth的一半,同时roof是不需要下降的
        public float box_depth = 3.0f;

        /// <summary>
        /// A model for the lower left corner of the frame to place when an image is detected.
        /// </summary>
        /// 
        public GameObject roof_left;
        public GameObject roof_right;
        public GameObject roof_up;
        public GameObject roof_down;
        public GameObject wall_forward;
        public GameObject wall_back;
        public GameObject wall_left;
        public GameObject wall_right;
        public GameObject wall_bottom;
        public GameObject ball_1;
        public GameObject ball_2;
        public GameObject ball_3;
        public GameObject ball_4;
        public GameObject Mask_roof_left;
        public GameObject Mask_roof_right;
        public GameObject Mask_roof_up;
        public GameObject Mask_roof_down;
        public GameObject Mask_wall_forward;
        public GameObject Mask_wall_back;
        public GameObject Mask_wall_left;
        public GameObject Mask_wall_right;
        public GameObject Mask_wall_bottom;

        /// <summary>
        /// The Unity Update method.
        /// </summary>
        //unity的刷新函数update是从Monobehaviour类中继承来的
        //当检测到增强图像时,这个update函数才会不断调用(因为这个函数的用处就是显示在增强图像上的模型)
        public void Update()
        {
           // UnityEngine.Debug.Log("update MyVisualizer");
            //1、当没有预先设置图像时
            //2、图像检索的状态并不是搜索中
            //将四个模型的Active都设置为False
            if (Image == null || Image.TrackingState != TrackingState.Tracking)
            {
                roof_left.SetActive(false);
                roof_right.SetActive(false);
                roof_up.SetActive(false);
                roof_down.SetActive(false);
                wall_forward.SetActive(false);
                wall_back.SetActive(false);
                wall_left.SetActive(false);
                wall_right.SetActive(false);
                wall_bottom.SetActive(false);
                ball_1.SetActive(false);
                ball_2.SetActive(false);
                ball_3.SetActive(false);
                ball_4.SetActive(false);
                Mask_wall_forward.SetActive(false);
                Mask_wall_back.SetActive(false);
                Mask_wall_left.SetActive(false);
                Mask_wall_right.SetActive(false);
                Mask_wall_bottom.SetActive(false);
                Mask_roof_left.SetActive(false);
                Mask_roof_right.SetActive(false);
                Mask_roof_up.SetActive(false);
                Mask_roof_down.SetActive(false);


                return;
            }

            float roof_thickness = 0.0001f;
            //估计图像(图像是2维的)物理宽度、高度的一半
            float halfWidth = Image.ExtentX / 2;
            float halfHeight = Image.ExtentZ / 2;

            //mask缩放和移动时使用的偏移量
            float offset_for_mask_scale = 0.005f;
            float offset_for_mask_pos = 0.001f;//此处应和墙的厚度一致,不然会出现闪烁
            //临时存储数据
            float temp_for_mask_x;
            float temp_for_mask_z;
           
            Vector3 temp;

            //ball
            float min = Math.Min(halfHeight, halfWidth);

            ball_1.transform.localScale =
                new Vector3(0.01f,0.01f,0.01f);

            temp = ball_2.transform.localScale;
            temp.x = min;
            temp.y = min ;
            temp.z = min ;
            ball_2.transform.localScale =
                new Vector3(temp.x, temp.y, temp.z);

            temp = ball_3.transform.localScale;
            temp.x = min ;
            temp.y = min ;
            temp.z = min ;
            ball_3.transform.localScale =
                new Vector3(temp.x, temp.y, temp.z);

            temp = ball_4.transform.localScale;
            temp.x = min ;
            temp.y = min ;
            temp.z = min ;
            ball_4.transform.localScale =
                new Vector3(temp.x,temp.y,temp.z);
            
            
            //roof_left和right的缩放
            temp = roof_left.transform.localScale;
            temp.x = halfWidth * (box_scale - 1);
            temp.z = halfHeight * 2 * box_scale;
            temp_for_mask_x = temp.x + offset_for_mask_scale;
            temp_for_mask_z = temp.z + offset_for_mask_scale;
            roof_left.transform.localScale = new Vector3(temp.x, roof_thickness, temp.z);
            roof_right.transform.localScale = new Vector3(temp.x, roof_thickness, temp.z);
            Mask_roof_left.transform.localScale = new Vector3(temp_for_mask_x, roof_thickness, temp_for_mask_z);
            Mask_roof_right.transform.localScale = new Vector3(temp_for_mask_x, roof_thickness, temp_for_mask_z);
            //roof up和down的缩放
            temp = roof_up.transform.localScale;
            temp.x = halfWidth * 2;
            temp.z = halfHeight *(box_scale-1);
            temp_for_mask_x = temp.x + offset_for_mask_scale;
            temp_for_mask_z = temp.z + offset_for_mask_scale;
            roof_up.transform.localScale = new Vector3(temp.x, roof_thickness, temp.z);
            roof_down.transform.localScale = new Vector3(temp.x, roof_thickness, temp.z);
            Mask_roof_up.transform.localScale = new Vector3(temp_for_mask_x, roof_thickness, temp_for_mask_z);
            Mask_roof_down.transform.localScale = new Vector3(temp_for_mask_x, roof_thickness, temp_for_mask_z);

            //forward和back的缩放
            temp = wall_forward.transform.localScale;
            temp.x = halfWidth * 2* box_scale;
            temp_for_mask_x = temp.x + offset_for_mask_scale;
            temp.y = box_depth;
            wall_forward.transform.localScale = new Vector3(temp.x, temp.y, temp.z);
            wall_back.transform.localScale = new Vector3(temp.x, temp.y, temp.z);
            Mask_wall_forward.transform.localScale = new Vector3(temp_for_mask_x, temp.y, temp.z);
            Mask_wall_back.transform.localScale = new Vector3(temp_for_mask_x, temp.y, temp.z);

            //left和right的缩放
            temp = wall_left.transform.localScale;
            temp.z = halfHeight * 2* box_scale;
            temp_for_mask_z = temp.z + offset_for_mask_scale;
            temp.y = box_depth;
            wall_left.transform.localScale = new Vector3(temp.x, temp.y, temp.z);
            wall_right.transform.localScale = new Vector3(temp.x, temp.y, temp.z);
            Mask_wall_left.transform.localScale = new Vector3(temp.x, temp.y, temp_for_mask_z);
            Mask_wall_right.transform.localScale = new Vector3(temp.x, temp.y, temp_for_mask_z);
            //bottom的缩放
            temp = wall_bottom.transform.localScale;
            temp.x = halfWidth * 2* box_scale;
            temp.z = halfHeight * 2* box_scale;
            temp.y = box_depth;
            wall_bottom.transform.localScale = new Vector3(temp.x, roof_thickness, temp.z);
            Mask_wall_bottom.transform.localScale = new Vector3(temp_for_mask_x, roof_thickness, temp_for_mask_z);

            ball_1.transform.localPosition =
                  (box_depth / 2 - ball_2.transform.localScale.y / 2) * Vector3.down;
            ball_2.transform.localPosition = 
                -halfWidth*box_depth*5/2*Vector3.left + (box_depth/2 - ball_2.transform.localScale.y/2)*Vector3.down + halfHeight*box_depth*5/2*Vector3.forward;
            ball_3.transform.localPosition =
                halfWidth*box_depth*5/2* Vector3.left + (box_depth / 2 - ball_2.transform.localScale.y / 2) * Vector3.down + halfHeight*box_depth*5/2 * Vector3.back;
            ball_4.transform.localPosition =
                 (box_depth / 2 - ball_2.transform.localScale.y / 2) * Vector3.down;
            // (halfHeight * (box_scale + 1) / 2 + offset_for_mask_pos) * Vector3.forward;
            //修改roof的位置
            roof_up.transform.localPosition =
                (halfHeight * (box_scale + 1) / 2 + offset_for_mask_pos) * Vector3.forward;
            roof_down.transform.localPosition =
                 (halfHeight * (box_scale + 1) / 2 + offset_for_mask_pos) * Vector3.back;
            roof_left.transform.localPosition =
                 (halfWidth * (box_scale + 1)/2 + offset_for_mask_pos)*Vector3.left;
            roof_right.transform.localPosition =
                 (halfWidth * (box_scale + 1)/2 + offset_for_mask_pos) * Vector3.right;
            //修改roof mask的位置
            Mask_roof_up.transform.localPosition =
                (offset_for_mask_pos * Vector3.up) + halfHeight * (box_scale + 1) / 2 * Vector3.forward;
            Mask_roof_down.transform.localPosition =
                (offset_for_mask_pos * Vector3.up) + halfHeight * (box_scale + 1) / 2 * Vector3.back;
            Mask_roof_left.transform.localPosition =
                (offset_for_mask_pos * Vector3.up) + halfWidth * (box_scale + 1) / 2 * Vector3.left;
            Mask_roof_right.transform.localPosition =
                (offset_for_mask_pos * Vector3.up) + halfWidth * (box_scale + 1) / 2 * Vector3.right;
            //修改位置,让这个box“下沉”,陷入到marker里面。
            wall_forward.transform.localPosition =
                 (box_depth/2 * Vector3.down) + (box_scale*halfHeight * Vector3.forward);
            wall_back.transform.localPosition =
                (box_depth/2 * Vector3.down) + (box_scale * halfHeight * Vector3.back);
            wall_left.transform.localPosition =
                (box_depth/2 * Vector3.down) + (box_scale * halfWidth * Vector3.left);
            wall_right.transform.localPosition =
                (box_depth/2 * Vector3.down) + (box_scale * halfWidth * Vector3.right);
            wall_bottom.transform.localPosition = box_depth/2 * Vector3.down;
            //修改mask的位置,保证能够覆盖墙面的同时,还要不与墙面重叠(否则会有闪烁)
            Mask_wall_forward.transform.localPosition =
                 (box_depth / 2 * Vector3.down) + ((box_scale * halfHeight + offset_for_mask_pos) * Vector3.forward);
            Mask_wall_back.transform.localPosition =
                (box_depth / 2 * Vector3.down) + ((box_scale * halfHeight + offset_for_mask_pos) * Vector3.back);
            Mask_wall_left.transform.localPosition =
                (box_depth / 2 * Vector3.down) + ((box_scale * halfWidth + offset_for_mask_pos) * Vector3.left);
            Mask_wall_right.transform.localPosition =
                (box_depth / 2 * Vector3.down) + ((box_scale * halfWidth + offset_for_mask_pos) * Vector3.right);
            Mask_wall_bottom.transform.localPosition = (box_depth+ offset_for_mask_pos) * Vector3.down;

            roof_left.SetActive(true);
            roof_right.SetActive(true);
            roof_up.SetActive(true);
            roof_down.SetActive(true);
            Mask_roof_left.SetActive(true);
            Mask_roof_right.SetActive(true);
            Mask_roof_up.SetActive(true);
            Mask_roof_down.SetActive(true);

            wall_forward.SetActive(true);
            wall_back.SetActive(true);
            wall_left.SetActive(true);
            wall_right.SetActive(true);
            wall_bottom.SetActive(true);
            ball_1.SetActive(true);
            ball_2.SetActive(true);
            ball_3.SetActive(true);
            ball_4.SetActive(true);
            Mask_wall_forward.SetActive(true);
            Mask_wall_back.SetActive(true);
            Mask_wall_left.SetActive(true);
            Mask_wall_right.SetActive(true);
            Mask_wall_bottom.SetActive(true);

        }
    }
}

 Controller:

//-----------------------------------------------------------------------
// <copyright file="AugmentedImageExampleController.cs" company="Google LLC">
//
// Copyright 2018 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// </copyright>
//-----------------------------------------------------------------------

namespace GoogleARCore.Examples.AugmentedImage
{
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    using GoogleARCore;
    using UnityEngine;
    using UnityEngine.UI;

    /// <summary>
    /// Controller for AugmentedImage example.
    /// </summary>
    /// <remarks>
    /// In this sample, we assume all images are static or moving slowly with
    /// a large occupation of the screen. If the target is actively moving,
    /// we recommend to check <see cref="AugmentedImage.TrackingMethod"/> and
    /// render only when the tracking method equals to
    /// <see cref="AugmentedImageTrackingMethod"/>.<c>FullTracking</c>.
    /// See details in <a href="https://developers.google.com/ar/develop/c/augmented-images/">
    /// Recognize and Augment Images</a>
    /// </remarks>
    /// 一个增强图像示例类
    public class MyController : MonoBehaviour
    {
        /// <summary>
        /// A prefab for visualizing an AugmentedImage.
        /// </summary>
        // 隔壁的MyVisualizer类创建一个预制体AugmentedImageVisualizerPrefab
        public MyVisualizer AugmentedImageVisualizerPrefab;

        /// <summary>
        /// The overlay containing the fit to scan user guide.
        /// </summary>
        // 用来指引用户的覆盖层
        public GameObject FitToScanOverlay;

        //创建一个私有的字典对象,int为索引,图像为内容
        //并对每个都初始化 每一个对应一张图片
        private Dictionary<int, MyVisualizer> m_Visualizers
            = new Dictionary<int, MyVisualizer>();

        private List<AugmentedImage> m_TempAugmentedImages = new List<AugmentedImage>();

        /// <summary>
        /// The Unity Awake() method.
        /// </summary>
        public void Awake()
        {
            UnityEngine.Debug.Log("example awake");
            // Enable ARCore to target 60fps camera capture frame rate on supported devices.
            // Note, Application.targetFrameRate is ignored when QualitySettings.vSyncCount != 0.
            //垂直同步不为0时,目标帧率是无效的
            Application.targetFrameRate = 60;
        }

        /// <summary>
        /// The Unity Update method.
        /// </summary>
        public void Update()
        {
            //UnityEngine.Debug.Log("update example");
            // Exit the app when the 'back' button is pressed.
            //检查是否退出
            if (Input.GetKey(KeyCode.Escape))
            {
                Application.Quit();
            }

            // Only allow the screen to sleep when not tracking.
            //当不追踪图像时允许屏幕睡眠
            //一共有几种Status?
            if (Session.Status != SessionStatus.Tracking)
            {
                Screen.sleepTimeout = SleepTimeout.SystemSetting;
            }
            else
            {
                Screen.sleepTimeout = SleepTimeout.NeverSleep;
            }

            // Get updated augmented images for this frame.
            //GetTrackable函数返回可追踪对象,存储到列表m_TempAugmentedImage
            //类型为增强图像AugmentedImage,还可以是其他的类型(比如增强面部,检测的平面,特征点)
            //使用TrackableQueryFilter.Updated对返回值进行过滤
            Session.GetTrackables<AugmentedImage>(
                m_TempAugmentedImages, TrackableQueryFilter.Updated);

            // Create visualizers and anchors for updated augmented images that are tracking and do
            //为每一个正在追踪的增强图像创建可视化工具和锚点
            // not previously have a visualizer. Remove visualizers for stopped images.
            //
            foreach (var image in m_TempAugmentedImages)
            {
                MyVisualizer visualizer = null;
                m_Visualizers.TryGetValue(image.DatabaseIndex, out visualizer);

                //TrackingState是枚举类型吗
                if (image.TrackingState == TrackingState.Tracking && visualizer == null)
                {
                    // Create an anchor to ensure that ARCore keeps tracking this augmented image.
                    //创建锚点来使ARCore持续追踪增强图像marker
                    //在图像的中心创建锚点
                    Anchor anchor = image.CreateAnchor(image.CenterPose);
                    //强制类型转换为marker可视化对象
                    //使用之前创建的预制体+锚点的方向
                    //Instantiate实例化,这是unity的函数,将一个对象及其子物体完全复制,生成一个新的对象(包括坐标),第一个参数是被复制的对象,第二个参数就是为其赋值新的坐标
                    //将预制体进行复制,坐标为每个图形锚点的坐标
                    visualizer = (MyVisualizer)Instantiate(
                        AugmentedImageVisualizerPrefab, anchor.transform);
                    //将增强图像传给可视化组件
                    visualizer.Image = image;
                    //将可视化组件添加到 字典 m_Visualizers中存储起来
                    m_Visualizers.Add(image.DatabaseIndex, visualizer);
                }
                else if (image.TrackingState == TrackingState.Stopped && visualizer != null)
                {
                    //如果不再追踪增强图像,则将其从字典中移除。并且释放对象
                    m_Visualizers.Remove(image.DatabaseIndex);
                    GameObject.Destroy(visualizer.gameObject);
                }
            }

            // Show the fit-to-scan overlay if there are no images that are Tracking.
            foreach (var visualizer in m_Visualizers.Values)
            {
                if (visualizer.Image.TrackingState == TrackingState.Tracking)
                {
                    FitToScanOverlay.SetActive(false);
                    return;
                }
            }

            FitToScanOverlay.SetActive(true);
        }
    }
}

最后

感觉用pad来显示marker,因为有屏幕反光的原因,会有一定的抖动。改天把marker打印出来试试抖动会不会小一些。打印出的marker是黑白的也没有关系。

检测仅基于高对比度的点,所以彩色和黑白图像都会被检测到,无论使用彩色还是黑白参考图像。

很简单的东西,因为自己太蠢踩了很多坑,其中大多是因为不熟悉unity和C#绕的弯路,感觉可以做点有意思的小玩意。

本文地址:https://blog.csdn.net/Vikanill/article/details/106955314

《ARCore Augmented Image+marker传送门.doc》

下载本文的Word格式文档,以方便收藏与打印。