I'm creating a dynamic AR app that will allow user to "scan" a painting and a video/animation will be superimposed on that object (image tracking)

Hello, I’m currently a senior college student doing my capstone project. Part of my project is creating an AR app that will mainly allow user to scan (point their phone’s camera) a painting and a video will be superimposed to it. I’m using firebase to store images and video/animation of a painting. Basically, I want it to be dynamic. Like a basic Artivive AR implementation. This is my first time using unity or making anything related to AR so it’s really overwhelming and quite new to me (also, I don’t have the time to learn from the beginning). Here are the scripts I’ve made :

FirebaseManager:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using Firebase;
using Firebase.Firestore;
using Firebase.Extensions;
using UnityEngine.Networking;

public class FirebaseManager : MonoBehaviour
{
    private FirebaseFirestore db; // Firebase Firestore database instance
    public static event Action<string, string> OnImageVideoPairReady; // Event for new image-video pair

    void Start()
    {
        InitializeFirebase(); // Initialize Firebase
        RetrieveAndDownloadPaintings(); // Retrieve and download paintings
    }

    private void InitializeFirebase()
    {
        db = FirebaseFirestore.DefaultInstance; // Get the default Firestore instance
        UnityEngine.Debug.Log("Initializing Firestore database instance.");
    }

    private void RetrieveAndDownloadPaintings()
    {
        UnityEngine.Debug.Log("Retrieving paintings...");

        db.Collection("paintings").GetSnapshotAsync().ContinueWithOnMainThread(task =>
        {
            if (task.IsFaulted || task.IsCanceled)
            {
                UnityEngine.Debug.LogError("Failed to retrieve paintings from Firestore.");
                return;
            }

            QuerySnapshot querySnapshot = task.Result;
            UnityEngine.Debug.Log("Successfully retrieved paintings.");

            foreach (var document in querySnapshot.Documents)
            {
                string id = document.Id;
                string title = "";
                string imageUrl = "";
                string animationUrl = "";

                if (document.TryGetValue("title", out title) &&
                    document.TryGetValue("imageUrl", out imageUrl) &&
                    document.TryGetValue("animationUrl", out animationUrl))
                {
                    if (string.IsNullOrEmpty(animationUrl))
                    {
                        UnityEngine.Debug.LogWarning($"Skipping painting {title} because animationUrl is null or empty.");
                        continue;
                    }

                    UnityEngine.Debug.Log($"Processing painting: {title}");

                    // Start downloading image and animation if URLs are present
                    StartCoroutine(DownloadAndSaveFile(imageUrl, "PaintingImageFolder", $"{id}-{title}.jpg", (imagePath) =>
                    {
                        StartCoroutine(DownloadAndSaveFile(animationUrl, "PaintingVideoFolder", $"{id}-{title}.mp4", (videoPath) =>
                        {
                            OnImageVideoPairReady?.Invoke(imagePath, videoPath);
                        }));
                    }));
                }
                else
                {
                    UnityEngine.Debug.LogError($"Document missing required fields: {id}");
                }
            }
        });
    }

    private IEnumerator DownloadAndSaveFile(string url, string folderName, string fileName, Action<string> onComplete)
    {
        UnityEngine.Debug.Log("Starting download for: " + url);
        UnityWebRequest request = UnityWebRequest.Get(url);
        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError)
        {
            UnityEngine.Debug.LogError("Failed to download file: " + request.error);
        }
        else
        {
            UnityEngine.Debug.Log("Download successful for: " + url);

            string folderPath = Path.Combine(UnityEngine.Application.persistentDataPath, folderName);
            if (!Directory.Exists(folderPath))
            {
                Directory.CreateDirectory(folderPath);
            }

            string filePath = Path.Combine(folderPath, fileName);
            File.WriteAllBytes(filePath, request.downloadHandler.data);
            UnityEngine.Debug.Log("File saved to: " + filePath);

            onComplete?.Invoke(filePath); // Invoke callback with the file path
        }
    }
}

ARImageVideoManager:

