release(camera-ai): publish 0.4.14 importer hotfix

This commit is contained in:
KINDNICK 2026-08-18 00:00:12 +09:00
parent 9cfdf3d947
commit 150ef2d30c
7 changed files with 1279 additions and 25 deletions

View File

@ -1,5 +1,17 @@
# Changelog # Changelog
## 0.4.14 - 2026-08-17
- Resolved the configured CinemachineBrain from camera tracks on descendant
PlayableDirectors, including Cinemachine 3 `OutputCamera` setups, so
orchestration Timelines can import AI results without duplicating camera
tracks on the parent Timeline.
- Made AI preview cloning inactive from construction and disconnected cloned
authored Cinemachine tracks and virtual cameras before playback. This
prevents the authored Motion Timeline from competing with the generated
Shot tracks while preserving the original scene hierarchy. This is an
Editor-only hotfix; the bundled Windows worker remains 0.1.9.
## 0.4.13 - 2026-08-16 ## 0.4.13 - 2026-08-16
- Enforced radial distance bounds on the actual float32 camera coordinates, - Enforced radial distance bounds on the actual float32 camera coordinates,

View File

@ -17,7 +17,7 @@ must remain together.
In Unity Package Manager, choose **Add package from git URL** and enter: In Unity Package Manager, choose **Add package from git URL** and enter:
```text ```text
https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.19 https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.20
``` ```
The initial package download is large because the frozen Windows worker is The initial package download is large because the frozen Windows worker is
@ -26,7 +26,7 @@ it, remove the package lock entry, and add the package again.
## Reference data ## Reference data
Version 0.1.19 includes a compact, read-only `RuntimeData~` bundle with the Version 0.1.20 includes a compact, read-only `RuntimeData~` bundle with the
263 prepared reference songs, cut policy, and ranker model. An artist 263 prepared reference songs, cut policy, and ranker model. An artist
workstation does not need a separate `CW-AI` checkout or Python installation. workstation does not need a separate `CW-AI` checkout or Python installation.
If a newer access-controlled library is available, it remains an optional If a newer access-controlled library is available, it remains an optional

View File

