Customize Camera Transition Path

Hey there!

If have an environment with lots of different virtual cams. The cameras are categorized into different layers in a hierarchy structure. Overview->Area->Building->Engine.

The cameras of the layers are set up to have different distances in the given order above and are free look cameras that rotate around a pivot using the orbital transposer. The layers are basically zoom levels of the environment. You can go freely up and down in the zoom levels and the transitions work great out of the box for most use cases.

The problem I currently face is happening when I switch from one Engine-Object to another Engine-Object that is in another building. The default blending options don’t allow me to specify a path where the cam moves along, so the cam takes the shortest path. And the shortest path is this case is very bad, as it is a straight line through tons of walls, which looks pretty ugly ;(

My current solution(which is actually working) is to add an Y-Offset arc to the blending path, which works as the following:
First I wrote an CinemachineExtension called “CameraPositionOffset”
Code

public class CameraPositionOffset : CinemachineExtension
{
    public Vector3 offset = Vector3.zero;
    public CinemachineCore.Stage stage = CinemachineCore.Stage.Aim;

    protected override void PostPipelineStageCallback(
        CinemachineVirtualCameraBase vcam,
        CinemachineCore.Stage stage, ref CameraState state, float deltaTime)
    {
        if (stage == this.stage)
        {
            state.PositionCorrection += offset;
        }
    }
}

which allows me to specify a position offset for the camera.

I then added a script to the GO that holds the CinemachineBrain:
Code

[RequireComponent(typeof(CinemachineBrain))]
public class CameraAnimatedHeightOffset : MonoBehaviour
{
    public float heightOffset = 20f;
    public CinemachineCore.Stage stage;

    private CinemachineBrain _brain;

    private CameraPositionOffset _lastOffsetCompA;
    private CameraPositionOffset _lastOffsetCompB;

    private Vector3 _lastOffset = Vector3.zero;

    private void Awake()
    {
        _brain = GetComponent<CinemachineBrain>();
    }

    private void Update()
    {
        TryReset(ref _lastOffsetCompA);
        TryReset(ref _lastOffsetCompB);

        if (_brain.ActiveBlend != null && !_brain.ActiveBlend.IsComplete)
        {
            var blend = _brain.ActiveBlend;

            if (blend.CamA != null && blend.CamB != null)
            {
                //Engine to engine transtion
                if (blend.CamA.Name.Contains("Engine") && blend.CamB.Name.Contains("Engine"))
                {
                    float lerp = Mathf.Clamp01(blend.TimeInBlend / blend.Duration);
                    Vector3 offset = heightOffset * Mathf.Sin(lerp * Mathf.PI) * Vector3.up;
                    _lastOffset = offset;

                    var compA = blend.CamA.VirtualCameraGameObject.GetComponent<CameraPositionOffset>();
                    TryApplyOffset(compA, ref _lastOffsetCompA, offset);
                    var compB = blend.CamB.VirtualCameraGameObject.GetComponent<CameraPositionOffset>();
                    TryApplyOffset(compB, ref _lastOffsetCompB, offset);
                    return;
                }
            }
        }
        _lastOffset = Vector3.zero;
    }

    private void TryReset(ref CameraPositionOffset cam)
    {
        if(cam != null)
        {
            cam.offset = _lastOffset;
            cam = null;
        }
    }

    private void TryApplyOffset(CameraPositionOffset from, ref CameraPositionOffset to, Vector3 offset)
    {
        if (from)
        {
            to = from;
            to.offset = offset;
            to.stage = stage;
        }
    }
}

It basically check every frame if a transition between two Engine-Object cams is happening (by name), and if true applies an sine wave Y-Offset to both cameras using the CameraPositionOffset script above.

This solution actually works as intended and the cam makes a nice arc that avoids flying though the walls.
But from a coding standpoint I find this to be a very very ugly solution :eyes:

So to the actual question after this way to long introduction :wink:
Is there any better and more flexible way to customize the transition path between cameras?

A system like the custom blends is pretty much what I am looking for, but with control over the path instead of only the duration and style. I looked through a lot of source code and other forum threatds but could’t find anything useful on this topic.

Any help is appreciated!

Greets
MSQTobi

Hi Tobi,

There is no out-of-the-box way in CM to arbitrarily customize a transition path. You have some control with blend hints - have you tried those? You can choose a spherical or cylindrical path around the target instead of a linear one.

4710356--445106--upload_2019-7-3_10-44-16.png

1 Like

Hi Gregoryl,

That was an insane fast answer thanks!
Yes I tried all of the options but none had the desired effect.

If there is no direct way to customize the transition path, it seems I have to stick to my solution of animating the source and target camera for now.

You could potentially simplify your code a little, and get rid of the script on the Brain.
In your extension, you can call CinemachineCore.Instance.FindPotentialTargetBrain(vcam) to get the Brain, and then check the blend there, so all the logic can be in the extension. A little neater that way. Not a bad solution, really.

Thanks for your help!
I did some refactoring and packed everything into a single component like you suggested.
I am pretty happy with the result now :slight_smile:

For anyone interested in the final code:

using UnityEngine;
using Cinemachine;

public class CameraAnimatedHeightOffset : CinemachineExtension
{
    [Header("Animation is only triggerd when other camera name contains this value")]
    public string otherCamNameToTrigger = "Engine";
    [Header("Aniamtion parameter (Curve should start and end at 0, with peak at 1)")]
    public AnimationCurve offsetCurve = new AnimationCurve(new Keyframe[]
        {
            new Keyframe(0f,0f),
            new Keyframe(0.5f,1f),
            new Keyframe(1f,0f),
        });
    public float heightOffset = 5f;
    private Vector3 _offset = Vector3.zero;
    private CinemachineBrain _brain;

    //This should only ever be called if the cam is either active or in transtion
    private void Update()
    {
        if(!VerifyBrain())
        {
            return;
        }
        //Reset offset
        _offset = Vector3.zero;
   
        //Is a blend currently happening
        if (_brain.ActiveBlend != null && !_brain.ActiveBlend.IsComplete)
        {
            var blend = _brain.ActiveBlend;
            if (blend.CamA != null && blend.CamB != null)
            {
                //Sould animate?
                if (ShouldAnimate(blend.CamA, blend.CamB))
                {
                    //Evaluate blend curve
                    float lerp = Mathf.Clamp01(blend.TimeInBlend / blend.Duration);
                    _offset = offsetCurve.Evaluate(lerp) * heightOffset * Vector3.up;
                }
            }
        }
    }

    //CinemachineExtension Interface method
    protected override void PostPipelineStageCallback(
        CinemachineVirtualCameraBase vcam,
        CinemachineCore.Stage stage, ref CameraState state, float deltaTime)
    {
        if (stage == CinemachineCore.Stage.Noise)
        {
            state.PositionCorrection += _offset;
        }
    }

    private bool VerifyBrain()
    {
        if (!_brain)
        {
            _brain = CinemachineCore.Instance.FindPotentialTargetBrain(VirtualCamera);
            return _brain != null;
        }
        return true;
    }

    private bool ShouldAnimate(ICinemachineCamera camA, ICinemachineCamera camB)
    {
        GameObject otherCam;
        if(camA.VirtualCameraGameObject == VirtualCamera.gameObject)
        {
            otherCam = camB.VirtualCameraGameObject;
        }
        else if(camB.VirtualCameraGameObject == VirtualCamera.gameObject)
        {
            otherCam = camA.VirtualCameraGameObject;
        }
        //Camera isn't part of the active blend?
        else
        {
            return false;
        }
        //Check for name match
        return otherCam.name.Contains(otherCamNameToTrigger);
    }
}
1 Like