1734 lines
74 KiB
C#
1734 lines
74 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using NUnit.Framework;
|
|
using Unity.Cinemachine;
|
|
using UnityEditor;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEngine;
|
|
using UnityEngine.Playables;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.TestTools;
|
|
using UnityEngine.Timeline;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace Streamingle.Editor.Tests
|
|
{
|
|
public sealed class PreviewCloneActivationProbe : MonoBehaviour
|
|
{
|
|
internal static int EnableCount { get; set; }
|
|
|
|
private void OnEnable()
|
|
{
|
|
EnableCount++;
|
|
}
|
|
}
|
|
|
|
public sealed class AICameraTimelinePreviewImporterTests
|
|
{
|
|
[Test]
|
|
public void BrainResolutionUsesOnlySceneBrainWithoutCinemachineTrack()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var cameraObject = new GameObject("Render Camera");
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
director.playableAsset = timeline;
|
|
cameraObject.AddComponent<Camera>();
|
|
var brain = cameraObject.AddComponent<CinemachineBrain>();
|
|
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter.ResolveCinemachineBrainForEditor(
|
|
director,
|
|
timeline,
|
|
new[] { brain }),
|
|
Is.SameAs(brain));
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.GetPreviewOutputStatusForEditor(brain),
|
|
Does.Contain("are active"));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(cameraObject);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void BrainResolutionPrefersUniqueMainCamera()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var mainCameraObject = new GameObject("Main Camera");
|
|
var secondaryCameraObject = new GameObject("Secondary Camera");
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
director.playableAsset = timeline;
|
|
mainCameraObject.tag = "MainCamera";
|
|
mainCameraObject.AddComponent<Camera>();
|
|
var mainBrain = mainCameraObject.AddComponent<CinemachineBrain>();
|
|
secondaryCameraObject.AddComponent<Camera>();
|
|
var secondaryBrain =
|
|
secondaryCameraObject.AddComponent<CinemachineBrain>();
|
|
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter.ResolveCinemachineBrainForEditor(
|
|
director,
|
|
timeline,
|
|
new[] { secondaryBrain, mainBrain }),
|
|
Is.SameAs(mainBrain));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(mainCameraObject);
|
|
Object.DestroyImmediate(secondaryCameraObject);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void BrainResolutionRejectsAmbiguousSceneBrains()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var firstCameraObject = new GameObject("First Camera");
|
|
var secondCameraObject = new GameObject("Second Camera");
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
director.playableAsset = timeline;
|
|
firstCameraObject.AddComponent<Camera>();
|
|
var firstBrain = firstCameraObject.AddComponent<CinemachineBrain>();
|
|
secondCameraObject.AddComponent<Camera>();
|
|
var secondBrain = secondCameraObject.AddComponent<CinemachineBrain>();
|
|
|
|
Assert.That(
|
|
() => AICameraTimelinePreviewImporter
|
|
.ResolveCinemachineBrainForEditor(
|
|
director,
|
|
timeline,
|
|
new[] { firstBrain, secondBrain }),
|
|
Throws.TypeOf<InvalidOperationException>()
|
|
.With.Message.Contains("Multiple enabled CinemachineBrain"));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(firstCameraObject);
|
|
Object.DestroyImmediate(secondCameraObject);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void BrainResolutionUsesDisabledUniqueDescendantBinding()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var childDirectorObject = new GameObject("Camera Director");
|
|
var cameraObject = new GameObject("Configured Camera");
|
|
childDirectorObject.transform.SetParent(directorObject.transform, false);
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
TimelineAsset childTimeline = null;
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
director.playableAsset = timeline;
|
|
var childDirector =
|
|
childDirectorObject.AddComponent<PlayableDirector>();
|
|
cameraObject.AddComponent<Camera>();
|
|
var brain = cameraObject.AddComponent<CinemachineBrain>();
|
|
childTimeline = CreateTimelineBoundToBrain(
|
|
childDirector,
|
|
brain,
|
|
"Child Camera Track");
|
|
childDirectorObject.SetActive(false);
|
|
brain.enabled = false;
|
|
|
|
LogAssert.Expect(
|
|
LogType.Warning,
|
|
"The CinemachineBrain explicitly bound to a Timeline or its " +
|
|
"Camera is disabled. Camera AI will use the configured Brain " +
|
|
"without enabling either component automatically.");
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter.ResolveCinemachineBrainForEditor(
|
|
director,
|
|
timeline,
|
|
new[] { brain }),
|
|
Is.SameAs(brain));
|
|
Assert.That(brain.enabled, Is.False);
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.GetPreviewOutputStatusForEditor(brain),
|
|
Does.Contain("enable it to see AI camera playback"));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(cameraObject);
|
|
Object.DestroyImmediate(childTimeline);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void BrainResolutionDeduplicatesSameBrainAcrossDescendants()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var firstChildObject = new GameObject("First Camera Director");
|
|
var secondChildObject = new GameObject("Second Camera Director");
|
|
var cameraObject = new GameObject("Configured Camera");
|
|
firstChildObject.transform.SetParent(directorObject.transform, false);
|
|
secondChildObject.transform.SetParent(directorObject.transform, false);
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
TimelineAsset firstChildTimeline = null;
|
|
TimelineAsset secondChildTimeline = null;
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
director.playableAsset = timeline;
|
|
var firstChildDirector =
|
|
firstChildObject.AddComponent<PlayableDirector>();
|
|
var secondChildDirector =
|
|
secondChildObject.AddComponent<PlayableDirector>();
|
|
cameraObject.AddComponent<Camera>();
|
|
var brain = cameraObject.AddComponent<CinemachineBrain>();
|
|
firstChildTimeline = CreateTimelineBoundToBrain(
|
|
firstChildDirector,
|
|
brain,
|
|
"First Child Camera Track");
|
|
secondChildTimeline = CreateTimelineBoundToBrain(
|
|
secondChildDirector,
|
|
brain,
|
|
"Second Child Camera Track");
|
|
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter.ResolveCinemachineBrainForEditor(
|
|
director,
|
|
timeline,
|
|
Array.Empty<CinemachineBrain>()),
|
|
Is.SameAs(brain));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(cameraObject);
|
|
Object.DestroyImmediate(firstChildTimeline);
|
|
Object.DestroyImmediate(secondChildTimeline);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void BrainResolutionRejectsAmbiguousDescendantBindings()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var firstChildObject = new GameObject("First Camera Director");
|
|
var secondChildObject = new GameObject("Second Camera Director");
|
|
var firstCameraObject = new GameObject("First Configured Camera");
|
|
var secondCameraObject = new GameObject("Second Configured Camera");
|
|
firstChildObject.transform.SetParent(directorObject.transform, false);
|
|
secondChildObject.transform.SetParent(directorObject.transform, false);
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
TimelineAsset firstChildTimeline = null;
|
|
TimelineAsset secondChildTimeline = null;
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
director.playableAsset = timeline;
|
|
var firstChildDirector =
|
|
firstChildObject.AddComponent<PlayableDirector>();
|
|
var secondChildDirector =
|
|
secondChildObject.AddComponent<PlayableDirector>();
|
|
firstCameraObject.AddComponent<Camera>();
|
|
var firstBrain =
|
|
firstCameraObject.AddComponent<CinemachineBrain>();
|
|
secondCameraObject.AddComponent<Camera>();
|
|
var secondBrain =
|
|
secondCameraObject.AddComponent<CinemachineBrain>();
|
|
firstChildTimeline = CreateTimelineBoundToBrain(
|
|
firstChildDirector,
|
|
firstBrain,
|
|
"First Child Camera Track");
|
|
secondChildTimeline = CreateTimelineBoundToBrain(
|
|
secondChildDirector,
|
|
secondBrain,
|
|
"Second Child Camera Track");
|
|
|
|
Assert.That(
|
|
() => AICameraTimelinePreviewImporter
|
|
.ResolveCinemachineBrainForEditor(
|
|
director,
|
|
timeline,
|
|
new[] { firstBrain, secondBrain }),
|
|
Throws.TypeOf<InvalidOperationException>()
|
|
.With.Message.Contains("Descendant PlayableDirectors"));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(firstCameraObject);
|
|
Object.DestroyImmediate(secondCameraObject);
|
|
Object.DestroyImmediate(firstChildTimeline);
|
|
Object.DestroyImmediate(secondChildTimeline);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void BrainResolutionPrefersDirectBindingOverDescendants()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var childDirectorObject = new GameObject("Camera Director");
|
|
var directCameraObject = new GameObject("Direct Camera");
|
|
var childCameraObject = new GameObject("Child Camera");
|
|
childDirectorObject.transform.SetParent(directorObject.transform, false);
|
|
TimelineAsset timeline = null;
|
|
TimelineAsset childTimeline = null;
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
var childDirector =
|
|
childDirectorObject.AddComponent<PlayableDirector>();
|
|
directCameraObject.AddComponent<Camera>();
|
|
var directBrain =
|
|
directCameraObject.AddComponent<CinemachineBrain>();
|
|
childCameraObject.AddComponent<Camera>();
|
|
var childBrain =
|
|
childCameraObject.AddComponent<CinemachineBrain>();
|
|
timeline = CreateTimelineBoundToBrain(
|
|
director,
|
|
directBrain,
|
|
"Direct Camera Track");
|
|
childTimeline = CreateTimelineBoundToBrain(
|
|
childDirector,
|
|
childBrain,
|
|
"Child Camera Track");
|
|
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter.ResolveCinemachineBrainForEditor(
|
|
director,
|
|
timeline,
|
|
new[] { childBrain, directBrain }),
|
|
Is.SameAs(directBrain));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(directCameraObject);
|
|
Object.DestroyImmediate(childCameraObject);
|
|
Object.DestroyImmediate(childTimeline);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void BrainResolutionRejectsExplicitBrainFromAnotherScene()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var cameraObject = new GameObject("Foreign Camera");
|
|
var foreignScene = EditorSceneManager.NewPreviewScene();
|
|
SceneManager.MoveGameObjectToScene(cameraObject, foreignScene);
|
|
TimelineAsset timeline = null;
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
cameraObject.AddComponent<Camera>();
|
|
var brain = cameraObject.AddComponent<CinemachineBrain>();
|
|
timeline = CreateTimelineBoundToBrain(
|
|
director,
|
|
brain,
|
|
"Foreign Camera Track");
|
|
|
|
Assert.That(
|
|
() => AICameraTimelinePreviewImporter
|
|
.ResolveCinemachineBrainForEditor(
|
|
director,
|
|
timeline,
|
|
new[] { brain }),
|
|
Throws.TypeOf<InvalidOperationException>()
|
|
.With.Message.Contains("same scene"));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(cameraObject);
|
|
Object.DestroyImmediate(timeline);
|
|
EditorSceneManager.ClosePreviewScene(foreignScene);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void BrainResolutionRejectsExplicitBrainWithoutCamera()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var cameraObject = new GameObject("Camera-less Brain");
|
|
TimelineAsset timeline = null;
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
var camera = cameraObject.AddComponent<Camera>();
|
|
var brain = cameraObject.AddComponent<CinemachineBrain>();
|
|
Object.DestroyImmediate(camera);
|
|
timeline = CreateTimelineBoundToBrain(
|
|
director,
|
|
brain,
|
|
"Invalid Camera Track");
|
|
|
|
Assert.That(
|
|
() => AICameraTimelinePreviewImporter
|
|
.ResolveCinemachineBrainForEditor(
|
|
director,
|
|
timeline,
|
|
new[] { brain }),
|
|
Throws.TypeOf<InvalidOperationException>()
|
|
.With.Message.Contains("Camera component"));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(cameraObject);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void BrainResolutionAcceptsSeparateControlledOutputCamera()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var brainObject = new GameObject("Configured Brain");
|
|
var outputCameraObject = new GameObject("Controlled Output Camera");
|
|
TimelineAsset timeline = null;
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
var brain = brainObject.AddComponent<CinemachineBrain>();
|
|
var outputCamera = outputCameraObject.AddComponent<Camera>();
|
|
brain.ControlledObject = outputCameraObject;
|
|
timeline = CreateTimelineBoundToBrain(
|
|
director,
|
|
brain,
|
|
"Controlled Output Camera Track");
|
|
|
|
Assert.That(brain.OutputCamera, Is.SameAs(outputCamera));
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter.ResolveCinemachineBrainForEditor(
|
|
director,
|
|
timeline,
|
|
new[] { brain }),
|
|
Is.SameAs(brain));
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.GetPreviewOutputStatusForEditor(brain),
|
|
Does.Contain(outputCameraObject.name));
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.GetPreviewOutputStatusForEditor(brain),
|
|
Does.Contain("are active"));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(brainObject);
|
|
Object.DestroyImmediate(outputCameraObject);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void BrainResolutionUsesControlledOutputMainCameraForFallback()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var mainBrainObject = new GameObject("Main Output Brain");
|
|
var mainCameraObject = new GameObject("Main Output Camera");
|
|
var secondaryCameraObject = new GameObject("Secondary Camera");
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
director.playableAsset = timeline;
|
|
var mainBrain = mainBrainObject.AddComponent<CinemachineBrain>();
|
|
mainCameraObject.tag = "MainCamera";
|
|
mainCameraObject.AddComponent<Camera>();
|
|
mainBrain.ControlledObject = mainCameraObject;
|
|
secondaryCameraObject.AddComponent<Camera>();
|
|
var secondaryBrain =
|
|
secondaryCameraObject.AddComponent<CinemachineBrain>();
|
|
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter.ResolveCinemachineBrainForEditor(
|
|
director,
|
|
timeline,
|
|
new[] { secondaryBrain, mainBrain }),
|
|
Is.SameAs(mainBrain));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(mainBrainObject);
|
|
Object.DestroyImmediate(mainCameraObject);
|
|
Object.DestroyImmediate(secondaryCameraObject);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void BrainResolutionRejectsControlledOutputCameraFromAnotherScene()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var brainObject = new GameObject("Configured Brain");
|
|
var outputCameraObject = new GameObject("Foreign Output Camera");
|
|
var foreignScene = EditorSceneManager.NewPreviewScene();
|
|
SceneManager.MoveGameObjectToScene(outputCameraObject, foreignScene);
|
|
TimelineAsset timeline = null;
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
var brain = brainObject.AddComponent<CinemachineBrain>();
|
|
outputCameraObject.AddComponent<Camera>();
|
|
brain.ControlledObject = outputCameraObject;
|
|
timeline = CreateTimelineBoundToBrain(
|
|
director,
|
|
brain,
|
|
"Foreign Controlled Output Track");
|
|
|
|
Assert.That(
|
|
() => AICameraTimelinePreviewImporter
|
|
.ResolveCinemachineBrainForEditor(
|
|
director,
|
|
timeline,
|
|
new[] { brain }),
|
|
Throws.TypeOf<InvalidOperationException>()
|
|
.With.Message.Contains("output Camera")
|
|
.And.Message.Contains("same scene"));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(brainObject);
|
|
Object.DestroyImmediate(outputCameraObject);
|
|
Object.DestroyImmediate(timeline);
|
|
EditorSceneManager.ClosePreviewScene(foreignScene);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void InactiveStagingCloneNeverActivatesAndPreservesSourcePlayback()
|
|
{
|
|
var parentObject = new GameObject("Preview Parent");
|
|
var sourceObject = new GameObject("Source Director");
|
|
var motionObject = new GameObject("Motion Director");
|
|
var authoredCameraObject = new GameObject("Authored Virtual Camera");
|
|
sourceObject.transform.SetParent(parentObject.transform, false);
|
|
motionObject.transform.SetParent(sourceObject.transform, false);
|
|
authoredCameraObject.transform.SetParent(
|
|
motionObject.transform,
|
|
false);
|
|
parentObject.transform.position = new Vector3(4f, -2f, 7f);
|
|
sourceObject.transform.localPosition = new Vector3(1f, 2f, 3f);
|
|
var sourceTimeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
var motionTimeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
GameObject cloneObject = null;
|
|
PlayableDirector motionDirector = null;
|
|
try
|
|
{
|
|
var sourceDirector =
|
|
sourceObject.AddComponent<PlayableDirector>();
|
|
sourceDirector.playOnAwake = false;
|
|
sourceDirector.playableAsset = sourceTimeline;
|
|
motionDirector = motionObject.AddComponent<PlayableDirector>();
|
|
motionDirector.playOnAwake = true;
|
|
motionDirector.timeUpdateMode = DirectorUpdateMode.Manual;
|
|
motionDirector.playableAsset = motionTimeline;
|
|
var authoredCamera = authoredCameraObject
|
|
.AddComponent<CinemachineCamera>();
|
|
sourceObject.AddComponent<PreviewCloneActivationProbe>();
|
|
|
|
motionDirector.Play();
|
|
motionDirector.time = 0.4d;
|
|
motionDirector.Evaluate();
|
|
var sourceWorldPosition = sourceObject.transform.position;
|
|
var sourcePlaybackState = motionDirector.state;
|
|
PreviewCloneActivationProbe.EnableCount = 0;
|
|
|
|
cloneObject = AICameraTimelinePreviewImporter
|
|
.InstantiateInactivePreviewCloneForEditor(
|
|
sourceObject,
|
|
parentObject.transform);
|
|
var cloneMotionDirector = cloneObject.transform
|
|
.Find(motionObject.name)
|
|
.GetComponent<PlayableDirector>();
|
|
var cloneAuthoredCamera = cloneObject.transform
|
|
.Find($"{motionObject.name}/{authoredCameraObject.name}")
|
|
.GetComponent<CinemachineCamera>();
|
|
|
|
Assert.That(cloneObject.transform.parent, Is.SameAs(parentObject.transform));
|
|
Assert.That(cloneObject.transform.position, Is.EqualTo(sourceWorldPosition));
|
|
Assert.That(cloneObject.activeSelf, Is.False);
|
|
Assert.That(cloneObject.activeInHierarchy, Is.False);
|
|
Assert.That(PreviewCloneActivationProbe.EnableCount, Is.Zero);
|
|
Assert.That(cloneMotionDirector.playOnAwake, Is.True);
|
|
Assert.That(cloneMotionDirector.playableGraph.IsValid(), Is.False);
|
|
Assert.That(cloneAuthoredCamera.enabled, Is.True);
|
|
Assert.That(cloneAuthoredCamera.isActiveAndEnabled, Is.False);
|
|
Assert.That(sourceObject.activeSelf, Is.True);
|
|
Assert.That(sourceObject.activeInHierarchy, Is.True);
|
|
Assert.That(motionDirector.state, Is.EqualTo(sourcePlaybackState));
|
|
Assert.That(motionDirector.time, Is.EqualTo(0.4d).Within(0.000001d));
|
|
Assert.That(authoredCamera.enabled, Is.True);
|
|
Assert.That(authoredCamera.isActiveAndEnabled, Is.True);
|
|
}
|
|
finally
|
|
{
|
|
PreviewCloneActivationProbe.EnableCount = 0;
|
|
if (motionDirector != null)
|
|
{
|
|
motionDirector.Stop();
|
|
}
|
|
|
|
Object.DestroyImmediate(cloneObject);
|
|
Object.DestroyImmediate(parentObject);
|
|
Object.DestroyImmediate(motionTimeline);
|
|
Object.DestroyImmediate(sourceTimeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void PreviewCloneSuppressesOnlyAuthoredCinemachineOutputs()
|
|
{
|
|
var sourceObject = new GameObject("Source Director");
|
|
var motionObject = new GameObject("Motion Director");
|
|
var authoredVirtualCameraObject =
|
|
new GameObject("Authored Virtual Camera");
|
|
var cameraObject = new GameObject("Authored Camera");
|
|
motionObject.transform.SetParent(sourceObject.transform, false);
|
|
authoredVirtualCameraObject.transform.SetParent(
|
|
motionObject.transform,
|
|
false);
|
|
var sourceTimeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
TimelineAsset motionTimeline = null;
|
|
GameObject previewObject = null;
|
|
try
|
|
{
|
|
var sourceDirector =
|
|
sourceObject.AddComponent<PlayableDirector>();
|
|
sourceDirector.playOnAwake = false;
|
|
sourceDirector.playableAsset = sourceTimeline;
|
|
var motionDirector =
|
|
motionObject.AddComponent<PlayableDirector>();
|
|
motionDirector.playOnAwake = false;
|
|
var motionAnimator = motionObject.AddComponent<Animator>();
|
|
var authoredVirtualCamera = authoredVirtualCameraObject
|
|
.AddComponent<CinemachineCamera>();
|
|
cameraObject.AddComponent<Camera>();
|
|
var authoredBrain =
|
|
cameraObject.AddComponent<CinemachineBrain>();
|
|
motionTimeline = CreateTimelineBoundToBrain(
|
|
motionDirector,
|
|
authoredBrain,
|
|
"Authored Camera Track");
|
|
var motionTrack = motionTimeline.CreateTrack<AnimationTrack>(
|
|
null,
|
|
"Motion Track");
|
|
motionDirector.SetGenericBinding(motionTrack, motionAnimator);
|
|
|
|
previewObject = Object.Instantiate(sourceObject);
|
|
var previewMotionObject =
|
|
previewObject.transform.Find(motionObject.name).gameObject;
|
|
var previewMotionDirector =
|
|
previewMotionObject.GetComponent<PlayableDirector>();
|
|
var previewMotionAnimator =
|
|
previewMotionObject.GetComponent<Animator>();
|
|
var previewAuthoredVirtualCamera = previewMotionObject.transform
|
|
.Find(authoredVirtualCameraObject.name)
|
|
.GetComponent<CinemachineCamera>();
|
|
var authoredCameraTrack = motionTimeline.GetOutputTracks()
|
|
.OfType<CinemachineTrack>()
|
|
.Single();
|
|
var motionTimelineWasDirty = EditorUtility.IsDirty(motionTimeline);
|
|
var suppressionState = AICameraTimelinePreviewImporter
|
|
.CaptureAuthoredCinemachineState(
|
|
previewObject.GetComponent<PlayableDirector>());
|
|
|
|
Assert.That(previewObject.activeSelf, Is.True);
|
|
Assert.That(
|
|
previewMotionDirector.GetGenericBinding(authoredCameraTrack),
|
|
Is.SameAs(authoredBrain));
|
|
Assert.That(
|
|
previewMotionDirector.GetGenericBinding(motionTrack),
|
|
Is.SameAs(previewMotionAnimator));
|
|
Assert.That(previewAuthoredVirtualCamera.enabled, Is.True);
|
|
Assert.That(authoredVirtualCamera.enabled, Is.True);
|
|
|
|
AICameraTimelinePreviewImporter
|
|
.SuppressAuthoredCinemachineOutputsForEditor(
|
|
previewObject.GetComponent<PlayableDirector>());
|
|
|
|
Assert.That(
|
|
previewMotionDirector.GetGenericBinding(authoredCameraTrack),
|
|
Is.Null);
|
|
Assert.That(
|
|
previewMotionDirector.GetGenericBinding(motionTrack),
|
|
Is.SameAs(previewMotionAnimator));
|
|
Assert.That(previewAuthoredVirtualCamera.enabled, Is.False);
|
|
Assert.That(authoredVirtualCamera.enabled, Is.True);
|
|
Assert.That(
|
|
previewMotionDirector.playableAsset,
|
|
Is.SameAs(motionTimeline));
|
|
Assert.That(
|
|
motionTimeline.GetOutputTracks(),
|
|
Does.Contain(authoredCameraTrack));
|
|
Assert.That(
|
|
motionTimeline.GetOutputTracks(),
|
|
Does.Contain(motionTrack));
|
|
Assert.That(
|
|
motionDirector.GetGenericBinding(authoredCameraTrack),
|
|
Is.SameAs(authoredBrain));
|
|
Assert.That(
|
|
motionDirector.GetGenericBinding(motionTrack),
|
|
Is.SameAs(motionAnimator));
|
|
Assert.That(
|
|
motionDirector.playableAsset,
|
|
Is.SameAs(motionTimeline));
|
|
Assert.That(
|
|
EditorUtility.IsDirty(motionTimeline),
|
|
Is.EqualTo(motionTimelineWasDirty));
|
|
|
|
AICameraTimelinePreviewImporter.RestoreAuthoredCinemachineState(
|
|
suppressionState);
|
|
|
|
Assert.That(
|
|
previewMotionDirector.GetGenericBinding(authoredCameraTrack),
|
|
Is.SameAs(authoredBrain));
|
|
Assert.That(
|
|
previewMotionDirector.GetGenericBinding(motionTrack),
|
|
Is.SameAs(previewMotionAnimator));
|
|
Assert.That(previewAuthoredVirtualCamera.enabled, Is.True);
|
|
Assert.That(authoredVirtualCamera.enabled, Is.True);
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(previewObject);
|
|
Object.DestroyImmediate(sourceObject);
|
|
Object.DestroyImmediate(cameraObject);
|
|
Object.DestroyImmediate(motionTimeline);
|
|
Object.DestroyImmediate(sourceTimeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void PreviewCloneKeepsParentGeneratedCinemachineBinding()
|
|
{
|
|
var sourceObject = new GameObject("Source Director");
|
|
var motionObject = new GameObject("Motion Director");
|
|
var generatedCameraObject = new GameObject("Generated Camera");
|
|
var authoredCameraObject = new GameObject("Authored Camera");
|
|
motionObject.transform.SetParent(sourceObject.transform, false);
|
|
TimelineAsset sourceTimeline = null;
|
|
TimelineAsset motionTimeline = null;
|
|
GameObject previewObject = null;
|
|
try
|
|
{
|
|
var sourceDirector =
|
|
sourceObject.AddComponent<PlayableDirector>();
|
|
sourceDirector.playOnAwake = false;
|
|
var motionDirector =
|
|
motionObject.AddComponent<PlayableDirector>();
|
|
motionDirector.playOnAwake = false;
|
|
generatedCameraObject.AddComponent<Camera>();
|
|
var generatedBrain =
|
|
generatedCameraObject.AddComponent<CinemachineBrain>();
|
|
authoredCameraObject.AddComponent<Camera>();
|
|
var authoredBrain =
|
|
authoredCameraObject.AddComponent<CinemachineBrain>();
|
|
sourceTimeline = CreateTimelineBoundToBrain(
|
|
sourceDirector,
|
|
generatedBrain,
|
|
AICameraTimelinePreviewImporter.GeneratedCinemachineTrackName);
|
|
motionTimeline = CreateTimelineBoundToBrain(
|
|
motionDirector,
|
|
authoredBrain,
|
|
"Authored Camera Track");
|
|
var generatedTrack = sourceTimeline.GetOutputTracks()
|
|
.OfType<CinemachineTrack>()
|
|
.Single();
|
|
var authoredTrack = motionTimeline.GetOutputTracks()
|
|
.OfType<CinemachineTrack>()
|
|
.Single();
|
|
|
|
previewObject = Object.Instantiate(sourceObject);
|
|
var previewDirector =
|
|
previewObject.GetComponent<PlayableDirector>();
|
|
var previewMotionDirector = previewObject.transform
|
|
.Find(motionObject.name)
|
|
.GetComponent<PlayableDirector>();
|
|
|
|
AICameraTimelinePreviewImporter
|
|
.SuppressAuthoredCinemachineOutputsForEditor(
|
|
previewDirector);
|
|
|
|
Assert.That(
|
|
previewDirector.GetGenericBinding(generatedTrack),
|
|
Is.SameAs(generatedBrain));
|
|
Assert.That(
|
|
previewMotionDirector.GetGenericBinding(authoredTrack),
|
|
Is.Null);
|
|
Assert.That(
|
|
sourceDirector.GetGenericBinding(generatedTrack),
|
|
Is.SameAs(generatedBrain));
|
|
Assert.That(
|
|
motionDirector.GetGenericBinding(authoredTrack),
|
|
Is.SameAs(authoredBrain));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(previewObject);
|
|
Object.DestroyImmediate(sourceObject);
|
|
Object.DestroyImmediate(generatedCameraObject);
|
|
Object.DestroyImmediate(authoredCameraObject);
|
|
Object.DestroyImmediate(motionTimeline);
|
|
Object.DestroyImmediate(sourceTimeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void FinalSuppressionDoesNotCreatePartialUndoEntry()
|
|
{
|
|
var destinationObject = new GameObject("Destination Director");
|
|
var motionObject = new GameObject("Motion Director");
|
|
var authoredVirtualCameraObject =
|
|
new GameObject("Authored Virtual Camera");
|
|
var outputCameraObject = new GameObject("Output Camera");
|
|
var undoSentinelObject = new GameObject("Undo Sentinel");
|
|
motionObject.transform.SetParent(destinationObject.transform, false);
|
|
authoredVirtualCameraObject.transform.SetParent(
|
|
motionObject.transform,
|
|
false);
|
|
var destinationTimeline =
|
|
ScriptableObject.CreateInstance<TimelineAsset>();
|
|
TimelineAsset motionTimeline = null;
|
|
try
|
|
{
|
|
var destinationDirector =
|
|
destinationObject.AddComponent<PlayableDirector>();
|
|
destinationDirector.playableAsset = destinationTimeline;
|
|
var motionDirector =
|
|
motionObject.AddComponent<PlayableDirector>();
|
|
var authoredVirtualCamera = authoredVirtualCameraObject
|
|
.AddComponent<CinemachineCamera>();
|
|
outputCameraObject.AddComponent<Camera>();
|
|
var brain = outputCameraObject.AddComponent<CinemachineBrain>();
|
|
motionTimeline = CreateTimelineBoundToBrain(
|
|
motionDirector,
|
|
brain,
|
|
"Authored Camera Track");
|
|
var cameraTrack = motionTimeline.GetOutputTracks()
|
|
.OfType<CinemachineTrack>()
|
|
.Single();
|
|
|
|
Undo.ClearAll();
|
|
Undo.IncrementCurrentGroup();
|
|
var sentinelGroup = Undo.GetCurrentGroup();
|
|
Undo.RegisterCompleteObjectUndo(
|
|
undoSentinelObject.transform,
|
|
"Final Suppression Undo Sentinel");
|
|
undoSentinelObject.transform.position = Vector3.one;
|
|
Undo.CollapseUndoOperations(sentinelGroup);
|
|
Undo.FlushUndoRecordObjects();
|
|
Undo.IncrementCurrentGroup();
|
|
|
|
AICameraTimelinePreviewImporter
|
|
.SuppressAuthoredCinemachineOutputsForEditor(
|
|
destinationDirector,
|
|
false);
|
|
Undo.FlushUndoRecordObjects();
|
|
|
|
Assert.That(
|
|
motionDirector.GetGenericBinding(cameraTrack),
|
|
Is.Null);
|
|
Assert.That(authoredVirtualCamera.enabled, Is.False);
|
|
|
|
Undo.PerformUndo();
|
|
|
|
Assert.That(undoSentinelObject.transform.position, Is.EqualTo(Vector3.zero));
|
|
Assert.That(
|
|
motionDirector.GetGenericBinding(cameraTrack),
|
|
Is.Null);
|
|
Assert.That(authoredVirtualCamera.enabled, Is.False);
|
|
}
|
|
finally
|
|
{
|
|
Undo.ClearAll();
|
|
Object.DestroyImmediate(destinationObject);
|
|
Object.DestroyImmediate(outputCameraObject);
|
|
Object.DestroyImmediate(undoSentinelObject);
|
|
Object.DestroyImmediate(motionTimeline);
|
|
Object.DestroyImmediate(destinationTimeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void SuppressionRebuildsPlayingDirectorAndReleasesBrainOverride()
|
|
{
|
|
var sourceObject = new GameObject("Source Director");
|
|
var motionObject = new GameObject("Motion Director");
|
|
var authoredCameraObject = new GameObject("Authored Virtual Camera");
|
|
var outputCameraObject = new GameObject("Output Camera");
|
|
motionObject.transform.SetParent(sourceObject.transform, false);
|
|
authoredCameraObject.transform.SetParent(
|
|
motionObject.transform,
|
|
false);
|
|
var sourceTimeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
var motionTimeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
PlayableDirector motionDirector = null;
|
|
try
|
|
{
|
|
var sourceDirector =
|
|
sourceObject.AddComponent<PlayableDirector>();
|
|
sourceDirector.playOnAwake = false;
|
|
sourceDirector.playableAsset = sourceTimeline;
|
|
motionDirector = motionObject.AddComponent<PlayableDirector>();
|
|
motionDirector.playOnAwake = false;
|
|
motionDirector.timeUpdateMode = DirectorUpdateMode.Manual;
|
|
motionDirector.playableAsset = motionTimeline;
|
|
var authoredCamera =
|
|
authoredCameraObject.AddComponent<CinemachineCamera>();
|
|
outputCameraObject.AddComponent<Camera>();
|
|
var brain = outputCameraObject.AddComponent<CinemachineBrain>();
|
|
brain.UpdateMethod = CinemachineBrain.UpdateMethods.ManualUpdate;
|
|
|
|
var track = motionTimeline.CreateTrack<CinemachineTrack>(
|
|
null,
|
|
"Authored Camera Track");
|
|
var clip = track.CreateDefaultClip();
|
|
clip.start = 0d;
|
|
clip.duration = 1d;
|
|
var shot = clip.asset as CinemachineShot;
|
|
Assert.That(shot, Is.Not.Null);
|
|
var exposedName = new PropertyName("AuthoredCameraForSuppressionTest");
|
|
shot.VirtualCamera.exposedName = exposedName;
|
|
shot.VirtualCamera.defaultValue = authoredCamera;
|
|
motionDirector.SetReferenceValue(exposedName, authoredCamera);
|
|
motionDirector.SetGenericBinding(track, brain);
|
|
|
|
motionDirector.Play();
|
|
motionDirector.time = 0.25d;
|
|
motionDirector.Evaluate();
|
|
brain.ManualUpdate(100, 1f / 60f);
|
|
|
|
Assert.That(motionDirector.playableGraph.IsValid(), Is.True);
|
|
Assert.That(motionDirector.state, Is.EqualTo(PlayState.Playing));
|
|
Assert.That(brain.ActiveVirtualCamera, Is.SameAs(authoredCamera));
|
|
var suppressionState = AICameraTimelinePreviewImporter
|
|
.CaptureAuthoredCinemachineState(sourceDirector);
|
|
|
|
AICameraTimelinePreviewImporter
|
|
.SuppressAuthoredCinemachineOutputsForEditor(sourceDirector);
|
|
brain.ManualUpdate(101, 1f / 60f);
|
|
|
|
Assert.That(motionDirector.state, Is.EqualTo(PlayState.Playing));
|
|
Assert.That(motionDirector.time, Is.EqualTo(0.25d).Within(0.000001d));
|
|
Assert.That(motionDirector.GetGenericBinding(track), Is.Null);
|
|
Assert.That(authoredCamera.enabled, Is.False);
|
|
Assert.That(brain.ActiveVirtualCamera, Is.Not.SameAs(authoredCamera));
|
|
|
|
AICameraTimelinePreviewImporter.RestoreAuthoredCinemachineState(
|
|
suppressionState);
|
|
brain.ManualUpdate(102, 1f / 60f);
|
|
|
|
Assert.That(motionDirector.state, Is.EqualTo(PlayState.Playing));
|
|
Assert.That(motionDirector.time, Is.EqualTo(0.25d).Within(0.000001d));
|
|
Assert.That(
|
|
motionDirector.GetGenericBinding(track),
|
|
Is.SameAs(brain));
|
|
Assert.That(authoredCamera.enabled, Is.True);
|
|
Assert.That(brain.ActiveVirtualCamera, Is.SameAs(authoredCamera));
|
|
}
|
|
finally
|
|
{
|
|
if (motionDirector != null)
|
|
{
|
|
motionDirector.Stop();
|
|
}
|
|
|
|
Object.DestroyImmediate(sourceObject);
|
|
Object.DestroyImmediate(outputCameraObject);
|
|
Object.DestroyImmediate(motionTimeline);
|
|
Object.DestroyImmediate(sourceTimeline);
|
|
}
|
|
}
|
|
|
|
[TestCase(false)]
|
|
[TestCase(true)]
|
|
public void GeneratedDirectorySupportsLegacyAndDeclaredHashes(
|
|
bool includeHashes)
|
|
{
|
|
var directory = CreateGeneratedDirectory(includeHashes);
|
|
try
|
|
{
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.ValidateGeneratedDirectoryForCli(directory),
|
|
Does.Contain("1 frames, 1 shots at 60 FPS"));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
|
|
[TestCase("generated_camera_world.f32", "worldCamera")]
|
|
[TestCase("time.f64", "time")]
|
|
[TestCase("shots.json", "shots")]
|
|
public void GeneratedDirectoryRejectsTamperedDeclaredPayloadBeforeParsing(
|
|
string fileName,
|
|
string payloadName)
|
|
{
|
|
var directory = CreateGeneratedDirectory(true);
|
|
try
|
|
{
|
|
File.AppendAllText(
|
|
Path.Combine(directory, fileName),
|
|
"tampered",
|
|
new UTF8Encoding(false));
|
|
|
|
Assert.That(
|
|
() => AICameraTimelinePreviewImporter
|
|
.ValidateGeneratedDirectoryForCli(directory),
|
|
Throws.TypeOf<InvalidDataException>()
|
|
.With.Message.Contains(
|
|
$"{payloadName} payload SHA-256 mismatch"));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void GeneratedDirectoryRejectsAbsolutePayloadPath()
|
|
{
|
|
var directory = CreateGeneratedDirectory(true);
|
|
try
|
|
{
|
|
ReplaceMetadataPayloadPath(
|
|
directory,
|
|
"worldCameraFile",
|
|
Path.Combine(directory, "generated_camera_world.f32"));
|
|
|
|
Assert.That(
|
|
() => AICameraTimelinePreviewImporter
|
|
.ValidateGeneratedDirectoryForCli(directory),
|
|
Throws.TypeOf<InvalidDataException>()
|
|
.With.Message.Contains("must be relative"));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void GeneratedDirectoryRejectsParentTraversalEvenWhenHashMatches()
|
|
{
|
|
var directory = CreateGeneratedDirectory(true);
|
|
var outsidePath = Path.Combine(
|
|
Path.GetDirectoryName(directory) ?? Path.GetTempPath(),
|
|
$"cw_ai_outside_{Guid.NewGuid():N}.f32");
|
|
try
|
|
{
|
|
File.Copy(
|
|
Path.Combine(directory, "generated_camera_world.f32"),
|
|
outsidePath);
|
|
ReplaceMetadataPayloadPath(
|
|
directory,
|
|
"worldCameraFile",
|
|
"../" + Path.GetFileName(outsidePath));
|
|
|
|
Assert.That(
|
|
() => AICameraTimelinePreviewImporter
|
|
.ValidateGeneratedDirectoryForCli(directory),
|
|
Throws.TypeOf<InvalidDataException>()
|
|
.With.Message.Contains("invalid segment"));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(directory, true);
|
|
if (File.Exists(outsidePath))
|
|
{
|
|
File.Delete(outsidePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void GeneratedDirectoryRejectsExplicitNullHashObject()
|
|
{
|
|
var directory = CreateGeneratedDirectory(false);
|
|
try
|
|
{
|
|
var metadataPath = Path.Combine(directory, "metadata.json");
|
|
var json = File.ReadAllText(metadataPath);
|
|
File.WriteAllText(
|
|
metadataPath,
|
|
json.Substring(0, json.Length - 1) +
|
|
",\"outputSha256\":null}",
|
|
new UTF8Encoding(false));
|
|
|
|
Assert.That(
|
|
() => AICameraTimelinePreviewImporter
|
|
.ValidateGeneratedDirectoryForCli(directory),
|
|
Throws.TypeOf<InvalidDataException>()
|
|
.With.Message.Contains(
|
|
"metadata.outputSha256 must be a JSON object"));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void GeneratedDirectoryRejectsIncompleteDeclaredHashes()
|
|
{
|
|
var directory = CreateGeneratedDirectory(true);
|
|
try
|
|
{
|
|
var metadataPath = Path.Combine(directory, "metadata.json");
|
|
var timePath = Path.Combine(directory, "time.f64");
|
|
var json = File.ReadAllText(metadataPath).Replace(
|
|
$",\"time\":\"{ComputeSha256(timePath)}\"",
|
|
string.Empty);
|
|
File.WriteAllText(
|
|
metadataPath,
|
|
json,
|
|
new UTF8Encoding(false));
|
|
|
|
Assert.That(
|
|
() => AICameraTimelinePreviewImporter
|
|
.ValidateGeneratedDirectoryForCli(directory),
|
|
Throws.TypeOf<InvalidDataException>()
|
|
.With.Message.Contains(
|
|
"metadata.outputSha256.time is missing"));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(directory, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void PreviewProvenancePersistsHiddenRelativeAndAbsoluteFallback()
|
|
{
|
|
var cwAiRoot = Path.Combine(
|
|
Path.GetTempPath(),
|
|
$"cw_ai_provenance_root_{Guid.NewGuid():N}");
|
|
var generatedDirectory = Path.Combine(
|
|
cwAiRoot,
|
|
"examples",
|
|
"UnityGenerated",
|
|
"camera_test");
|
|
Directory.CreateDirectory(generatedDirectory);
|
|
File.WriteAllText(
|
|
Path.Combine(generatedDirectory, "metadata.json"),
|
|
"{\"generation\":1}",
|
|
new UTF8Encoding(false));
|
|
var timelinePath =
|
|
$"Assets/__CWAI_Provenance_{Guid.NewGuid():N}.playable";
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
try
|
|
{
|
|
AssetDatabase.CreateAsset(timeline, timelinePath);
|
|
var provenance = AICameraTimelinePreviewImporter
|
|
.UpsertPreviewGenerationProvenanceForTests(
|
|
timeline,
|
|
generatedDirectory,
|
|
cwAiRoot,
|
|
false);
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.ImportAsset(
|
|
timelinePath,
|
|
ImportAssetOptions.ForceSynchronousImport);
|
|
|
|
var reloaded = AssetDatabase.LoadAssetAtPath<TimelineAsset>(
|
|
timelinePath);
|
|
Assert.That(reloaded, Is.Not.Null);
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.TryGetPreviewGenerationProvenanceForTests(
|
|
reloaded,
|
|
out var info),
|
|
Is.True);
|
|
Assert.That(
|
|
info.CwAiRelativeGeneratedFolder,
|
|
Is.EqualTo("examples/UnityGenerated/camera_test"));
|
|
Assert.That(
|
|
info.AbsoluteGeneratedFolder,
|
|
Is.EqualTo(Path.GetFullPath(generatedDirectory)));
|
|
Assert.That(
|
|
info.MetadataSha256,
|
|
Is.EqualTo(ComputeSha256(
|
|
Path.Combine(generatedDirectory, "metadata.json"))));
|
|
Assert.That(
|
|
provenance.hideFlags,
|
|
Is.EqualTo(
|
|
HideFlags.HideInHierarchy |
|
|
HideFlags.HideInInspector));
|
|
}
|
|
finally
|
|
{
|
|
AssetDatabase.DeleteAsset(timelinePath);
|
|
Directory.Delete(cwAiRoot, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void PreviewProvenanceUndoRedoRestoresMaterializedBase()
|
|
{
|
|
var cwAiRoot = Path.Combine(
|
|
Path.GetTempPath(),
|
|
$"cw_ai_provenance_undo_{Guid.NewGuid():N}");
|
|
var first = Path.Combine(cwAiRoot, "outputs", "first");
|
|
var second = Path.Combine(cwAiRoot, "outputs", "second");
|
|
Directory.CreateDirectory(first);
|
|
Directory.CreateDirectory(second);
|
|
File.WriteAllText(
|
|
Path.Combine(first, "metadata.json"),
|
|
"{\"generation\":1}",
|
|
new UTF8Encoding(false));
|
|
File.WriteAllText(
|
|
Path.Combine(second, "metadata.json"),
|
|
"{\"generation\":2}",
|
|
new UTF8Encoding(false));
|
|
var timelinePath =
|
|
$"Assets/__CWAI_ProvenanceUndo_{Guid.NewGuid():N}.playable";
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
try
|
|
{
|
|
AssetDatabase.CreateAsset(timeline, timelinePath);
|
|
AICameraTimelinePreviewImporter
|
|
.UpsertPreviewGenerationProvenanceForTests(
|
|
timeline,
|
|
first,
|
|
cwAiRoot,
|
|
false);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
Undo.IncrementCurrentGroup();
|
|
var undoGroup = Undo.GetCurrentGroup();
|
|
Undo.SetCurrentGroupName("Test AI Camera Provenance Undo");
|
|
AICameraTimelinePreviewImporter
|
|
.UpsertPreviewGenerationProvenanceForTests(
|
|
timeline,
|
|
second,
|
|
cwAiRoot,
|
|
true);
|
|
Undo.CollapseUndoOperations(undoGroup);
|
|
Undo.FlushUndoRecordObjects();
|
|
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.TryGetPreviewGenerationProvenanceForTests(
|
|
timeline,
|
|
out var changed),
|
|
Is.True);
|
|
Assert.That(
|
|
changed.AbsoluteGeneratedFolder,
|
|
Is.EqualTo(Path.GetFullPath(second)));
|
|
|
|
Undo.PerformUndo();
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.TryGetPreviewGenerationProvenanceForTests(
|
|
timeline,
|
|
out var undone),
|
|
Is.True);
|
|
Assert.That(
|
|
undone.AbsoluteGeneratedFolder,
|
|
Is.EqualTo(Path.GetFullPath(first)));
|
|
|
|
Undo.PerformRedo();
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.TryGetPreviewGenerationProvenanceForTests(
|
|
timeline,
|
|
out var redone),
|
|
Is.True);
|
|
Assert.That(
|
|
redone.AbsoluteGeneratedFolder,
|
|
Is.EqualTo(Path.GetFullPath(second)));
|
|
}
|
|
finally
|
|
{
|
|
AssetDatabase.DeleteAsset(timelinePath);
|
|
Directory.Delete(cwAiRoot, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void FinalTimelineCleanupRemovesPreviewOnlyProvenanceSubasset()
|
|
{
|
|
var generatedDirectory = Path.Combine(
|
|
Path.GetTempPath(),
|
|
$"cw_ai_provenance_final_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(generatedDirectory);
|
|
File.WriteAllText(
|
|
Path.Combine(generatedDirectory, "metadata.json"),
|
|
"{\"generation\":1}",
|
|
new UTF8Encoding(false));
|
|
var timelinePath =
|
|
$"Assets/__CWAI_ProvenanceFinal_{Guid.NewGuid():N}.playable";
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
try
|
|
{
|
|
AssetDatabase.CreateAsset(timeline, timelinePath);
|
|
AICameraTimelinePreviewImporter
|
|
.UpsertPreviewGenerationProvenanceForTests(
|
|
timeline,
|
|
generatedDirectory,
|
|
string.Empty,
|
|
false);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
AICameraTimelinePreviewImporter
|
|
.RemovePreviewGenerationProvenanceForTests(timeline);
|
|
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.TryGetPreviewGenerationProvenanceForTests(
|
|
timeline,
|
|
out _),
|
|
Is.False);
|
|
Assert.That(
|
|
AssetDatabase.LoadAllAssetsAtPath(timelinePath)
|
|
.OfType<AICameraPreviewGenerationProvenance>(),
|
|
Is.Empty);
|
|
}
|
|
finally
|
|
{
|
|
AssetDatabase.DeleteAsset(timelinePath);
|
|
Directory.Delete(generatedDirectory, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ShotDisplayNameAndProvenanceRestoreInOneUndoGroup()
|
|
{
|
|
var root = Path.Combine(
|
|
Path.GetTempPath(),
|
|
$"cw_ai_track_undo_{Guid.NewGuid():N}");
|
|
var first = Path.Combine(root, "first");
|
|
var second = Path.Combine(root, "second");
|
|
Directory.CreateDirectory(first);
|
|
Directory.CreateDirectory(second);
|
|
File.WriteAllText(
|
|
Path.Combine(first, "metadata.json"),
|
|
"{\"generation\":1}");
|
|
File.WriteAllText(
|
|
Path.Combine(second, "metadata.json"),
|
|
"{\"generation\":2}");
|
|
var timelinePath =
|
|
$"Assets/__CWAI_TrackUndo_{Guid.NewGuid():N}.playable";
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
try
|
|
{
|
|
AssetDatabase.CreateAsset(timeline, timelinePath);
|
|
var track = timeline.CreateTrack<AnimationTrack>(null, "Shot");
|
|
var shot = track.CreateDefaultClip();
|
|
shot.displayName = "Before";
|
|
AICameraTimelinePreviewImporter
|
|
.UpsertPreviewGenerationProvenanceForTests(
|
|
timeline,
|
|
first,
|
|
root,
|
|
false);
|
|
EditorUtility.SetDirty(track);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
Undo.IncrementCurrentGroup();
|
|
var undoGroup = Undo.GetCurrentGroup();
|
|
Undo.SetCurrentGroupName("Test Shot and Provenance Undo");
|
|
Undo.RegisterCompleteObjectUndo(track, "Test Shot and Provenance Undo");
|
|
shot.displayName = "After";
|
|
AICameraTimelinePreviewImporter
|
|
.UpsertPreviewGenerationProvenanceForTests(
|
|
timeline,
|
|
second,
|
|
root,
|
|
true);
|
|
Undo.CollapseUndoOperations(undoGroup);
|
|
Undo.FlushUndoRecordObjects();
|
|
|
|
Undo.PerformUndo();
|
|
Assert.That(shot.displayName, Is.EqualTo("Before"));
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.TryGetPreviewGenerationProvenanceForTests(
|
|
timeline,
|
|
out var undone),
|
|
Is.True);
|
|
Assert.That(
|
|
undone.AbsoluteGeneratedFolder,
|
|
Is.EqualTo(Path.GetFullPath(first)));
|
|
|
|
Undo.PerformRedo();
|
|
Assert.That(shot.displayName, Is.EqualTo("After"));
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.TryGetPreviewGenerationProvenanceForTests(
|
|
timeline,
|
|
out var redone),
|
|
Is.True);
|
|
Assert.That(
|
|
redone.AbsoluteGeneratedFolder,
|
|
Is.EqualTo(Path.GetFullPath(second)));
|
|
}
|
|
finally
|
|
{
|
|
AssetDatabase.DeleteAsset(timelinePath);
|
|
Directory.Delete(root, true);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void GeneratedMotionUsesOneAnimatorAndTrackPerCamera()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var cameraRoot = new GameObject("AI Camera Root");
|
|
var firstCamera = new GameObject("Shot_000");
|
|
var secondCamera = new GameObject("Shot_001");
|
|
firstCamera.transform.SetParent(cameraRoot.transform, false);
|
|
secondCamera.transform.SetParent(cameraRoot.transform, false);
|
|
firstCamera.transform.SetLocalPositionAndRotation(
|
|
new Vector3(1.25f, 1.8f, -3.5f),
|
|
Quaternion.Euler(8f, 135f, 0f));
|
|
secondCamera.transform.SetLocalPositionAndRotation(
|
|
new Vector3(-2f, 0.9f, 6f),
|
|
Quaternion.Euler(-4f, 20f, 2f));
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
var firstClip = new AnimationClip();
|
|
var secondClip = new AnimationClip();
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
director.playableAsset = timeline;
|
|
var group = timeline.CreateTrack<GroupTrack>(
|
|
null,
|
|
AICameraTimelinePreviewImporter.GeneratedAnimationTrackName);
|
|
|
|
var firstTrack = AICameraTimelinePreviewImporter
|
|
.CreateGeneratedAnimationTrackForCamera(
|
|
timeline,
|
|
group,
|
|
director,
|
|
firstCamera,
|
|
firstClip,
|
|
0,
|
|
firstCamera.name,
|
|
0,
|
|
1);
|
|
var secondTrack = AICameraTimelinePreviewImporter
|
|
.CreateGeneratedAnimationTrackForCamera(
|
|
timeline,
|
|
group,
|
|
director,
|
|
secondCamera,
|
|
secondClip,
|
|
1,
|
|
secondCamera.name,
|
|
1,
|
|
2);
|
|
|
|
var firstAnimator = firstCamera.GetComponent<Animator>();
|
|
var secondAnimator = secondCamera.GetComponent<Animator>();
|
|
Assert.That(cameraRoot.GetComponent<Animator>(), Is.Null);
|
|
Assert.That(firstAnimator, Is.Not.Null);
|
|
Assert.That(secondAnimator, Is.Not.Null);
|
|
Assert.That(firstAnimator, Is.Not.SameAs(secondAnimator));
|
|
Assert.That(
|
|
firstAnimator.cullingMode,
|
|
Is.EqualTo(AnimatorCullingMode.AlwaysAnimate));
|
|
Assert.That(
|
|
secondAnimator.cullingMode,
|
|
Is.EqualTo(AnimatorCullingMode.AlwaysAnimate));
|
|
Assert.That(director.GetGenericBinding(firstTrack), Is.SameAs(firstAnimator));
|
|
Assert.That(director.GetGenericBinding(secondTrack), Is.SameAs(secondAnimator));
|
|
Assert.That(firstTrack.parent, Is.SameAs(group));
|
|
Assert.That(secondTrack.parent, Is.SameAs(group));
|
|
Assert.That(
|
|
firstTrack.position,
|
|
Is.EqualTo(firstCamera.transform.localPosition));
|
|
Assert.That(
|
|
secondTrack.position,
|
|
Is.EqualTo(secondCamera.transform.localPosition));
|
|
Assert.That(
|
|
Quaternion.Angle(
|
|
firstTrack.rotation,
|
|
firstCamera.transform.localRotation),
|
|
Is.LessThan(0.001f));
|
|
Assert.That(
|
|
Quaternion.Angle(
|
|
secondTrack.rotation,
|
|
secondCamera.transform.localRotation),
|
|
Is.LessThan(0.001f));
|
|
Assert.That(
|
|
System.Linq.Enumerable.Count(firstTrack.GetClips()),
|
|
Is.EqualTo(1));
|
|
Assert.That(
|
|
System.Linq.Enumerable.Count(secondTrack.GetClips()),
|
|
Is.EqualTo(1));
|
|
StringAssert.StartsWith(
|
|
AICameraTimelinePreviewImporter.GeneratedAnimationTrackPrefix,
|
|
firstTrack.name);
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(cameraRoot);
|
|
Object.DestroyImmediate(firstClip);
|
|
Object.DestroyImmediate(secondClip);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ShotSelectionResolvesTimelineClipAssetAndCamera()
|
|
{
|
|
var directorObject = new GameObject("Director");
|
|
var cameraRoot = new GameObject("AI Camera Root");
|
|
var firstCamera = new GameObject("Shot_000");
|
|
var secondCamera = new GameObject("Shot_001");
|
|
firstCamera.transform.SetParent(cameraRoot.transform, false);
|
|
secondCamera.transform.SetParent(cameraRoot.transform, false);
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
var firstClip = new AnimationClip();
|
|
var secondClip = new AnimationClip();
|
|
try
|
|
{
|
|
var director = directorObject.AddComponent<PlayableDirector>();
|
|
director.playableAsset = timeline;
|
|
var group = timeline.CreateTrack<GroupTrack>(
|
|
null,
|
|
AICameraTimelinePreviewImporter.GeneratedAnimationTrackName);
|
|
AICameraTimelinePreviewImporter
|
|
.CreateGeneratedAnimationTrackForCamera(
|
|
timeline,
|
|
group,
|
|
director,
|
|
firstCamera,
|
|
firstClip,
|
|
0,
|
|
firstCamera.name,
|
|
0,
|
|
1);
|
|
var secondTrack = AICameraTimelinePreviewImporter
|
|
.CreateGeneratedAnimationTrackForCamera(
|
|
timeline,
|
|
group,
|
|
director,
|
|
secondCamera,
|
|
secondClip,
|
|
1,
|
|
secondCamera.name,
|
|
1,
|
|
2);
|
|
var selectedTimelineClip = System.Linq.Enumerable.Single(
|
|
secondTrack.GetClips());
|
|
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.TryResolveGeneratedShotIndexForEditor(
|
|
timeline,
|
|
cameraRoot,
|
|
selectedTimelineClip,
|
|
null,
|
|
null,
|
|
out var timelineIndex),
|
|
Is.True);
|
|
Assert.That(timelineIndex, Is.EqualTo(1));
|
|
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.TryResolveGeneratedShotIndexForEditor(
|
|
timeline,
|
|
cameraRoot,
|
|
null,
|
|
null,
|
|
secondClip,
|
|
out var assetIndex),
|
|
Is.True);
|
|
Assert.That(assetIndex, Is.EqualTo(1));
|
|
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.TryResolveGeneratedShotIndexForEditor(
|
|
timeline,
|
|
cameraRoot,
|
|
null,
|
|
firstCamera,
|
|
null,
|
|
out var cameraIndex),
|
|
Is.True);
|
|
Assert.That(cameraIndex, Is.EqualTo(0));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(directorObject);
|
|
Object.DestroyImmediate(cameraRoot);
|
|
Object.DestroyImmediate(firstClip);
|
|
Object.DestroyImmediate(secondClip);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void LegacySharedAnimationTrackStillResolvesShots()
|
|
{
|
|
var cameraRoot = new GameObject("Legacy AI Camera Root");
|
|
var firstCamera = new GameObject("Shot_000");
|
|
var secondCamera = new GameObject("Shot_001");
|
|
firstCamera.transform.SetParent(cameraRoot.transform, false);
|
|
secondCamera.transform.SetParent(cameraRoot.transform, false);
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
var firstClip = new AnimationClip();
|
|
var secondClip = new AnimationClip();
|
|
try
|
|
{
|
|
var track = timeline.CreateTrack<AnimationTrack>(
|
|
null,
|
|
AICameraTimelinePreviewImporter.GeneratedAnimationTrackName);
|
|
var firstTimelineClip = track.CreateClip(firstClip);
|
|
firstTimelineClip.start = 0;
|
|
firstTimelineClip.duration = 1;
|
|
var secondTimelineClip = track.CreateClip(secondClip);
|
|
secondTimelineClip.start = 1;
|
|
secondTimelineClip.duration = 1;
|
|
|
|
Assert.That(
|
|
AICameraTimelinePreviewImporter
|
|
.TryResolveGeneratedShotIndexForEditor(
|
|
timeline,
|
|
cameraRoot,
|
|
secondTimelineClip,
|
|
null,
|
|
null,
|
|
out var shotIndex),
|
|
Is.True);
|
|
Assert.That(shotIndex, Is.EqualTo(1));
|
|
}
|
|
finally
|
|
{
|
|
Object.DestroyImmediate(cameraRoot);
|
|
Object.DestroyImmediate(firstClip);
|
|
Object.DestroyImmediate(secondClip);
|
|
Object.DestroyImmediate(timeline);
|
|
}
|
|
}
|
|
|
|
private static TimelineAsset CreateTimelineBoundToBrain(
|
|
PlayableDirector director,
|
|
CinemachineBrain brain,
|
|
string trackName)
|
|
{
|
|
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
|
|
var track = timeline.CreateTrack<CinemachineTrack>(null, trackName);
|
|
director.playableAsset = timeline;
|
|
director.SetGenericBinding(track, brain);
|
|
return timeline;
|
|
}
|
|
|
|
private static string CreateGeneratedDirectory(bool includeHashes)
|
|
{
|
|
var directory = Path.Combine(
|
|
Path.GetTempPath(),
|
|
$"cw_ai_importer_test_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(directory);
|
|
var cameraPath = Path.Combine(
|
|
directory,
|
|
"generated_camera_world.f32");
|
|
var timePath = Path.Combine(directory, "time.f64");
|
|
var shotsPath = Path.Combine(directory, "shots.json");
|
|
using (var writer = new BinaryWriter(File.Create(cameraPath)))
|
|
{
|
|
writer.Write(0f);
|
|
writer.Write(1.5f);
|
|
writer.Write(-3f);
|
|
writer.Write(0f);
|
|
writer.Write(0f);
|
|
writer.Write(0f);
|
|
writer.Write(1f);
|
|
writer.Write(60f);
|
|
writer.Write(0f);
|
|
}
|
|
|
|
using (var writer = new BinaryWriter(File.Create(timePath)))
|
|
{
|
|
writer.Write(0d);
|
|
}
|
|
|
|
File.WriteAllText(
|
|
shotsPath,
|
|
"{\"shots\":[{\"index\":0,\"cameraName\":\"Shot_000\"," +
|
|
"\"shotType\":\"medium\",\"startFrame\":0," +
|
|
"\"endFrameExclusive\":1}]}",
|
|
new UTF8Encoding(false));
|
|
|
|
var hashes = includeHashes
|
|
? ",\"outputSha256\":{" +
|
|
$"\"worldCamera\":\"{ComputeSha256(cameraPath)}\"," +
|
|
$"\"time\":\"{ComputeSha256(timePath)}\"," +
|
|
$"\"shots\":\"{ComputeSha256(shotsPath)}\"}}"
|
|
: string.Empty;
|
|
File.WriteAllText(
|
|
Path.Combine(directory, "metadata.json"),
|
|
"{\"songName\":\"Hash Validation Test\",\"sampleRate\":60," +
|
|
"\"frameCount\":1,\"duration\":0.016666666666666666," +
|
|
"\"worldCameraFile\":\"generated_camera_world.f32\"," +
|
|
"\"timeFile\":\"time.f64\",\"shotsFile\":\"shots.json\"," +
|
|
$"\"lookAtUsed\":false{hashes}}}",
|
|
new UTF8Encoding(false));
|
|
return directory;
|
|
}
|
|
|
|
private static void ReplaceMetadataPayloadPath(
|
|
string directory,
|
|
string propertyName,
|
|
string replacementPath)
|
|
{
|
|
var metadataPath = Path.Combine(directory, "metadata.json");
|
|
var json = File.ReadAllText(metadataPath);
|
|
var marker = $"\"{propertyName}\":\"";
|
|
var valueStart = json.IndexOf(marker, StringComparison.Ordinal);
|
|
Assert.That(valueStart, Is.GreaterThanOrEqualTo(0));
|
|
valueStart += marker.Length;
|
|
var valueEnd = json.IndexOf('"', valueStart);
|
|
Assert.That(valueEnd, Is.GreaterThan(valueStart));
|
|
var escapedReplacement = replacementPath
|
|
.Replace("\\", "\\\\")
|
|
.Replace("\"", "\\\"");
|
|
json = json.Substring(0, valueStart) +
|
|
escapedReplacement +
|
|
json.Substring(valueEnd);
|
|
File.WriteAllText(
|
|
metadataPath,
|
|
json,
|
|
new UTF8Encoding(false));
|
|
}
|
|
|
|
private static string ComputeSha256(string path)
|
|
{
|
|
using var algorithm = SHA256.Create();
|
|
return string.Concat(
|
|
algorithm.ComputeHash(File.ReadAllBytes(path))
|
|
.Select(value => value.ToString("x2")));
|
|
}
|
|
}
|
|
}
|