Video script - ImmersiveStorytelling/DocumentationMasterclass GitHub Wiki

Video Script

This script will be the brain behind the story. It creates, detects and handles data so that the story can react and adapt. To make it more clear, we will divide the code in several pieces.

With this script attached to the Camera Rig, it will be executed once it runs. This script will be the intelligence behind the story that will adapt to the user.

Here we will create all the shots for the story that are needed in correct order (array; 0 = first shot) to play the story. Then we will declare the angles to look at for when to change to the next shot (if programmed to do so).

Since the room made it easy for us, we created a way which made it easy for us to choose a side of the room to look to in the code. To those sides we attached the angle from where in the virtual reality the side was directed to (relative to the direction of the Camera Rig).

As we see in the image below, the camera was not placed accordingly to the sides of the room. The reason of this was that where the two halves of the video meet (camera has two lenses), there is a piece cut out of the 360° video because of automatically stitching of the camera. By rotating the camera a bit, we could avoid important pieces of the room from being left out. However, this made that the angles towards the walls also had to be adjusted.

Room Legend

We created an enum with the absolute positions of the walls (front, right, back, left), which then we could use fast for programming a position to look at. To those enums we linked an array (EulerAngArr) which held the angles relative to the direction of the Camera Rig so that the front wall would be right in front of you and not rotated (by 10° to the left in this case).

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

public enum eulerAngEnum
{
    front,
    right,
    back,
    left,
    none
};

public class startUp : MonoBehaviour
{
    private GameObject obj;

    private int[] eulerAngArr = { 170, 260, 350, 80 }; //angles in degrees {front, right, back, left}

    public int cacheAmount;
    private int step = 0;
    private int numberOfShots;

    private double yAngle = 0;

    //Here we will set the side towards the user has to look to be able to switch shots
    private eulerAngEnum[] switchAngles = {
        eulerAngEnum.none,
        eulerAngEnum.front,
        eulerAngEnum.front,
        eulerAngEnum.front, 
        eulerAngEnum.none, 
        eulerAngEnum.back, 
        eulerAngEnum.none, 
        eulerAngEnum.front 
    };

    void Start()
    {
        numberOfShots = switchAngles.Length + 1;
        //makeSpheres();
    }

    void Update()
    {
        //y-angle from the direction of where the camera is pointed to (direction of sight of the user)
        yAngle = GameObject.Find("Camera (eye)").transform.rotation.eulerAngles.y; 

        /////// CODE ///////
       
        //set begin point of videos
        if (GameObject.Find("[CameraRig]").GetComponent<HPV_Manager>().getPositionMs((byte)step) == 0)
        {
            switch (step)
            {
                case 0: setFilm((byte)step, 101640, false); break;
                case 3: setFilm((byte)step, 18000, true); break;
                case 4: setFilm((byte)step, 18000, true); break;
            }
        }
    }
}

On startup the program will create the shots attached with the correct shots. We will only on start loading the first three spheres with attached shots, so that the program doesn't slow down too much. Whenever a shot changes to the next, another following sphere will be loaded into the program.

    void Start()
    {
        numberOfShots = switchAngles.Length + 1;
        makeSpheres();
    }

    private void makeSpheres()
    {
        for (int i = 1; i < numberOfShots + 1; i++)
        {
            obj = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            obj.name = "shot" + i;
            obj.transform.localScale = new Vector3(100, 100, 100);
            obj.AddComponent<HPV_Node>();
            obj.GetComponent<HPV_Node>().filename = i + ".hpv";

            Renderer myRenderer = obj.GetComponent<MeshRenderer>();
            myRenderer.material.shader = Shader.Find("Custom/HPV/Spherical");

            if (i > 1)
            {
                if (i > 3)
                    obj.SetActive(false);
            }
            obj.transform.parent = GameObject.Find("Steps").transform;
        }
    }

We set the status of all loaded spheres on 'false'. On the running program, we can then choose to set a sphere on 'true' so that the sphere with the shot will be visible to the user. In this way only one sphere will be active during the story (if we set the previous sphere back on 'false' when we switch to the next sphere).

With this function we can set a video by node (number of shot), startPos (position by frames to start from), and the direction (true = forward, false = reverse).