using System; // Importing System namespace
using System.Collections; // Importing System.Collections namespace
using System.Collections.Generic; // Importing System.Collections.Generic namespace
using System.Diagnostics;
using System.IO; // Importing System.IO namespace
using UnityEngine; // Importing UnityEngine namespace
using UnityEngine.Networking; // Importing UnityEngine.Networking namespace
using UnityEngine.Video; // Importing UnityEngine.Video namespace
using UnityEngine.XR.ARFoundation; // Importing UnityEngine.XR.ARFoundation namespace
using UnityEngine.XR.ARSubsystems; // Importing UnityEngine.XR.ARSubsystems namespace

public class ARImageVideoManager : MonoBehaviour // Defining ARImageVideoManager class inheriting from MonoBehaviour
{
    public ARTrackedImageManager trackedImageManager; // Reference to the ARTrackedImageManager
    public GameObject videoPrefab; // Prefab for video playback

    private Dictionary<string, string> imageToVideoMap = new Dictionary<string, string>(); // Dictionary to map image paths to video paths
    private MutableRuntimeReferenceImageLibrary mutableLibrary; // Mutable runtime reference image library

    void Start()
    {
        StartCoroutine(WaitForARSessionReady());
    }

    private IEnumerator WaitForARSessionReady()
    {
        // Wait until AR session is ready
        while (ARSession.state == ARSessionState.CheckingAvailability ||
               ARSession.state == ARSessionState.None)
        {
            UnityEngine.Debug.Log("Waiting for AR session to become ready. Current state: " + ARSession.state);
            yield return new WaitForSeconds(0.3f); // Check every 0.5 seconds
        }

        if (ARSession.state == ARSessionState.Ready || ARSession.state == ARSessionState.SessionInitializing)
        {
            UnityEngine.Debug.Log("AR session is ready. Current state: " + ARSession.state);
            InitializeRuntimeLibrary();
        }
        else
        {
            UnityEngine.Debug.LogWarning("AR session could not be initialized. Current state: " + ARSession.state);
        }
    }


    private void OnEnable() // Method called when the script is enabled
    {
        FirebaseManager.OnImageVideoPairReady += OnImageVideoPairReady; // Subscribe to the OnImageVideoPairReady event
        trackedImageManager.trackedImagesChanged += OnTrackedImagesChanged; // Subscribe to the trackedImagesChanged event
    }

    private void OnDisable() // Method called when the script is disabled
    {
        FirebaseManager.OnImageVideoPairReady -= OnImageVideoPairReady; // Unsubscribe from the OnImageVideoPairReady event
        trackedImageManager.trackedImagesChanged -= OnTrackedImagesChanged; // Unsubscribe from the trackedImagesChanged event
    }

    private void InitializeRuntimeLibrary() // Method to initialize the runtime image library
    {
        if (trackedImageManager.descriptor.supportsMutableLibrary) // Check if the device supports mutable libraries
        {
            // Create an empty mutable library if supported
            mutableLibrary = trackedImageManager.CreateRuntimeLibrary() as MutableRuntimeReferenceImageLibrary; // Create a mutable runtime library
            if (mutableLibrary != null) // Check if the mutable library is created successfully
            {
                trackedImageManager.referenceLibrary = mutableLibrary; // Set the reference library to the mutable library
                UnityEngine.Debug.Log("Mutable runtime library created successfully."); // Log success
            }
            else
            {
                UnityEngine.Debug.LogError("Failed to create a mutable runtime library."); // Log error if creation failed
            }
        }
        else
        {
            UnityEngine.Debug.LogWarning("This device does not support mutable image libraries."); // Log warning if device does not support mutable libraries
        }
    }

