Simple Persistent AR: Complete code

All of the code for the project can be found below or on GitHub.

^PersistentARinDomain.cs:^:

using System.Collections;
using System.Collections.Generic;
using Auki.ConjureKit;
using Auki.ConjureKit.Manna;
using Auki.Integration.ARFoundation;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
using Random = UnityEngine.Random;

public class PersistentARinDomain : MonoBehaviour
{
    [SerializeField] private Camera arCamera;
    [SerializeField] private GameObject cube;
    [SerializeField] private GameObject calibrateUI;
    [SerializeField] private ARRaycastManager raycastManager;
    [SerializeField] private Button createCubeButton;

    private const string AppKey = "YOUR_APP_KEY";
    private const string AppSecret = "YOUR_APP_SECRET";

    private IConjureKit _conjureKit;
    private Manna _manna;
    private bool _calibrated = false;
    private List<ARRaycastHit> _arRaycastHits = new List<ARRaycastHit>();
    private SaveData _saveData = new SaveData();
    
    private void Start()
    {
        _conjureKit = new ConjureKit(
            arCamera.transform,
            AppKey,
            AppSecret);
        _manna = new Manna(_conjureKit);
    
        var textureProviderComp = CameraFrameProvider.GetOrCreateComponent();
        textureProviderComp.OnNewFrameReady += frame => _manna.ProcessVideoFrameTexture(frame.Texture, frame.ARProjectionMatrix, frame.ARWorldToCameraMatrix);
        _manna.OnLighthouseTracked += OnLighthouseTracked;
        
        createCubeButton.onClick.AddListener(OnCubeButtonClick);
    
        _conjureKit.Connect();
    }
    
    private void OnLighthouseTracked(Lighthouse lighthouse, Pose qrPose, bool isCalibrationGood)
    {
        // If the QR detection was good enough and the QR code is static (generated from the posemesh console),
        // hide the calibration view and show the cube marker 
        if (isCalibrationGood && lighthouse.Type == Lighthouse.LighthouseType.Static)
        {
            if(!_calibrated)
            {
                _calibrated = true;
                calibrateUI.SetActive(false);
                cube.SetActive(true);
                LoadLocally();
            }
        }
    }

    private void Update()
    {
        // Make a raycast from the center of the screen to an AR plane (floor, wall, or any other surface detected by ARFoundation)
        var ray = arCamera.ViewportPointToRay(Vector3.one * 0.5f);
        if (raycastManager.Raycast(ray, _arRaycastHits, TrackableType.PlaneWithinPolygon))
        {
            // Place the cube where the raycast hits a plane. Move it half the cube size along the hit normal (up if on the ground, forward if on the wall)
            cube.transform.position = _arRaycastHits[0].pose.position + _arRaycastHits[0].pose.up * cube.transform.localScale.x / 2f;
            // Rotate the cube only around y axis to always face the camera
            cube.transform.rotation = Quaternion.Euler(Vector3.Scale(arCamera.transform.rotation.eulerAngles, Vector3.up));
        }
    }
    
    private void PlaceCube(Vector3 position, Quaternion rotation, Color color)
    {
        var placedCube = Instantiate(cube, position, rotation);
        placedCube.GetComponent<Renderer>().material.color = color;
        placedCube.gameObject.SetActive(true);
    }
    
    private void OnCubeButtonClick()
    {
        var color = Random.ColorHSV();
        // Place the cube where the cube marker is 
        PlaceCube(cube.transform.position, cube.transform.rotation, color);
        // Save the position and rotation information locally
        _saveData.cubes.Add(new CubeData(cube.transform.position, cube.transform.rotation, color));
        SaveLocally();
    }
    
    private void SaveLocally()
    {
        var json = JsonUtility.ToJson(_saveData);
        PlayerPrefs.SetString("_saveData", json);
        PlayerPrefs.Save();
    }
    
    private void LoadLocally()
    {
        if(!PlayerPrefs.HasKey("_saveData"))
            return;
    
        var json = PlayerPrefs.GetString("_saveData");
        _saveData = JsonUtility.FromJson<SaveData>(json);

        foreach (var savedCube in _saveData.cubes)
        {
            PlaceCube(savedCube.position.ToVector3(), savedCube.rotation.ToQuaternion(), savedCube.color.ToColor());
        }
    }
}

^SaveData.cs^:

using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class SaveData
{
    public List<CubeData> cubes = new List<CubeData>();
}

// Because Unity's Vector3, Quaternion and Color structs are not marked as [Serializable] they can't be serialized into JSON.
// For that we create serializable versions of each one. There can be other approaches depending on how you serialize/deserialize the data.
[Serializable]
public class CubeData
{
    public SerializableVector3 position;
    public SerializableQuaternion rotation;
    public SerializableColor color;

    public CubeData() {}

    public CubeData(Vector3 position, Quaternion rotation, Color color)
    {
        this.position = new SerializableVector3(position);
        this.rotation = new SerializableQuaternion(rotation);
        this.color = new SerializableColor(color);
    }
}

[Serializable]
public class SerializableVector3
{
    public float x, y, z;
    
    public SerializableVector3() {}

    public SerializableVector3(Vector3 sourceVector)
    {
        x = sourceVector.x;
        y = sourceVector.y;
        z = sourceVector.z;
    }

    public Vector3 ToVector3() => new Vector3(x, y, z);
}

[Serializable]
public class SerializableQuaternion
{
    public float x, y, z, w;
    
    public SerializableQuaternion() {}

    public SerializableQuaternion(Quaternion sourceQuaternion)
    {
        x = sourceQuaternion.x;
        y = sourceQuaternion.y;
        z = sourceQuaternion.z;
        w = sourceQuaternion.w;
    }
    
    public Quaternion ToQuaternion() => new Quaternion(x, y, z, w);
}

[Serializable]
public class SerializableColor
{
    public float r, g, b, a;
    
    public SerializableColor() {}

    public SerializableColor(Color sourceColor)
    {
        r = sourceColor.r;
        g = sourceColor.g;
        b = sourceColor.b;
        a = sourceColor.a;
    }
    
    public Color ToColor() => new Color(r, g, b, a);
}

The full project can be found on GitHub.

Need a refresher on the essentials?

Check out more lessons, DIY kits and essentials reading material at the developer learning centre homepage.

Want to build on the posemesh and need a helping hand?

Apply for a grant of AUKI tokens to get your project off the ground, and work directly with the Auki Labs team to get your creation to market. Successful applicants may be granted up to 100k USD worth of AUKI tokens, and development and marketing support from the Auki Labs team.