private void setFilm(byte node, int startPos, bool direction)
    {
        GameObject g = GameObject.Find("[CameraRig]");
        g.GetComponent<HPV_Manager>().seekToMs(node, startPos);
        g.GetComponent<HPV_Manager>().setDirection(node, direction);
    }

With this we were able to switch the room upside down so that it looked like the actors were walking on the ceiling. For this we adjusted the rotating variables of the sphere.

Here we are possible to play a video backwards, which we used in the first shot of Black Room. For this we use HPV_Manager.

See here for the code.

(Tip: because the video is reversed, the time also will be reversed of the video. So when using HPV_Manager for including a timer, remember that now 0ms is the end of the video, and the length of the video in ms is the starting point.)

With this function we can detect wether the user (yAngle) overlaps the angle of detection to switch shots. When detection is true, it will switch to the next shot.

private void checkAngles()
    {
        if (yAngle > (eulerAngArr[(int)switchAngles[step]] - 10) && yAngle < (eulerAngArr[(int)switchAngles[step]] + 10)) //angle -10° and +10° gives us an angle of 20° for detection
        {
            if ((step + cacheAmount) < numberOfShots)
                GameObject.Find("Steps").transform.GetChild(step + cacheAmount).gameObject.SetActive(true);

            GameObject.Find("Steps").transform.GetChild(step).gameObject.SetActive(false);
            if (step < numberOfShots - 2)
            {
                step++;
                setFilm((byte)step, 0, true);
            }
            else if (!lastStepStarted)
            {
                lastStepStarted = true;
                setFilm((byte)(step + 1), 0, true);
            }
        }
    }

By using the function checkAngles(), we can compare the user's angle (yAngle) with the angle to look to for switching shots, and when it's true, will switch to the next shot. The angle to look at, we setted on the startup. Here is how we put it in code:

void Update()
{
        if (switchAngles[step] != eulerAngEnum.none) // only if there is an angle to look at for switching shots
        {
            if(step == 3)
            {
                if(step4Finished)
                {
                    checkAngles();
                }
            }
            else if(step == 5)
            {
                if (step5Finished)
                {
                    checkAngles();
                }
            }
            else if(step == 7)
            {
                if (step7Finished)
                {
                    checkAngles();
                }
            }
            else
            {
                checkAngles();
            }
        }
}

In some cases where the user has to look at a certain angle to switch shots, it would already change before an important aspect of the story was played. We made it sure that after a certain action happened, that only from then the detection could be triggered on the angle to switch to the next shot.

We have used two ways to do so.

In many cases, you want first of all to play the video to a certain point before it is able to switch to the next shot.

Since we use HPV for managing video's, our timer depends on the frames that plays in ms. By placing a debug log like the next line of code, we can see the current ms when the video is playing:

Debug.Log(GameObject.Find("[CameraRig]").GetComponent<HPV_Manager>().getPositionMs(shot)); 
//shot is the number of position in the array for which video you want to find the time

If then we found the time until the video has to play, we use the next code for detecting on the angle:

private bool step4Finished = false;

if (GameObject.Find("[CameraRig]").GetComponent<HPV_Manager>().getPositionMs(3) >= 45000) //ms to play to
        {
            step4Finished = true;
        }

if (switchAngles[step] != eulerAngEnum.none)
        {
            if(step == 3)
            {
                if(step4Finished)
                {
                    checkAngles();
                }
            }
        }

We had a shot where something important had to be seen before it was allowed to change to the next shot. To prevent the program from already detecting and switching to the next shot before the object was seen, we simply used a boolean.

private bool charlotteSeen = false;

if (step == 2)
        {
            //Charlotte was sitting to a side which we used for detection
            if (yAngle > (eulerAngArr[2] - 10) && yAngle < (eulerAngArr[2] + 10)) 
            {
                charlotteSeen = true;
            }
        }

if (switchAngles[step] != eulerAngEnum.none)
        {
            if(step == 2)
            {
                if(charlotteSeen)
                {
                    checkAngles();
                    setFilm(3, 18000, true);
                }
            }
       }
⚠️ **GitHub.com Fallback** ⚠️