    private void OnImageVideoPairReady(string imagePath, string videoPath) // Event handler for OnImageVideoPairReady event
    {
        if (!imageToVideoMap.ContainsKey(imagePath)) // Check if the image path is not already in the dictionary
        {
            imageToVideoMap[imagePath] = videoPath; // Add the image and video paths to the dictionary

            // Only attempt to add images if the library is mutable
            if (mutableLibrary != null) // Check if the mutable library is not null
            {
                StartCoroutine(LoadAndAddImageToLibrary(imagePath, Path.GetFileNameWithoutExtension(imagePath))); // Start coroutine to load and add image to library
                UnityEngine.Debug.Log("Added image to mutable image library"); // Log success
            }
        }
    }

    private IEnumerator LoadAndAddImageToLibrary(string filePath, string fileName) // Coroutine to load and add image to library
    {
        UnityEngine.Debug.Log("Loading image from: " + filePath); // Log the image path
        using (UnityWebRequest uwr = UnityWebRequestTexture.GetTexture("file://" + filePath)) // Create a UnityWebRequest to load the image
        {
            yield return uwr.SendWebRequest(); // Send the web request and wait for it to complete

            if (uwr.result == UnityWebRequest.Result.ConnectionError || uwr.result == UnityWebRequest.Result.ProtocolError) // Check for errors
            {
                UnityEngine.Debug.LogError("Failed to load image: " + uwr.error); // Log error if the image failed to load
            }
            else
            {
                Texture2D texture = DownloadHandlerTexture.GetContent(uwr); // Get the texture from the web request
                if (texture != null) // Check if the texture is not null
                {
                    UnityEngine.Debug.Log("Image loaded successfully: " + fileName); // Log success
                    mutableLibrary.ScheduleAddImageWithValidationJob(texture, fileName, 0.2f); // Add the image to the mutable library with a specified size
                }
                else
                {
                    UnityEngine.Debug.LogError("Failed to convert image to texture: " + fileName); // Log error if the texture conversion failed
                }
            }
        }
        if (trackedImageManager.referenceLibrary != null)
        {
            UnityEngine.Debug.Log("Reference library image count: " + trackedImageManager.referenceLibrary.count);
        }
    }

    private void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs) // Event handler for trackedImagesChanged event
    {
        UnityEngine.Debug.Log("Current AR Session State during tracking at OnTracked: " + ARSession.state);

        UnityEngine.Debug.Log("Tracked images changed."); // Log when tracked images change

        foreach (ARTrackedImage trackedImage in eventArgs.updated) // Iterate through updated tracked images
        {
            UnityEngine.Debug.Log("Processing tracked image: " + trackedImage.referenceImage.name); // Log the name of the tracked image being processed

            if (trackedImage.trackingState == TrackingState.Tracking) // Check if the tracking state is Tracking
            {
                UnityEngine.Debug.Log("Tracking state is Tracking for image: " + trackedImage.referenceImage.name); // Log if the image is being tracked
                UpdateTrackedImage(trackedImage); // Update the tracked image
            }
            else
            {
                UnityEngine.Debug.Log("Tracking state is not Tracking for image: " + trackedImage.referenceImage.name); // Log if the image is not being tracked
                foreach (Transform child in trackedImage.transform) // Iterate through child objects of the tracked image
                {
                    UnityEngine.Debug.Log("Destroying child object of tracked image: " + trackedImage.referenceImage.name); // Log the destruction of the child object
                    Destroy(child.gameObject); // Destroy video prefab if tracking is lost
                }
            }
        }
    }


    private void UpdateTrackedImage(ARTrackedImage trackedImage) // Method to update a tracked image
    {
        string imageName = trackedImage.referenceImage.name; // Get the image name
        string imagePath = Path.Combine(UnityEngine.Application.persistentDataPath, "PaintingImageFolder", imageName + ".jpg"); // Get the image path

        if (imageToVideoMap.ContainsKey(imagePath)) // Check if the image path exists in the dictionary
        {
            string videoPath = imageToVideoMap[imagePath]; // Get the corresponding video path
            GameObject videoObject = Instantiate(videoPrefab, trackedImage.transform); // Instantiate the video prefab
            videoObject.GetComponent<VideoPlayer>().url = videoPath; // Set the video URL
            videoObject.GetComponent<VideoPlayer>().Play(); // Play the video
            UnityEngine.Debug.Log("Playing video for image: " + imageName); // Log success
        }
    }
}