@ -440,7 +440,7 @@ namespace Streamingle.Editor
originalDirector.gameObject.activeSelf; originalDirector.gameObject.activeSelf;
try try
{ {
previewDirectorObject = UnityEngine.Object.Instantiate( previewDirectorObject = InstantiateInactivePreviewCloneForEditor(
originalDirector.gameObject, originalDirector.gameObject,
originalDirector.transform.parent); originalDirector.transform.parent);
Undo.RegisterCreatedObjectUndo( Undo.RegisterCreatedObjectUndo(
@ -456,6 +456,7 @@ namespace Streamingle.Editor
originalTimeline, originalTimeline,
previewDirector, previewDirector,
copiedTimeline); copiedTimeline);
SuppressAuthoredCinemachineOutputsForEditor(previewDirector);
MuteCopiedCameraTracks(previewDirector, copiedTimeline); MuteCopiedCameraTracks(previewDirector, copiedTimeline);
MuteCopiedRecorderTracks(copiedTimeline); MuteCopiedRecorderTracks(copiedTimeline);
@ -595,6 +596,7 @@ namespace Streamingle.Editor
$"Position Dense C2 Fallbacks: {positionDenseFallbackCount:N0}", $"Position Dense C2 Fallbacks: {positionDenseFallbackCount:N0}",
$"Preview Director: {GetHierarchyPath(previewDirector.transform)}", $"Preview Director: {GetHierarchyPath(previewDirector.transform)}",
$"Preview Camera Root: {GetHierarchyPath(previewCameraObject.transform)}", $"Preview Camera Root: {GetHierarchyPath(previewCameraObject.transform)}",
GetPreviewOutputStatusForEditor(brain),
$"Timeline Asset: {copiedTimelinePath}", $"Timeline Asset: {copiedTimelinePath}",
$"Animation Assets: {generatedClipPaths.Count:N0} " + $"Animation Assets: {generatedClipPaths.Count:N0} " +
$"under {shotAnimationFolder}", $"under {shotAnimationFolder}",
@ -1449,6 +1451,8 @@ namespace Streamingle.Editor
TimelineAsset finalTimeline = null; TimelineAsset finalTimeline = null;
GameObject cameraPrefab = null; GameObject cameraPrefab = null;
GameObject finalCameraRoot = null; GameObject finalCameraRoot = null;
AuthoredCinemachineStateSnapshot destinationCinemachineStateBefore =
null;
try try
{ {
var sourceTimelinePath = AssetDatabase.GetAssetPath(sourceTimeline); var sourceTimelinePath = AssetDatabase.GetAssetPath(sourceTimeline);
@ -1511,6 +1515,11 @@ namespace Streamingle.Editor
destinationTimelineBefore, destinationTimelineBefore,
finalTimeline, finalTimeline,
finalCameraRoot); finalCameraRoot);
destinationCinemachineStateBefore =
CaptureAuthoredCinemachineState(destinationDirector);
SuppressAuthoredCinemachineOutputsForEditor(
destinationDirector,
false);
destinationDirector.time = destinationTimeBefore; destinationDirector.time = destinationTimeBefore;
destinationDirector.RebuildGraph(); destinationDirector.RebuildGraph();
destinationDirector.time = destinationTimeBefore; destinationDirector.time = destinationTimeBefore;
@ -1545,6 +1554,8 @@ namespace Streamingle.Editor
} }
catch catch
{ {
RestoreAuthoredCinemachineState(
destinationCinemachineStateBefore);
RestoreDestinationDirectorAfterFinalizationFailure( RestoreDestinationDirectorAfterFinalizationFailure(
destinationStateBefore, destinationStateBefore,
finalTimeline); finalTimeline);
@ -1943,6 +1954,7 @@ namespace Streamingle.Editor
destinationDirector.SetReferenceValue(exposedName, camera); destinationDirector.SetReferenceValue(exposedName, camera);
EditorUtility.SetDirty(shot); EditorUtility.SetDirty(shot);
} }
} }
internal static void ClearTimelineBindingsForFinalization( internal static void ClearTimelineBindingsForFinalization(
@ -5575,6 +5587,46 @@ namespace Streamingle.Editor
return clonedComponents[index]; return clonedComponents[index];
} }
internal static GameObject InstantiateInactivePreviewCloneForEditor(
GameObject sourceObject,
Transform destinationParent)
{
if (sourceObject == null)
{
throw new ArgumentNullException(nameof(sourceObject));
}
var sourceScene = sourceObject.scene;
if (!sourceScene.IsValid() || !sourceScene.isLoaded)
{
throw new InvalidOperationException(
"The source Timeline object must belong to a loaded scene.");
}
var stagingObject = new GameObject(
"__CWAI_InactivePreviewCloneStaging");
stagingObject.SetActive(false);
try
{
if (stagingObject.scene != sourceScene)
{
SceneManager.MoveGameObjectToScene(stagingObject, sourceScene);
}
var clone = UnityEngine.Object.Instantiate(
sourceObject,
stagingObject.transform,
true);
clone.SetActive(false);
clone.transform.SetParent(destinationParent, true);
return clone;
}
finally
{
UnityEngine.Object.DestroyImmediate(stagingObject);
}
}
private static void EnsureNoExistingPreview( private static void EnsureNoExistingPreview(
Scene scene, Scene scene,
string previewDirectorName, string previewDirectorName,
@ -5645,30 +5697,49 @@ namespace Streamingle.Editor
} }
var scene = director.gameObject.scene; var scene = director.gameObject.scene;
var boundBrains = timeline.GetOutputTracks() var directlyBoundBrains = GetBoundCinemachineBrains(
.OfType<CinemachineTrack>() director,
.Select(track => director.GetGenericBinding(track) as CinemachineBrain) timeline);
.Where(brain => brain != null) if (directlyBoundBrains.Length > 1)
.Distinct()
.ToArray();
if (boundBrains.Length > 1)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"The Timeline has Cinemachine Tracks bound to multiple " + "The Timeline has Cinemachine Tracks bound to multiple " +
"CinemachineBrain components. Bind every camera track to one Brain."); "CinemachineBrain components. Bind every camera track to one Brain.");
} }
if (boundBrains.Length == 1) if (directlyBoundBrains.Length == 1)
{ {
var boundBrain = boundBrains[0]; return ValidateExplicitCinemachineBrain(
if (!IsUsableCinemachineBrain(boundBrain, scene)) directlyBoundBrains[0],
{ scene);
throw new InvalidOperationException( }
"The CinemachineBrain bound to the Timeline must be enabled, " +
"attached to an enabled Camera, and belong to the Timeline scene.");
}
return boundBrain; var descendantBoundBrains = director.gameObject
.GetComponentsInChildren<PlayableDirector>(true)
.Where(descendant =>
descendant != null &&
descendant != director &&
descendant.gameObject.scene == scene &&
descendant.playableAsset is TimelineAsset)
.SelectMany(descendant => GetBoundCinemachineBrains(
descendant,
(TimelineAsset)descendant.playableAsset))
.Distinct()
.ToArray();
if (descendantBoundBrains.Length > 1)
{
throw new InvalidOperationException(
"Descendant PlayableDirectors under the selected Timeline object " +
"have Cinemachine Tracks bound to multiple CinemachineBrain " +
"components. Bind them to one Brain or bind the selected Timeline " +
"directly.");
}
if (descendantBoundBrains.Length == 1)
{
return ValidateExplicitCinemachineBrain(
descendantBoundBrains[0],
scene);
} }
var usableBrains = sceneBrains var usableBrains = sceneBrains
@ -5678,7 +5749,7 @@ namespace Streamingle.Editor
var mainCameraBrains = usableBrains var mainCameraBrains = usableBrains
.Where(brain => .Where(brain =>
{ {
var camera = brain.GetComponent<Camera>(); var camera = brain.OutputCamera;
return camera != null && camera.CompareTag("MainCamera"); return camera != null && camera.CompareTag("MainCamera");
}) })
.ToArray(); .ToArray();
@ -5714,6 +5785,81 @@ namespace Streamingle.Editor
"one Camera as MainCamera or bind a Cinemachine Track explicitly."); "one Camera as MainCamera or bind a Cinemachine Track explicitly.");
} }
private static CinemachineBrain[] GetBoundCinemachineBrains(
PlayableDirector director,
TimelineAsset timeline)
{
return timeline.GetOutputTracks()
.OfType<CinemachineTrack>()
.Select(track =>
director.GetGenericBinding(track) as CinemachineBrain)
.Where(brain => brain != null)
.Distinct()
.ToArray();
}
private static CinemachineBrain ValidateExplicitCinemachineBrain(
CinemachineBrain brain,
Scene scene)
{
if (brain.gameObject.scene != scene)
{
throw new InvalidOperationException(
"The CinemachineBrain explicitly bound to a Timeline must belong " +
"to the same scene as the selected Timeline director.");
}
var camera = brain.OutputCamera;
if (camera == null)
{
throw new InvalidOperationException(
"The CinemachineBrain explicitly bound to a Timeline must have an " +
"output Camera component on its controlled GameObject.");
}
if (camera.gameObject.scene != scene)
{
throw new InvalidOperationException(
"The CinemachineBrain output Camera explicitly bound to a Timeline " +
"must belong to the same scene as the selected Timeline director.");
}
if (!brain.isActiveAndEnabled || !camera.isActiveAndEnabled)
{
Debug.LogWarning(
"The CinemachineBrain explicitly bound to a Timeline or its " +
"Camera is disabled. Camera AI will use the configured Brain " +
"without enabling either component automatically.",
brain);
}
return brain;
}
internal static string GetPreviewOutputStatusForEditor(
CinemachineBrain brain)
{
if (brain == null)
{
throw new ArgumentNullException(nameof(brain));
}
var camera = brain.OutputCamera;
var cameraName = camera != null
? camera.gameObject.name
: brain.gameObject.name;
if (camera != null &&
brain.isActiveAndEnabled &&
camera.isActiveAndEnabled)
{
return $"Preview Output: configured '{cameraName}' CinemachineBrain " +
"and Camera are active.";
}
return $"Preview Output: configured '{cameraName}' CinemachineBrain or " +
"Camera is disabled; enable it to see AI camera playback.";
}
private static bool IsUsableCinemachineBrain( private static bool IsUsableCinemachineBrain(
CinemachineBrain brain, CinemachineBrain brain,
Scene scene) Scene scene)
@ -5725,8 +5871,193 @@ namespace Streamingle.Editor
return false; return false;
} }
var camera = brain.GetComponent<Camera>(); var camera = brain.OutputCamera;
return camera != null && camera.isActiveAndEnabled; return camera != null &&
camera.gameObject.scene == scene &&
camera.isActiveAndEnabled;
}
internal static void SuppressAuthoredCinemachineOutputsForEditor(
PlayableDirector selectedDirector,
bool recordUndo = true)
{
if (selectedDirector == null)
{
throw new ArgumentNullException(nameof(selectedDirector));
}
var directorObject = selectedDirector.gameObject;
var descendantDirectors = directorObject
.GetComponentsInChildren<PlayableDirector>(true)
.Where(descendant =>
descendant != null &&
descendant != selectedDirector &&
descendant.playableAsset is TimelineAsset)
.ToArray();
foreach (var descendantDirector in descendantDirectors)
{
var descendantTimeline =
(TimelineAsset)descendantDirector.playableAsset;
var cinemachineTracks = descendantTimeline.GetOutputTracks()
.OfType<CinemachineTrack>()
.ToArray();
if (cinemachineTracks.Length == 0)
{
continue;
}
if (recordUndo)
{
Undo.RecordObject(
descendantDirector,
"Disconnect Authored Cameras From AI Preview");
}
foreach (var cinemachineTrack in cinemachineTracks)
{
descendantDirector.ClearGenericBinding(cinemachineTrack);
}
RebuildDirectorGraphPreservingPlayback(descendantDirector);
EditorUtility.SetDirty(descendantDirector);
}
foreach (var authoredCamera in directorObject
.GetComponentsInChildren<CinemachineVirtualCameraBase>(true))
{
if (authoredCamera == null || !authoredCamera.enabled)
{
continue;
}
if (recordUndo)
{
Undo.RecordObject(
authoredCamera,
"Disable Authored Cameras In AI Output");
}
authoredCamera.enabled = false;
EditorUtility.SetDirty(authoredCamera);
}
}
internal static AuthoredCinemachineStateSnapshot
CaptureAuthoredCinemachineState(PlayableDirector selectedDirector)
{
if (selectedDirector == null)
{
throw new ArgumentNullException(nameof(selectedDirector));
}
var directorObject = selectedDirector.gameObject;
var directorStates = directorObject
.GetComponentsInChildren<PlayableDirector>(true)
.Where(descendant =>
descendant != null &&
descendant != selectedDirector &&
descendant.playableAsset is TimelineAsset)
.Select(descendant =>
{
var timeline = (TimelineAsset)descendant.playableAsset;
var tracks = timeline.GetOutputTracks()
.OfType<CinemachineTrack>()
.ToArray();
return new AuthoredCinemachineDirectorState(
descendant,
tracks,
tracks.Select(descendant.GetGenericBinding).ToArray());
})
.Where(state => state.Tracks.Length > 0)
.ToArray();
var cameraStates = directorObject
.GetComponentsInChildren<CinemachineVirtualCameraBase>(true)
.Where(camera => camera != null)
.Select(camera => new AuthoredVirtualCameraState(
camera,
camera.enabled))
.ToArray();
return new AuthoredCinemachineStateSnapshot(
directorStates,
cameraStates);
}
internal static void RestoreAuthoredCinemachineState(
AuthoredCinemachineStateSnapshot snapshot)
{
if (snapshot == null)
{
return;
}
foreach (var cameraState in snapshot.CameraStates)
{
if (cameraState.Camera == null)
{
continue;
}
cameraState.Camera.enabled = cameraState.Enabled;
EditorUtility.SetDirty(cameraState.Camera);
}
foreach (var directorState in snapshot.DirectorStates)
{
var director = directorState.Director;
if (director == null)
{
continue;
}
for (var index = 0; index < directorState.Tracks.Length; index++)
{
var track = directorState.Tracks[index];
if (track == null)
{
continue;
}
var binding = directorState.Bindings[index];
if (binding != null)
{
director.SetGenericBinding(track, binding);
}
else
{
director.ClearGenericBinding(track);
}
}
RebuildDirectorGraphPreservingPlayback(director);
EditorUtility.SetDirty(director);
}
}
private static void RebuildDirectorGraphPreservingPlayback(
PlayableDirector director)
{
if (!director.playableGraph.IsValid())
{
return;
}
var time = director.time;
var wasPlaying = director.state == PlayState.Playing;
director.Stop();
director.RebuildGraph();
director.time = time;
if (wasPlaying)
{
director.Play();
director.time = time;
}
else
{
director.Evaluate();
director.time = time;
}
} }
private static void CopyTrackBindings( private static void CopyTrackBindings(
@ -6944,6 +7275,51 @@ namespace Streamingle.Editor
public GameObject CameraRoot { get; } public GameObject CameraRoot { get; }
} }
internal sealed class AuthoredCinemachineStateSnapshot
{
public AuthoredCinemachineStateSnapshot(
AuthoredCinemachineDirectorState[] directorStates,
AuthoredVirtualCameraState[] cameraStates)
{
DirectorStates = directorStates;
CameraStates = cameraStates;
}
public AuthoredCinemachineDirectorState[] DirectorStates { get; }
public AuthoredVirtualCameraState[] CameraStates { get; }
}
internal sealed class AuthoredCinemachineDirectorState
{
public AuthoredCinemachineDirectorState(
PlayableDirector director,
CinemachineTrack[] tracks,
UnityEngine.Object[] bindings)
{
Director = director;
Tracks = tracks;
Bindings = bindings;
}
public PlayableDirector Director { get; }
public CinemachineTrack[] Tracks { get; }
public UnityEngine.Object[] Bindings { get; }
}
internal sealed class AuthoredVirtualCameraState
{
public AuthoredVirtualCameraState(
CinemachineVirtualCameraBase camera,
bool enabled)
{
Camera = camera;
Enabled = enabled;
}
public CinemachineVirtualCameraBase Camera { get; }
public bool Enabled { get; }
}
private sealed class DirectorStateSnapshot private sealed class DirectorStateSnapshot
{ {
public DirectorStateSnapshot( public DirectorStateSnapshot(

View File

@ -16,7 +16,7 @@ Cinemachine Track이나 기존 카메라 애니메이션은 필요하지 않으
Unity Package Manager의 `Add package from git URL`에는 다음 주소를 사용할 수 Unity Package Manager의 `Add package from git URL`에는 다음 주소를 사용할 수
있습니다. 있습니다.
`https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.19` `https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.20`
배포 패키지에는 Windows x64용 `CWCameraWorker` 폴더 전체가 포함됩니다. 배포 패키지에는 Windows x64용 `CWCameraWorker` 폴더 전체가 포함됩니다.
Python은 따로 설치하지 않아도 되지만, Git 패키지의 대용량 바이너리를 받으려면 Python은 따로 설치하지 않아도 되지만, Git 패키지의 대용량 바이너리를 받으려면

View File

@ -6,13 +6,26 @@ using System.Text;
using NUnit.Framework; using NUnit.Framework;
using Unity.Cinemachine; using Unity.Cinemachine;
using UnityEditor; using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine; using UnityEngine;
using UnityEngine.Playables; using UnityEngine.Playables;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using UnityEngine.Timeline; using UnityEngine.Timeline;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
namespace Streamingle.Editor.Tests namespace Streamingle.Editor.Tests
{ {
public sealed class PreviewCloneActivationProbe : MonoBehaviour
{
internal static int EnableCount { get; set; }
private void OnEnable()
{
EnableCount++;
}
}
public sealed class AICameraTimelinePreviewImporterTests public sealed class AICameraTimelinePreviewImporterTests
{ {
[Test] [Test]
@ -34,6 +47,10 @@ namespace Streamingle.Editor.Tests
timeline, timeline,
new[] { brain }), new[] { brain }),
Is.SameAs(brain)); Is.SameAs(brain));
Assert.That(
AICameraTimelinePreviewImporter
.GetPreviewOutputStatusForEditor(brain),
Does.Contain("are active"));
} }
finally finally
{ {
@ -111,6 +128,843 @@ namespace Streamingle.Editor.Tests
} }
} }
[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(false)]
[TestCase(true)] [TestCase(true)]
public void GeneratedDirectorySupportsLegacyAndDeclaredHashes( public void GeneratedDirectorySupportsLegacyAndDeclaredHashes(
@ -778,6 +1632,18 @@ namespace Streamingle.Editor.Tests
} }
} }
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) private static string CreateGeneratedDirectory(bool includeHashes)
{ {
var directory = Path.Combine( var directory = Path.Combine(

View File

@ -1,6 +1,6 @@
{ {
"name": "com.mingle.cw-ai", "name": "com.mingle.cw-ai",
"version": "0.4.13", "version": "0.4.14",
"displayName": "Mingle Camera Work AI", "displayName": "Mingle Camera Work AI",
"description": "Self-contained high-quality Unity Timeline camera generation with an embedded prepared reference library, per-shot editable clips, and A/B review tools.", "description": "Self-contained high-quality Unity Timeline camera generation with an embedded prepared reference library, per-shot editable clips, and A/B review tools.",
"unity": "6000.0", "unity": "6000.0",

View File

@ -37,7 +37,7 @@ Install **Git and Git LFS before opening Unity**, then use **Add package from
git URL** with: git URL** with:
```text ```text
https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.19 https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.20
``` ```
The Camera AI package includes the complete Windows x64 The Camera AI package includes the complete Windows x64