Also, if it were you how would you go about making the scene? Like which gameObjects and components to make. If possible, can you give me the step-by-step process.

Because in the scene that I’ve created, based on the logs, everything in the script is working except the tracking part of ARImageVideoManager (where, the no logs are printing).

   private void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs) // Event handler for trackedImagesChanged event
   {
       UnityEngine.Debug.Log("Current AR Session State during tracking at OnTracked: " + ARSession.state);

       UnityEngine.Debug.Log("Tracked images changed."); // Log when tracked images change

       foreach (ARTrackedImage trackedImage in eventArgs.updated) // Iterate through updated tracked images
       {
           UnityEngine.Debug.Log("Processing tracked image: " + trackedImage.referenceImage.name); // Log the name of the tracked image being processed

           if (trackedImage.trackingState == TrackingState.Tracking) // Check if the tracking state is Tracking
           {
               UnityEngine.Debug.Log("Tracking state is Tracking for image: " + trackedImage.referenceImage.name); // Log if the image is being tracked
               UpdateTrackedImage(trackedImage); // Update the tracked image
           }
           else
           {
               UnityEngine.Debug.Log("Tracking state is not Tracking for image: " + trackedImage.referenceImage.name); // Log if the image is not being tracked
               foreach (Transform child in trackedImage.transform) // Iterate through child objects of the tracked image
               {
                   UnityEngine.Debug.Log("Destroying child object of tracked image: " + trackedImage.referenceImage.name); // Log the destruction of the child object
                   Destroy(child.gameObject); // Destroy video prefab if tracking is lost
               }
           }
       }
   }


   private void UpdateTrackedImage(ARTrackedImage trackedImage) // Method to update a tracked image
   {
       string imageName = trackedImage.referenceImage.name; // Get the image name
       string imagePath = Path.Combine(UnityEngine.Application.persistentDataPath, "PaintingImageFolder", imageName + ".jpg"); // Get the image path

       if (imageToVideoMap.ContainsKey(imagePath)) // Check if the image path exists in the dictionary
       {
           string videoPath = imageToVideoMap[imagePath]; // Get the corresponding video path
           GameObject videoObject = Instantiate(videoPrefab, trackedImage.transform); // Instantiate the video prefab
           videoObject.GetComponent<VideoPlayer>().url = videoPath; // Set the video URL
           videoObject.GetComponent<VideoPlayer>().Play(); // Play the video
           UnityEngine.Debug.Log("Playing video for image: " + imageName); // Log success
       }
   }

I’ve been at it for almost a week now, and I can’t seem to make it work. Tutorials available on Youtube and other sites are mostly for static ones. Would really appreciate your help.

Unity doesn’t have any sample code for your Firebase use case. The best we can offer you is this:

Docs: AR Tracked Image Manager component | AR Foundation | 6.0.3

Sample scene that demonstrates mutable reference image libraries (but not the Firebase piece):

You can also add images to the reference image library at runtime. This sample includes a button that adds the images one.png and two.png to the reference image library. Refer to the script DynamicLibrary.cs for example code.

When using Firebase in Unity you should not call any Firebase methods until initializing Firebase using Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread( as described in Add Firebase to your Unity project

Initialization would look somewhat like this:

Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task => {
  var dependencyStatus = task.Result;
  if (dependencyStatus == Firebase.DependencyStatus.Available) {
    // Create and hold a reference to your FirebaseApp,
    // where app is a Firebase.FirebaseApp property of your application class.
       app = Firebase.FirebaseApp.DefaultInstance;

    // Set a flag here to indicate whether Firebase is ready to use by your app.
    // ALTERNATIVELY: Start your initialization code from here.
  } else {
    UnityEngine.Debug.LogError(System.String.Format(
      "Could not resolve all Firebase dependencies: {0}", dependencyStatus));
    // Firebase Unity SDK is not safe to use here.
  }
});