fix(camera-ai): smooth generated camera curves
This commit is contained in:
parent
5a2e95485e
commit
b78c6319b6
@ -1,5 +1,28 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.4.10 - 2026-08-09
|
||||||
|
|
||||||
|
- Replaced the loose first-passing angular limiter with an authored-target
|
||||||
|
minimum-jerk selector. The Arisa regression output keeps all 69 music-shaped
|
||||||
|
cuts while angular jerk P90 falls from about 852 to 235 deg/s^3 and P99 from
|
||||||
|
about 4,455 to 912 deg/s^3.
|
||||||
|
- Made final float32 quaternion tracking authoritative, removed shot-entry
|
||||||
|
pre-echo, added a C2 semantic-focus dead zone, and recorded explicit angular
|
||||||
|
and dynamics fallback shot indices.
|
||||||
|
- Hardened translation rewrites and the final radial clamp so a repair cannot
|
||||||
|
hide or introduce a local acceleration/jerk seam. Intentional dolly and orbit
|
||||||
|
motion remains available.
|
||||||
|
- Baked position and quaternion animation as audited not-a-knot C2 curves
|
||||||
|
instead of piecewise-linear or C1-only curves. The importer verifies C2 seams,
|
||||||
|
240/480 Hz motion, quaternion continuity, and the post-Unity clip curves, with
|
||||||
|
adaptive key refinement and fail-closed dense C2 fallback.
|
||||||
|
- Resolved the active package through `FileUtil.GetPhysicalPath` so serialized
|
||||||
|
paths to an older `Library/PackageCache` revision can no longer keep running a
|
||||||
|
stale worker after a package update. Valid explicit external paths are kept.
|
||||||
|
- Rebuilt the self-contained Windows worker as 0.1.6 with Python 3.12.13. The
|
||||||
|
clean 647-file build passes `doctor`; camera, time, and shot outputs are
|
||||||
|
byte-identical to source generation for the 12,269-frame regression input.
|
||||||
|
|
||||||
## 0.4.9 - 2026-08-09
|
## 0.4.9 - 2026-08-09
|
||||||
|
|
||||||
- Restored the authored same-audio cut prior for production generation inputs
|
- Restored the authored same-audio cut prior for production generation inputs
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
- Git 2.14 or newer available on `PATH`
|
- Git 2.14 or newer available on `PATH`
|
||||||
- Git LFS installed before Unity resolves the package
|
- Git LFS installed before Unity resolves the package
|
||||||
|
|
||||||
The bundled `CWCameraWorker` contains Python 3.10 and its inference
|
The bundled `CWCameraWorker` contains Python 3.12.13 and its inference
|
||||||
dependencies. Do not install Python on an artist workstation for this package.
|
dependencies. Do not install Python on an artist workstation for this package.
|
||||||
The worker is an onedir build: `CWCameraWorker.exe` and its `_internal` folder
|
The worker is an onedir build: `CWCameraWorker.exe` and its `_internal` folder
|
||||||
must remain together.
|
must remain together.
|
||||||
@ -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.15
|
https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.16
|
||||||
```
|
```
|
||||||
|
|
||||||
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.15 includes a compact, read-only `RuntimeData~` bundle with the
|
Version 0.1.16 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
|
||||||
|
|||||||
@ -4169,31 +4169,122 @@ namespace Streamingle.Editor
|
|||||||
{
|
{
|
||||||
var projectRoot = Directory.GetParent(Application.dataPath)?.FullName
|
var projectRoot = Directory.GetParent(Application.dataPath)?.FullName
|
||||||
?? Environment.CurrentDirectory;
|
?? Environment.CurrentDirectory;
|
||||||
return AICameraCliRunner.ResolveExecutable(
|
return ResolveCliExecutablePath(
|
||||||
ignoreConfigured ? string.Empty : _cliExecutable,
|
ignoreConfigured ? string.Empty : _cliExecutable,
|
||||||
_cwAiRoot,
|
_cwAiRoot,
|
||||||
projectRoot);
|
projectRoot,
|
||||||
|
ResolvePhysicalPackageRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string ResolveCliExecutablePath(
|
||||||
|
string configuredExecutable,
|
||||||
|
string cwAiRoot,
|
||||||
|
string projectRoot,
|
||||||
|
string packageRoot)
|
||||||
|
{
|
||||||
|
var normalizedConfigured = TryGetFullPath(configuredExecutable);
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedConfigured) &&
|
||||||
|
!IsCameraAiPackageCachePath(normalizedConfigured) &&
|
||||||
|
File.Exists(normalizedConfigured))
|
||||||
|
{
|
||||||
|
// A valid path outside PackageCache was explicitly selected by
|
||||||
|
// the user and must remain authoritative.
|
||||||
|
return normalizedConfigured;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedPackageRoot = TryGetFullPath(packageRoot);
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedPackageRoot))
|
||||||
|
{
|
||||||
|
var packagedWorker = Path.Combine(
|
||||||
|
normalizedPackageRoot,
|
||||||
|
"Tools~",
|
||||||
|
"CWCameraWorker",
|
||||||
|
AICameraCliRunner.ExecutableName);
|
||||||
|
if (File.Exists(packagedWorker))
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(packagedWorker);
|
||||||
|
}
|
||||||
|
|
||||||
|
packagedWorker = Path.Combine(
|
||||||
|
normalizedPackageRoot,
|
||||||
|
"Tools~",
|
||||||
|
AICameraCliRunner.ExecutableName);
|
||||||
|
if (File.Exists(packagedWorker))
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(packagedWorker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never fall back to a serialized worker or data root from an old
|
||||||
|
// Git package revision merely because Unity has not evicted that
|
||||||
|
// PackageCache directory yet.
|
||||||
|
var safeConfigured = IsCameraAiPackageCachePath(configuredExecutable)
|
||||||
|
? string.Empty
|
||||||
|
: normalizedConfigured;
|
||||||
|
var safeCwAiRoot = IsCameraAiPackageCachePath(cwAiRoot)
|
||||||
|
? string.Empty
|
||||||
|
: TryGetFullPath(cwAiRoot);
|
||||||
|
return AICameraCliRunner.ResolveExecutable(
|
||||||
|
safeConfigured,
|
||||||
|
safeCwAiRoot,
|
||||||
|
projectRoot,
|
||||||
|
normalizedPackageRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static string ResolveDefaultCwAiRoot(
|
internal static string ResolveDefaultCwAiRoot(
|
||||||
string projectRoot,
|
string projectRoot,
|
||||||
string configuredRoot)
|
string configuredRoot)
|
||||||
|
{
|
||||||
|
return ResolveDefaultCwAiRoot(
|
||||||
|
projectRoot,
|
||||||
|
configuredRoot,
|
||||||
|
ResolvePhysicalPackageRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string ResolveDefaultCwAiRoot(
|
||||||
|
string projectRoot,
|
||||||
|
string configuredRoot,
|
||||||
|
string packageRoot)
|
||||||
{
|
{
|
||||||
var normalizedProjectRoot = string.IsNullOrWhiteSpace(projectRoot)
|
var normalizedProjectRoot = string.IsNullOrWhiteSpace(projectRoot)
|
||||||
? Environment.CurrentDirectory
|
? Environment.CurrentDirectory
|
||||||
: Path.GetFullPath(projectRoot);
|
: Path.GetFullPath(projectRoot);
|
||||||
|
|
||||||
|
var normalizedConfigured = TryGetFullPath(configuredRoot);
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedConfigured) &&
|
||||||
|
!IsCameraAiPackageCachePath(normalizedConfigured) &&
|
||||||
|
IsGenerationLibraryRoot(normalizedConfigured))
|
||||||
|
{
|
||||||
|
// Preserve a valid repository or data library that the user
|
||||||
|
// explicitly selected outside Unity's PackageCache.
|
||||||
|
return normalizedConfigured;
|
||||||
|
}
|
||||||
|
|
||||||
var candidates = new List<string>();
|
var candidates = new List<string>();
|
||||||
// Prefer the self-contained package, including Unity's resolved
|
var normalizedPackageRoot = TryGetFullPath(packageRoot);
|
||||||
// Git PackageCache path. A stale serialized external CW-AI root
|
if (!string.IsNullOrWhiteSpace(normalizedPackageRoot))
|
||||||
// must not silently override the bundled reference library.
|
{
|
||||||
candidates.AddRange(EnumeratePackagedRuntimeRoots(normalizedProjectRoot));
|
candidates.Add(Path.Combine(
|
||||||
candidates.Add(configuredRoot);
|
normalizedPackageRoot,
|
||||||
candidates.Add(normalizedProjectRoot);
|
"RuntimeData~"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This logical package path supports embedded/local packages when
|
||||||
|
// FileUtil cannot resolve a physical path during early domain load.
|
||||||
candidates.Add(Path.Combine(
|
candidates.Add(Path.Combine(
|
||||||
Directory.GetParent(normalizedProjectRoot)?.FullName ??
|
|
||||||
normalizedProjectRoot,
|
normalizedProjectRoot,
|
||||||
"CW-AI"));
|
"Packages",
|
||||||
candidates.Add(Environment.CurrentDirectory);
|
"com.mingle.cw-ai",
|
||||||
|
"RuntimeData~"));
|
||||||
|
candidates.AddRange(new[]
|
||||||
|
{
|
||||||
|
normalizedProjectRoot,
|
||||||
|
Path.Combine(
|
||||||
|
Directory.GetParent(normalizedProjectRoot)?.FullName ??
|
||||||
|
normalizedProjectRoot,
|
||||||
|
"CW-AI"),
|
||||||
|
Environment.CurrentDirectory
|
||||||
|
});
|
||||||
|
|
||||||
foreach (var candidate in candidates)
|
foreach (var candidate in candidates)
|
||||||
{
|
{
|
||||||
@ -4224,34 +4315,47 @@ namespace Streamingle.Editor
|
|||||||
"CW-AI");
|
"CW-AI");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> EnumeratePackagedRuntimeRoots(
|
private static string ResolvePhysicalPackageRoot()
|
||||||
string projectRoot)
|
|
||||||
{
|
{
|
||||||
yield return Path.Combine(
|
|
||||||
projectRoot,
|
|
||||||
"Packages",
|
|
||||||
"com.mingle.cw-ai",
|
|
||||||
"RuntimeData~");
|
|
||||||
var packageCache = Path.Combine(projectRoot, "Library", "PackageCache");
|
|
||||||
if (!Directory.Exists(packageCache))
|
|
||||||
{
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
IEnumerable<string> matches;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
matches = Directory.EnumerateDirectories(
|
return TryGetFullPath(FileUtil.GetPhysicalPath(
|
||||||
packageCache,
|
"Packages/com.mingle.cw-ai"));
|
||||||
"com.mingle.cw-ai@*",
|
|
||||||
SearchOption.TopDirectoryOnly);
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
yield break;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
foreach (var match in matches.OrderByDescending(path => path))
|
}
|
||||||
|
|
||||||
|
private static bool IsCameraAiPackageCachePath(string candidate)
|
||||||
|
{
|
||||||
|
var fullPath = TryGetFullPath(candidate);
|
||||||
|
if (string.IsNullOrWhiteSpace(fullPath))
|
||||||
{
|
{
|
||||||
yield return Path.Combine(match, "RuntimeData~");
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = fullPath.Replace('\\', '/');
|
||||||
|
return normalized.IndexOf(
|
||||||
|
"/Library/PackageCache/com.mingle.cw-ai@",
|
||||||
|
StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TryGetFullPath(string candidate)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(candidate.Trim());
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4322,26 +4426,32 @@ namespace Streamingle.Editor
|
|||||||
currentRoot = string.Empty;
|
currentRoot = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (string.Equals(
|
var previousRoot = _cwAiRoot;
|
||||||
currentRoot.TrimEnd('\\', '/'),
|
var repairedLibraryRoot = !string.Equals(
|
||||||
resolvedRoot.TrimEnd('\\', '/'),
|
currentRoot.TrimEnd('\\', '/'),
|
||||||
|
resolvedRoot.TrimEnd('\\', '/'),
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (repairedLibraryRoot)
|
||||||
|
{
|
||||||
|
_cwAiRoot = resolvedRoot;
|
||||||
|
AppendLog(
|
||||||
|
"[library-root-auto-repair] " +
|
||||||
|
$"{previousRoot} -> {_cwAiRoot}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousExecutable = _cliExecutable;
|
||||||
|
var resolvedExecutable = ResolveCliExecutable();
|
||||||
|
if (!string.Equals(
|
||||||
|
TryGetFullPath(previousExecutable),
|
||||||
|
TryGetFullPath(resolvedExecutable),
|
||||||
StringComparison.OrdinalIgnoreCase))
|
StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return false;
|
_cliExecutable = resolvedExecutable;
|
||||||
|
AppendLog(
|
||||||
|
"[worker-path-auto-repair] " +
|
||||||
|
$"{previousExecutable} -> {_cliExecutable}");
|
||||||
}
|
}
|
||||||
|
return repairedLibraryRoot;
|
||||||
var previousRoot = _cwAiRoot;
|
|
||||||
_cwAiRoot = resolvedRoot;
|
|
||||||
if (string.IsNullOrWhiteSpace(_cliExecutable) ||
|
|
||||||
!File.Exists(_cliExecutable))
|
|
||||||
{
|
|
||||||
_cliExecutable = ResolveCliExecutable(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppendLog(
|
|
||||||
"[library-root-auto-repair] " +
|
|
||||||
$"{previousRoot} -> {_cwAiRoot}");
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ExistingDirectoryOrFallback(
|
private static string ExistingDirectoryOrFallback(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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.15`
|
`https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.16`
|
||||||
|
|
||||||
배포 패키지에는 Windows x64용 `CWCameraWorker` 폴더 전체가 포함됩니다.
|
배포 패키지에는 Windows x64용 `CWCameraWorker` 폴더 전체가 포함됩니다.
|
||||||
Python은 따로 설치하지 않아도 되지만, Git 패키지의 대용량 바이너리를 받으려면
|
Python은 따로 설치하지 않아도 되지만, Git 패키지의 대용량 바이너리를 받으려면
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Third-party notices
|
# Third-party notices
|
||||||
|
|
||||||
The bundled Windows x64 `CWCameraWorker` is a frozen Python application. Its
|
The bundled Windows x64 `CWCameraWorker` is a frozen Python application. Its
|
||||||
runtime includes Python 3.10 and open-source packages including NumPy, SciPy,
|
runtime includes Python 3.12.13 and open-source packages including NumPy, SciPy,
|
||||||
scikit-learn, librosa, python-soundfile, joblib, PyInstaller, and their
|
scikit-learn, librosa, python-soundfile, joblib, PyInstaller, and their
|
||||||
transitive dependencies.
|
transitive dependencies.
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Playables;
|
using UnityEngine.Playables;
|
||||||
using UnityEngine.Timeline;
|
using UnityEngine.Timeline;
|
||||||
@ -157,7 +158,7 @@ namespace Streamingle.Editor
|
|||||||
|
|
||||||
[TestCase(AICameraTimelinePreviewImporter.CurveSimplificationPreset.Balanced)]
|
[TestCase(AICameraTimelinePreviewImporter.CurveSimplificationPreset.Balanced)]
|
||||||
[TestCase(AICameraTimelinePreviewImporter.CurveSimplificationPreset.Editable)]
|
[TestCase(AICameraTimelinePreviewImporter.CurveSimplificationPreset.Editable)]
|
||||||
public void ReducedPositionCurvesUseAuditedC1HermiteWithoutOvershoot(
|
public void ReducedPositionCurvesUseAuditedC2SplineWithoutOvershoot(
|
||||||
AICameraTimelinePreviewImporter.CurveSimplificationPreset preset)
|
AICameraTimelinePreviewImporter.CurveSimplificationPreset preset)
|
||||||
{
|
{
|
||||||
var samples = CreateQuadraticMotionSamples(241);
|
var samples = CreateQuadraticMotionSamples(241);
|
||||||
@ -172,9 +173,9 @@ namespace Streamingle.Editor
|
|||||||
Assert.That(
|
Assert.That(
|
||||||
curves.XCurve.length,
|
curves.XCurve.length,
|
||||||
Is.LessThan(samples.Times.Length));
|
Is.LessThan(samples.Times.Length));
|
||||||
AssertCurveIsC1(curves.XCurve);
|
AssertCurveIsC2(curves.XCurve);
|
||||||
AssertCurveIsC1(curves.YCurve);
|
AssertCurveIsC2(curves.YCurve);
|
||||||
AssertCurveIsC1(curves.ZCurve);
|
AssertCurveIsC2(curves.ZCurve);
|
||||||
AssertCurveDoesNotOvershootSelectedSegments(
|
AssertCurveDoesNotOvershootSelectedSegments(
|
||||||
curves.XCurve,
|
curves.XCurve,
|
||||||
simplified.Times,
|
simplified.Times,
|
||||||
@ -214,7 +215,7 @@ namespace Streamingle.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void ReducedPositionCurveAuditFallsBackToExactPositionOnly()
|
public void SparsePositionErrorIsAdaptivelyRefinedWithoutDenseFallback()
|
||||||
{
|
{
|
||||||
const int count = 61;
|
const int count = 61;
|
||||||
var times = new double[count];
|
var times = new double[count];
|
||||||
@ -237,23 +238,69 @@ namespace Streamingle.Editor
|
|||||||
simplified,
|
simplified,
|
||||||
AICameraTimelinePreviewImporter.CurveSimplificationPreset.Editable);
|
AICameraTimelinePreviewImporter.CurveSimplificationPreset.Editable);
|
||||||
|
|
||||||
Assert.That(curves.UsedExactFallback, Is.True);
|
Assert.That(curves.UsedExactFallback, Is.False, curves.FallbackReason);
|
||||||
Assert.That(curves.FallbackReason, Does.Contain("position error"));
|
Assert.That(simplified.PositionUsedExactFallback, Is.False);
|
||||||
Assert.That(simplified.PositionUsedExactFallback, Is.True);
|
Assert.That(curves.XCurve.length, Is.GreaterThan(2));
|
||||||
Assert.That(
|
Assert.That(curves.XCurve.length, Is.LessThan(count));
|
||||||
simplified.PositionFallbackReason,
|
AssertCurveIsC2(curves.XCurve);
|
||||||
Is.EqualTo(curves.FallbackReason));
|
AssertCurveIsC2(curves.YCurve);
|
||||||
Assert.That(curves.XCurve.length, Is.EqualTo(count));
|
AssertCurveIsC2(curves.ZCurve);
|
||||||
Assert.That(curves.YCurve.length, Is.EqualTo(count));
|
var settings = AICameraTimelinePreviewImporter
|
||||||
Assert.That(curves.ZCurve.length, Is.EqualTo(count));
|
.GetCurveSimplificationSettings(
|
||||||
|
AICameraTimelinePreviewImporter
|
||||||
|
.CurveSimplificationPreset.Editable);
|
||||||
for (var index = 0; index < count; index++)
|
for (var index = 0; index < count; index++)
|
||||||
{
|
{
|
||||||
Assert.That(
|
Assert.That(
|
||||||
curves.XCurve.Evaluate((float)times[index]),
|
Mathf.Abs(
|
||||||
Is.EqualTo(positions[index].x).Within(0.000001f));
|
curves.XCurve.Evaluate((float)times[index]) -
|
||||||
|
positions[index].x),
|
||||||
|
Is.LessThanOrEqualTo(
|
||||||
|
settings.PositionErrorMeters + 0.0001f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void HighFrequencyPositionExceedingRefinementBudgetUsesAuditedDenseFallback()
|
||||||
|
{
|
||||||
|
const int count = 1201;
|
||||||
|
var times = new double[count];
|
||||||
|
var positions = new Vector3[count];
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
var time = index / 60f;
|
||||||
|
times[index] = time;
|
||||||
|
positions[index] = new Vector3(
|
||||||
|
Mathf.Sin(time * Mathf.PI * 6f),
|
||||||
|
1.6f,
|
||||||
|
-4f);
|
||||||
|
}
|
||||||
|
|
||||||
|
var simplified = CreatePositionOnlySimplifiedCurves(
|
||||||
|
times,
|
||||||
|
positions,
|
||||||
|
new[] { 0, count - 1 });
|
||||||
|
var curves = AICameraTimelinePreviewImporter.BuildPositionCurves(
|
||||||
|
simplified,
|
||||||
|
AICameraTimelinePreviewImporter.CurveSimplificationPreset.Editable);
|
||||||
|
|
||||||
|
Assert.That(curves.UsedExactFallback, Is.True);
|
||||||
|
Assert.That(curves.XCurve.length, Is.EqualTo(count));
|
||||||
|
AssertCurveIsC2(curves.XCurve);
|
||||||
|
var audit = AICameraTimelinePreviewImporter.AuditPositionCurves(
|
||||||
|
times,
|
||||||
|
positions,
|
||||||
|
curves.XCurve,
|
||||||
|
curves.YCurve,
|
||||||
|
curves.ZCurve,
|
||||||
|
AICameraTimelinePreviewImporter
|
||||||
|
.GetCurveSimplificationSettings(
|
||||||
|
AICameraTimelinePreviewImporter
|
||||||
|
.CurveSimplificationPreset.Editable)
|
||||||
|
.PositionErrorMeters);
|
||||||
|
Assert.That(audit.Passed, Is.True, audit.FailureReason);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void MotionAuditRejectsPathologicalMidpointVelocityRegression()
|
public void MotionAuditRejectsPathologicalMidpointVelocityRegression()
|
||||||
{
|
{
|
||||||
@ -281,7 +328,9 @@ namespace Streamingle.Editor
|
|||||||
100f);
|
100f);
|
||||||
|
|
||||||
Assert.That(audit.Passed, Is.False);
|
Assert.That(audit.Passed, Is.False);
|
||||||
Assert.That(audit.FailureReason, Does.Contain("regressed"));
|
Assert.That(
|
||||||
|
audit.FailureReason,
|
||||||
|
Does.Contain("regressed").Or.Contain("not C1").Or.Contain("not C2"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -412,7 +461,7 @@ namespace Streamingle.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void ExactPositionCurvesRetainEveryLinearSampleWithoutFallback()
|
public void ExactPositionCurvesRetainEverySampleWithC2Tangents()
|
||||||
{
|
{
|
||||||
var samples = CreateSamples(91);
|
var samples = CreateSamples(91);
|
||||||
var simplified = Simplify(
|
var simplified = Simplify(
|
||||||
@ -427,6 +476,9 @@ namespace Streamingle.Editor
|
|||||||
Assert.That(curves.XCurve.length, Is.EqualTo(samples.Times.Length));
|
Assert.That(curves.XCurve.length, Is.EqualTo(samples.Times.Length));
|
||||||
Assert.That(curves.YCurve.length, Is.EqualTo(samples.Times.Length));
|
Assert.That(curves.YCurve.length, Is.EqualTo(samples.Times.Length));
|
||||||
Assert.That(curves.ZCurve.length, Is.EqualTo(samples.Times.Length));
|
Assert.That(curves.ZCurve.length, Is.EqualTo(samples.Times.Length));
|
||||||
|
AssertCurveIsC2(curves.XCurve);
|
||||||
|
AssertCurveIsC2(curves.YCurve);
|
||||||
|
AssertCurveIsC2(curves.ZCurve);
|
||||||
for (var index = 0; index < samples.Times.Length; index++)
|
for (var index = 0; index < samples.Times.Length; index++)
|
||||||
{
|
{
|
||||||
var time = (float)samples.Times[index];
|
var time = (float)samples.Times[index];
|
||||||
@ -442,6 +494,40 @@ namespace Streamingle.Editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ExactPositionCurvesRejectUnboundedBetweenFrameJerk()
|
||||||
|
{
|
||||||
|
var times = new[]
|
||||||
|
{
|
||||||
|
0.0,
|
||||||
|
1.0 / 60.0,
|
||||||
|
2.0 / 60.0,
|
||||||
|
2.0
|
||||||
|
};
|
||||||
|
var positions = new[]
|
||||||
|
{
|
||||||
|
new Vector3(0f, 1.6f, -4f),
|
||||||
|
new Vector3(0f, 1.6f, -4f),
|
||||||
|
new Vector3(1f, 1.6f, -4f),
|
||||||
|
new Vector3(1f, 1.6f, -4f)
|
||||||
|
};
|
||||||
|
var simplified = CreatePositionOnlySimplifiedCurves(
|
||||||
|
times,
|
||||||
|
positions,
|
||||||
|
Enumerable.Range(0, times.Length).ToArray());
|
||||||
|
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
AICameraTimelinePreviewImporter.BuildPositionCurves(
|
||||||
|
simplified,
|
||||||
|
AICameraTimelinePreviewImporter
|
||||||
|
.CurveSimplificationPreset.Exact));
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
exception.Message,
|
||||||
|
Does.Contain("Exact dense C2 position curves failed"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("high-rate motion audit"));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void EditablePresetPreservesStopThroughDirectionChangeKeys()
|
public void EditablePresetPreservesStopThroughDirectionChangeKeys()
|
||||||
{
|
{
|
||||||
@ -637,6 +723,37 @@ namespace Streamingle.Editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void C1OnlyCurveIsRejectedAndItsJerkDivergesWithSampleRate()
|
||||||
|
{
|
||||||
|
var curve = new AnimationCurve(
|
||||||
|
new Keyframe(0f, 0f, 0f, 0f),
|
||||||
|
new Keyframe(1f, 1f, 0f, 0f),
|
||||||
|
new Keyframe(2f, 1f, 0f, 0f));
|
||||||
|
AssertCurveIsC1(curve);
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
AICameraTimelinePreviewImporter.TryDescribeC2CurveFailure(
|
||||||
|
curve,
|
||||||
|
"C1-only fixture",
|
||||||
|
out var failureReason),
|
||||||
|
Is.True);
|
||||||
|
Assert.That(failureReason, Does.Contain("not C2"));
|
||||||
|
|
||||||
|
var jerk120 = MeasureMaximumJerk(
|
||||||
|
CreateUniformTimes(0f, 2f, 120),
|
||||||
|
EvaluateScalarCurveAsPositions(curve, 0f, 2f, 120));
|
||||||
|
var jerk240 = MeasureMaximumJerk(
|
||||||
|
CreateUniformTimes(0f, 2f, 240),
|
||||||
|
EvaluateScalarCurveAsPositions(curve, 0f, 2f, 240));
|
||||||
|
var jerk480 = MeasureMaximumJerk(
|
||||||
|
CreateUniformTimes(0f, 2f, 480),
|
||||||
|
EvaluateScalarCurveAsPositions(curve, 0f, 2f, 480));
|
||||||
|
|
||||||
|
Assert.That(jerk240, Is.GreaterThan(jerk120 * 1.7f));
|
||||||
|
Assert.That(jerk480, Is.GreaterThan(jerk240 * 1.7f));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void PreviewDirectorDetectionAcceptsScopedGeneratedPreviewOnly()
|
public void PreviewDirectorDetectionAcceptsScopedGeneratedPreviewOnly()
|
||||||
{
|
{
|
||||||
@ -789,6 +906,115 @@ namespace Streamingle.Editor
|
|||||||
allIndices);
|
allIndices);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static AICameraTimelinePreviewImporter.SimplifiedCameraCurves
|
||||||
|
CreateRotationOnlySimplifiedCurves(
|
||||||
|
double[] times,
|
||||||
|
Quaternion[] rotations,
|
||||||
|
AICameraTimelinePreviewImporter.CurveSimplificationPreset preset)
|
||||||
|
{
|
||||||
|
var positions = Enumerable.Repeat(Vector3.zero, times.Length).ToArray();
|
||||||
|
var fieldOfView = Enumerable.Repeat(40f, times.Length).ToArray();
|
||||||
|
var dutch = new float[times.Length];
|
||||||
|
return AICameraTimelinePreviewImporter.SimplifyCameraCurves(
|
||||||
|
times,
|
||||||
|
positions,
|
||||||
|
rotations,
|
||||||
|
fieldOfView,
|
||||||
|
dutch,
|
||||||
|
preset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AICameraTimelinePreviewImporter.SimplifiedCameraCurves
|
||||||
|
CreateRotationOnlySimplifiedCurves(
|
||||||
|
double[] times,
|
||||||
|
Quaternion[] rotations,
|
||||||
|
int[] rotationIndices)
|
||||||
|
{
|
||||||
|
var positions = Enumerable.Repeat(Vector3.zero, times.Length).ToArray();
|
||||||
|
var fieldOfView = Enumerable.Repeat(40f, times.Length).ToArray();
|
||||||
|
var dutch = new float[times.Length];
|
||||||
|
var allIndices = Enumerable.Range(0, times.Length).ToArray();
|
||||||
|
return new AICameraTimelinePreviewImporter.SimplifiedCameraCurves(
|
||||||
|
times,
|
||||||
|
positions,
|
||||||
|
rotations,
|
||||||
|
fieldOfView,
|
||||||
|
dutch,
|
||||||
|
allIndices,
|
||||||
|
rotationIndices,
|
||||||
|
allIndices,
|
||||||
|
allIndices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationCurve BuildQuaternionCornerCurve(
|
||||||
|
IReadOnlyList<float> times,
|
||||||
|
IReadOnlyList<float> values)
|
||||||
|
{
|
||||||
|
var firstSlope =
|
||||||
|
(values[1] - values[0]) / (times[1] - times[0]);
|
||||||
|
var secondSlope =
|
||||||
|
(values[2] - values[1]) / (times[2] - times[1]);
|
||||||
|
return new AnimationCurve(
|
||||||
|
new Keyframe(
|
||||||
|
times[0],
|
||||||
|
values[0],
|
||||||
|
firstSlope,
|
||||||
|
firstSlope),
|
||||||
|
new Keyframe(
|
||||||
|
times[1],
|
||||||
|
values[1],
|
||||||
|
firstSlope,
|
||||||
|
secondSlope),
|
||||||
|
new Keyframe(
|
||||||
|
times[2],
|
||||||
|
values[2],
|
||||||
|
secondSlope,
|
||||||
|
secondSlope));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetRotationCurve(
|
||||||
|
AnimationClip clip,
|
||||||
|
string propertyName,
|
||||||
|
AnimationCurve curve)
|
||||||
|
{
|
||||||
|
AnimationUtility.SetEditorCurve(
|
||||||
|
clip,
|
||||||
|
EditorCurveBinding.FloatCurve(
|
||||||
|
string.Empty,
|
||||||
|
typeof(Transform),
|
||||||
|
propertyName),
|
||||||
|
curve);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationCurve GetRotationCurve(
|
||||||
|
AnimationClip clip,
|
||||||
|
string propertyName)
|
||||||
|
{
|
||||||
|
return AnimationUtility.GetEditorCurve(
|
||||||
|
clip,
|
||||||
|
EditorCurveBinding.FloatCurve(
|
||||||
|
string.Empty,
|
||||||
|
typeof(Transform),
|
||||||
|
propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertQuaternionEndpoints(
|
||||||
|
IReadOnlyList<double> times,
|
||||||
|
IReadOnlyList<Quaternion> rotations,
|
||||||
|
AICameraTimelinePreviewImporter.RotationCurveBuildResult curves)
|
||||||
|
{
|
||||||
|
var endpointTimes = new[] { times[0], times[times.Count - 1] };
|
||||||
|
var evaluated = AICameraTimelinePreviewImporter.EvaluateRotationCurves(
|
||||||
|
endpointTimes,
|
||||||
|
curves);
|
||||||
|
Assert.That(
|
||||||
|
Quaternion.Angle(rotations[0], evaluated[0]),
|
||||||
|
Is.LessThan(0.05f));
|
||||||
|
Assert.That(
|
||||||
|
Quaternion.Angle(rotations[rotations.Count - 1], evaluated[1]),
|
||||||
|
Is.LessThan(0.05f));
|
||||||
|
}
|
||||||
|
|
||||||
private static void AssertCurveIsC1(AnimationCurve curve)
|
private static void AssertCurveIsC1(AnimationCurve curve)
|
||||||
{
|
{
|
||||||
var keys = curve.keys;
|
var keys = curve.keys;
|
||||||
@ -801,6 +1027,16 @@ namespace Streamingle.Editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AssertCurveIsC2(AnimationCurve curve)
|
||||||
|
{
|
||||||
|
AssertCurveIsC1(curve);
|
||||||
|
var failed = AICameraTimelinePreviewImporter.TryDescribeC2CurveFailure(
|
||||||
|
curve,
|
||||||
|
"test curve",
|
||||||
|
out var failureReason);
|
||||||
|
Assert.That(failed, Is.False, failureReason);
|
||||||
|
}
|
||||||
|
|
||||||
private static void AssertCurveDoesNotOvershootSelectedSegments(
|
private static void AssertCurveDoesNotOvershootSelectedSegments(
|
||||||
AnimationCurve curve,
|
AnimationCurve curve,
|
||||||
IReadOnlyList<double> times,
|
IReadOnlyList<double> times,
|
||||||
@ -829,6 +1065,390 @@ namespace Streamingle.Editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Shot68StyleLongRotationUsesAuditedSparseC2Curves()
|
||||||
|
{
|
||||||
|
const int count = 1081;
|
||||||
|
var times = new double[count];
|
||||||
|
var rotations = new Quaternion[count];
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
var time = index / 60f;
|
||||||
|
var normalized = index / (float)(count - 1);
|
||||||
|
var eased = normalized * normalized * (3f - 2f * normalized);
|
||||||
|
times[index] = time;
|
||||||
|
rotations[index] = Quaternion.Euler(
|
||||||
|
-3f + 1.25f * Mathf.Sin(time * 0.22f),
|
||||||
|
-24f + 48f * eased + 1.5f * Mathf.Sin(time * 0.3f),
|
||||||
|
0.6f * Mathf.Sin(time * 0.27f));
|
||||||
|
}
|
||||||
|
|
||||||
|
var simplified = CreateRotationOnlySimplifiedCurves(
|
||||||
|
times,
|
||||||
|
rotations,
|
||||||
|
AICameraTimelinePreviewImporter.CurveSimplificationPreset.Balanced);
|
||||||
|
var curves = AICameraTimelinePreviewImporter.BuildRotationCurves(
|
||||||
|
simplified,
|
||||||
|
AICameraTimelinePreviewImporter.CurveSimplificationPreset.Balanced);
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
simplified.RotationIndices.Length,
|
||||||
|
Is.LessThan(count / 4),
|
||||||
|
"The long smooth shot should remain meaningfully editable.");
|
||||||
|
Assert.That(curves.UsedDenseFallback, Is.False, curves.FallbackReason);
|
||||||
|
Assert.That(curves.XCurve.length, Is.LessThan(count / 4));
|
||||||
|
AssertCurveIsC2(curves.XCurve);
|
||||||
|
AssertCurveIsC2(curves.YCurve);
|
||||||
|
AssertCurveIsC2(curves.ZCurve);
|
||||||
|
AssertCurveIsC2(curves.WCurve);
|
||||||
|
AssertQuaternionEndpoints(times, rotations, curves);
|
||||||
|
|
||||||
|
var audit = AICameraTimelinePreviewImporter.AuditRotationCurves(
|
||||||
|
times,
|
||||||
|
rotations,
|
||||||
|
curves.XCurve,
|
||||||
|
curves.YCurve,
|
||||||
|
curves.ZCurve,
|
||||||
|
curves.WCurve,
|
||||||
|
AICameraTimelinePreviewImporter
|
||||||
|
.GetCurveSimplificationSettings(
|
||||||
|
AICameraTimelinePreviewImporter
|
||||||
|
.CurveSimplificationPreset.Balanced)
|
||||||
|
.QuaternionAngleErrorDegrees);
|
||||||
|
Assert.That(audit.Passed, Is.True, audit.FailureReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AngularMotionAuditRejectsSparseLinearVelocityCorner()
|
||||||
|
{
|
||||||
|
const int count = 121;
|
||||||
|
var times = new double[count];
|
||||||
|
var rotations = new Quaternion[count];
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
var time = index / 60f;
|
||||||
|
times[index] = time;
|
||||||
|
rotations[index] = Quaternion.Euler(0f, 15f * time, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
var cornerRotations = new[]
|
||||||
|
{
|
||||||
|
Quaternion.Euler(0f, 0f, 0f),
|
||||||
|
Quaternion.Euler(0f, 10f, 0f),
|
||||||
|
Quaternion.Euler(0f, 30f, 0f)
|
||||||
|
};
|
||||||
|
var cornerTimes = new[] { 0f, 1f, 2f };
|
||||||
|
var xCurve = BuildQuaternionCornerCurve(
|
||||||
|
cornerTimes,
|
||||||
|
cornerRotations.Select(value => value.x).ToArray());
|
||||||
|
var yCurve = BuildQuaternionCornerCurve(
|
||||||
|
cornerTimes,
|
||||||
|
cornerRotations.Select(value => value.y).ToArray());
|
||||||
|
var zCurve = BuildQuaternionCornerCurve(
|
||||||
|
cornerTimes,
|
||||||
|
cornerRotations.Select(value => value.z).ToArray());
|
||||||
|
var wCurve = BuildQuaternionCornerCurve(
|
||||||
|
cornerTimes,
|
||||||
|
cornerRotations.Select(value => value.w).ToArray());
|
||||||
|
|
||||||
|
var audit = AICameraTimelinePreviewImporter.AuditRotationCurves(
|
||||||
|
times,
|
||||||
|
rotations,
|
||||||
|
xCurve,
|
||||||
|
yCurve,
|
||||||
|
zCurve,
|
||||||
|
wCurve,
|
||||||
|
180f);
|
||||||
|
|
||||||
|
Assert.That(audit.Passed, Is.False);
|
||||||
|
Assert.That(
|
||||||
|
audit.FailureReason,
|
||||||
|
Does.Contain("angular").Or.Contain("rotation"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Shot66StyleMissedCorrectionIsAdaptivelyRefinedAsC2Rotation()
|
||||||
|
{
|
||||||
|
const int count = 121;
|
||||||
|
var times = new double[count];
|
||||||
|
var rotations = new Quaternion[count];
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
var time = index / 60f;
|
||||||
|
var correctionDistance = (index - 82f) / 8f;
|
||||||
|
var correction = 3f * Mathf.Exp(
|
||||||
|
-correctionDistance * correctionDistance);
|
||||||
|
times[index] = time;
|
||||||
|
rotations[index] = Quaternion.Euler(
|
||||||
|
0.4f * Mathf.Sin(time),
|
||||||
|
8f * time + correction,
|
||||||
|
0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
var simplified = CreateRotationOnlySimplifiedCurves(
|
||||||
|
times,
|
||||||
|
rotations,
|
||||||
|
new[] { 0, count - 1 });
|
||||||
|
var curves = AICameraTimelinePreviewImporter.BuildRotationCurves(
|
||||||
|
simplified,
|
||||||
|
AICameraTimelinePreviewImporter.CurveSimplificationPreset.Editable);
|
||||||
|
|
||||||
|
Assert.That(curves.UsedDenseFallback, Is.False, curves.FallbackReason);
|
||||||
|
Assert.That(simplified.RotationUsedDenseFallback, Is.False);
|
||||||
|
Assert.That(curves.XCurve.length, Is.GreaterThan(2));
|
||||||
|
Assert.That(curves.XCurve.length, Is.LessThan(count));
|
||||||
|
AssertCurveIsC2(curves.XCurve);
|
||||||
|
AssertCurveIsC2(curves.YCurve);
|
||||||
|
AssertCurveIsC2(curves.ZCurve);
|
||||||
|
AssertCurveIsC2(curves.WCurve);
|
||||||
|
AssertQuaternionEndpoints(times, rotations, curves);
|
||||||
|
|
||||||
|
var evaluated = AICameraTimelinePreviewImporter.EvaluateRotationCurves(
|
||||||
|
times,
|
||||||
|
curves);
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
Assert.That(
|
||||||
|
Quaternion.Angle(rotations[index], evaluated[index]),
|
||||||
|
Is.LessThan(0.51f),
|
||||||
|
$"Adaptive C2 refinement missed source rotation {index}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void HighFrequencyRotationExceedingRefinementBudgetUsesDenseC2Fallback()
|
||||||
|
{
|
||||||
|
const int count = 1201;
|
||||||
|
var times = new double[count];
|
||||||
|
var rotations = new Quaternion[count];
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
var time = index / 60f;
|
||||||
|
times[index] = time;
|
||||||
|
rotations[index] = Quaternion.Euler(
|
||||||
|
0f,
|
||||||
|
10f * Mathf.Sin(time * Mathf.PI * 6f),
|
||||||
|
0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
var simplified = CreateRotationOnlySimplifiedCurves(
|
||||||
|
times,
|
||||||
|
rotations,
|
||||||
|
new[] { 0, count - 1 });
|
||||||
|
var curves = AICameraTimelinePreviewImporter.BuildRotationCurves(
|
||||||
|
simplified,
|
||||||
|
AICameraTimelinePreviewImporter.CurveSimplificationPreset.Editable);
|
||||||
|
|
||||||
|
Assert.That(curves.UsedDenseFallback, Is.True);
|
||||||
|
Assert.That(curves.XCurve.length, Is.EqualTo(count));
|
||||||
|
AssertCurveIsC2(curves.XCurve);
|
||||||
|
AssertCurveIsC2(curves.YCurve);
|
||||||
|
AssertCurveIsC2(curves.ZCurve);
|
||||||
|
AssertCurveIsC2(curves.WCurve);
|
||||||
|
var audit = AICameraTimelinePreviewImporter.AuditRotationCurves(
|
||||||
|
times,
|
||||||
|
rotations,
|
||||||
|
curves.XCurve,
|
||||||
|
curves.YCurve,
|
||||||
|
curves.ZCurve,
|
||||||
|
curves.WCurve,
|
||||||
|
AICameraTimelinePreviewImporter
|
||||||
|
.GetCurveSimplificationSettings(
|
||||||
|
AICameraTimelinePreviewImporter
|
||||||
|
.CurveSimplificationPreset.Editable)
|
||||||
|
.QuaternionAngleErrorDegrees);
|
||||||
|
Assert.That(audit.Passed, Is.True, audit.FailureReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void UnityQuaternionContinuityPassPreservesAuditedC2Curves()
|
||||||
|
{
|
||||||
|
const int count = 241;
|
||||||
|
var times = new double[count];
|
||||||
|
var rotations = new Quaternion[count];
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
var time = index / 60f;
|
||||||
|
times[index] = time;
|
||||||
|
rotations[index] = Quaternion.Euler(
|
||||||
|
2f * Mathf.Sin(time * 0.45f),
|
||||||
|
12f * Mathf.Sin(time * 0.3f),
|
||||||
|
0.7f * Mathf.Sin(time * 0.6f));
|
||||||
|
}
|
||||||
|
|
||||||
|
var simplified = CreateRotationOnlySimplifiedCurves(
|
||||||
|
times,
|
||||||
|
rotations,
|
||||||
|
AICameraTimelinePreviewImporter.CurveSimplificationPreset.Balanced);
|
||||||
|
var curves = AICameraTimelinePreviewImporter.BuildRotationCurves(
|
||||||
|
simplified,
|
||||||
|
AICameraTimelinePreviewImporter.CurveSimplificationPreset.Balanced);
|
||||||
|
var clip = new AnimationClip();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SetRotationCurve(clip, "m_LocalRotation.x", curves.XCurve);
|
||||||
|
SetRotationCurve(clip, "m_LocalRotation.y", curves.YCurve);
|
||||||
|
SetRotationCurve(clip, "m_LocalRotation.z", curves.ZCurve);
|
||||||
|
SetRotationCurve(clip, "m_LocalRotation.w", curves.WCurve);
|
||||||
|
clip.EnsureQuaternionContinuity();
|
||||||
|
|
||||||
|
var xCurve = GetRotationCurve(clip, "m_LocalRotation.x");
|
||||||
|
var yCurve = GetRotationCurve(clip, "m_LocalRotation.y");
|
||||||
|
var zCurve = GetRotationCurve(clip, "m_LocalRotation.z");
|
||||||
|
var wCurve = GetRotationCurve(clip, "m_LocalRotation.w");
|
||||||
|
AssertCurveIsC2(xCurve);
|
||||||
|
AssertCurveIsC2(yCurve);
|
||||||
|
AssertCurveIsC2(zCurve);
|
||||||
|
AssertCurveIsC2(wCurve);
|
||||||
|
|
||||||
|
var audit = AICameraTimelinePreviewImporter.AuditRotationCurves(
|
||||||
|
times,
|
||||||
|
rotations,
|
||||||
|
xCurve,
|
||||||
|
yCurve,
|
||||||
|
zCurve,
|
||||||
|
wCurve,
|
||||||
|
AICameraTimelinePreviewImporter
|
||||||
|
.GetCurveSimplificationSettings(
|
||||||
|
AICameraTimelinePreviewImporter
|
||||||
|
.CurveSimplificationPreset.Balanced)
|
||||||
|
.QuaternionAngleErrorDegrees);
|
||||||
|
Assert.That(audit.Passed, Is.True, audit.FailureReason);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
UnityEngine.Object.DestroyImmediate(clip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Crossing180WithSignFlipsStaysC2AndSamplesWithoutNearZero()
|
||||||
|
{
|
||||||
|
const int count = 241;
|
||||||
|
var times = new double[count];
|
||||||
|
var rotations = new Quaternion[count];
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
var time = index / 60f;
|
||||||
|
times[index] = time;
|
||||||
|
var rotation = Quaternion.Euler(
|
||||||
|
1.5f * Mathf.Sin(time * 0.4f),
|
||||||
|
170f + 10f * time,
|
||||||
|
0.5f * Mathf.Sin(time * 0.7f));
|
||||||
|
rotations[index] = index % 7 == 0
|
||||||
|
? new Quaternion(
|
||||||
|
-rotation.x,
|
||||||
|
-rotation.y,
|
||||||
|
-rotation.z,
|
||||||
|
-rotation.w)
|
||||||
|
: rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
var simplified = CreateRotationOnlySimplifiedCurves(
|
||||||
|
times,
|
||||||
|
rotations,
|
||||||
|
AICameraTimelinePreviewImporter.CurveSimplificationPreset.Balanced);
|
||||||
|
var curves = AICameraTimelinePreviewImporter.BuildRotationCurves(
|
||||||
|
simplified,
|
||||||
|
AICameraTimelinePreviewImporter.CurveSimplificationPreset.Balanced);
|
||||||
|
var clip = new AnimationClip();
|
||||||
|
var target = new GameObject("QuaternionSampleTarget");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SetRotationCurve(clip, "m_LocalRotation.x", curves.XCurve);
|
||||||
|
SetRotationCurve(clip, "m_LocalRotation.y", curves.YCurve);
|
||||||
|
SetRotationCurve(clip, "m_LocalRotation.z", curves.ZCurve);
|
||||||
|
SetRotationCurve(clip, "m_LocalRotation.w", curves.WCurve);
|
||||||
|
clip.EnsureQuaternionContinuity();
|
||||||
|
|
||||||
|
var xCurve = GetRotationCurve(clip, "m_LocalRotation.x");
|
||||||
|
var yCurve = GetRotationCurve(clip, "m_LocalRotation.y");
|
||||||
|
var zCurve = GetRotationCurve(clip, "m_LocalRotation.z");
|
||||||
|
var wCurve = GetRotationCurve(clip, "m_LocalRotation.w");
|
||||||
|
AssertCurveIsC2(xCurve);
|
||||||
|
AssertCurveIsC2(yCurve);
|
||||||
|
AssertCurveIsC2(zCurve);
|
||||||
|
AssertCurveIsC2(wCurve);
|
||||||
|
|
||||||
|
var previous = Quaternion.identity;
|
||||||
|
var auditTimes = AICameraTimelinePreviewImporter
|
||||||
|
.BuildUniformCurveAuditTimes(times, 240);
|
||||||
|
for (var index = 0; index < auditTimes.Count; index++)
|
||||||
|
{
|
||||||
|
var time = (float)auditTimes[index];
|
||||||
|
var raw = new Quaternion(
|
||||||
|
xCurve.Evaluate(time),
|
||||||
|
yCurve.Evaluate(time),
|
||||||
|
zCurve.Evaluate(time),
|
||||||
|
wCurve.Evaluate(time));
|
||||||
|
var magnitude = Mathf.Sqrt(
|
||||||
|
raw.x * raw.x +
|
||||||
|
raw.y * raw.y +
|
||||||
|
raw.z * raw.z +
|
||||||
|
raw.w * raw.w);
|
||||||
|
Assert.That(
|
||||||
|
magnitude,
|
||||||
|
Is.GreaterThan(0.9f),
|
||||||
|
$"Quaternion curve approached zero at {time:F4} s.");
|
||||||
|
var expected = Normalize(raw);
|
||||||
|
if (index > 0 && Quaternion.Dot(previous, expected) < 0f)
|
||||||
|
{
|
||||||
|
expected = new Quaternion(
|
||||||
|
-expected.x,
|
||||||
|
-expected.y,
|
||||||
|
-expected.z,
|
||||||
|
-expected.w);
|
||||||
|
}
|
||||||
|
|
||||||
|
clip.SampleAnimation(target, time);
|
||||||
|
Assert.That(
|
||||||
|
Quaternion.Angle(expected, target.transform.localRotation),
|
||||||
|
Is.LessThan(0.1f),
|
||||||
|
$"SampleAnimation diverged at {time:F4} s.");
|
||||||
|
previous = expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
var audit = AICameraTimelinePreviewImporter.AuditRotationCurves(
|
||||||
|
times,
|
||||||
|
rotations,
|
||||||
|
xCurve,
|
||||||
|
yCurve,
|
||||||
|
zCurve,
|
||||||
|
wCurve,
|
||||||
|
AICameraTimelinePreviewImporter
|
||||||
|
.GetCurveSimplificationSettings(
|
||||||
|
AICameraTimelinePreviewImporter
|
||||||
|
.CurveSimplificationPreset.Balanced)
|
||||||
|
.QuaternionAngleErrorDegrees);
|
||||||
|
Assert.That(audit.Passed, Is.True, audit.FailureReason);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
UnityEngine.Object.DestroyImmediate(target);
|
||||||
|
UnityEngine.Object.DestroyImmediate(clip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RotationAuditRejectsNearZeroQuaternionCurve()
|
||||||
|
{
|
||||||
|
var times = new[] { 0.0, 1.0 };
|
||||||
|
var rotations = new[] { Quaternion.identity, Quaternion.identity };
|
||||||
|
var zero = AnimationCurve.Linear(0f, 0f, 1f, 0f);
|
||||||
|
|
||||||
|
var audit = AICameraTimelinePreviewImporter.AuditRotationCurves(
|
||||||
|
times,
|
||||||
|
rotations,
|
||||||
|
zero,
|
||||||
|
zero,
|
||||||
|
zero,
|
||||||
|
zero,
|
||||||
|
180f);
|
||||||
|
|
||||||
|
Assert.That(audit.Passed, Is.False);
|
||||||
|
Assert.That(audit.FailureReason, Does.Contain("zero quaternion"));
|
||||||
|
}
|
||||||
|
|
||||||
private static Vector3[] EvaluatePositionCurves(
|
private static Vector3[] EvaluatePositionCurves(
|
||||||
IReadOnlyList<double> times,
|
IReadOnlyList<double> times,
|
||||||
AICameraTimelinePreviewImporter.PositionCurveBuildResult curves)
|
AICameraTimelinePreviewImporter.PositionCurveBuildResult curves)
|
||||||
@ -889,6 +1509,48 @@ namespace Streamingle.Editor
|
|||||||
return maximumJerk;
|
return maximumJerk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static double[] CreateUniformTimes(
|
||||||
|
float startTime,
|
||||||
|
float endTime,
|
||||||
|
int samplesPerSecond)
|
||||||
|
{
|
||||||
|
var intervalCount = Mathf.Max(
|
||||||
|
1,
|
||||||
|
Mathf.CeilToInt((endTime - startTime) * samplesPerSecond));
|
||||||
|
var times = new double[intervalCount + 1];
|
||||||
|
for (var index = 0; index <= intervalCount; index++)
|
||||||
|
{
|
||||||
|
times[index] = (float)Mathf.Lerp(
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
index / (float)intervalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return times;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3[] EvaluateScalarCurveAsPositions(
|
||||||
|
AnimationCurve curve,
|
||||||
|
float startTime,
|
||||||
|
float endTime,
|
||||||
|
int samplesPerSecond)
|
||||||
|
{
|
||||||
|
var times = CreateUniformTimes(
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
samplesPerSecond);
|
||||||
|
var positions = new Vector3[times.Length];
|
||||||
|
for (var index = 0; index < times.Length; index++)
|
||||||
|
{
|
||||||
|
positions[index] = new Vector3(
|
||||||
|
curve.Evaluate((float)times[index]),
|
||||||
|
0f,
|
||||||
|
0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
private static CameraSamples CreateSamples(int count)
|
private static CameraSamples CreateSamples(int count)
|
||||||
{
|
{
|
||||||
var times = new double[count];
|
var times = new double[count];
|
||||||
|
|||||||
@ -81,12 +81,14 @@ namespace Streamingle.Editor.Tests
|
|||||||
Assert.That(
|
Assert.That(
|
||||||
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
||||||
project,
|
project,
|
||||||
configured),
|
configured,
|
||||||
|
Path.Combine(root, "MissingPackage")),
|
||||||
Is.EqualTo(Path.GetFullPath(configured)));
|
Is.EqualTo(Path.GetFullPath(configured)));
|
||||||
Assert.That(
|
Assert.That(
|
||||||
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
||||||
project,
|
project,
|
||||||
string.Empty),
|
string.Empty,
|
||||||
|
Path.Combine(root, "MissingPackage")),
|
||||||
Is.EqualTo(Path.GetFullPath(sibling)));
|
Is.EqualTo(Path.GetFullPath(sibling)));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -117,7 +119,8 @@ namespace Streamingle.Editor.Tests
|
|||||||
Assert.That(
|
Assert.That(
|
||||||
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
||||||
project,
|
project,
|
||||||
project),
|
project,
|
||||||
|
Path.Combine(root, "MissingPackage")),
|
||||||
Is.EqualTo(Path.GetFullPath(sibling)));
|
Is.EqualTo(Path.GetFullPath(sibling)));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -141,7 +144,8 @@ namespace Streamingle.Editor.Tests
|
|||||||
Assert.That(
|
Assert.That(
|
||||||
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
||||||
project,
|
project,
|
||||||
"\0invalid"),
|
"\0invalid",
|
||||||
|
Path.Combine(root, "MissingPackage")),
|
||||||
Is.EqualTo(Path.GetFullPath(sibling)));
|
Is.EqualTo(Path.GetFullPath(sibling)));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -190,6 +194,158 @@ namespace Streamingle.Editor.Tests
|
|||||||
"{}");
|
"{}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DefaultCwAiRootReplacesStalePackageCacheRevision()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(
|
||||||
|
Path.GetTempPath(),
|
||||||
|
"cw-ai-package-root-tests-" + Guid.NewGuid().ToString("N"));
|
||||||
|
var project = Path.Combine(root, "UnityProject");
|
||||||
|
var staleRuntime = Path.Combine(
|
||||||
|
project,
|
||||||
|
"Library",
|
||||||
|
"PackageCache",
|
||||||
|
"com.mingle.cw-ai@old-revision",
|
||||||
|
"RuntimeData~");
|
||||||
|
var currentPackage = Path.Combine(
|
||||||
|
project,
|
||||||
|
"Library",
|
||||||
|
"PackageCache",
|
||||||
|
"com.mingle.cw-ai@current-revision");
|
||||||
|
var currentRuntime = Path.Combine(currentPackage, "RuntimeData~");
|
||||||
|
CreateCwAiRootMarker(staleRuntime);
|
||||||
|
CreateCwAiRootMarker(currentRuntime);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.That(
|
||||||
|
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
||||||
|
project,
|
||||||
|
staleRuntime,
|
||||||
|
currentPackage),
|
||||||
|
Is.EqualTo(Path.GetFullPath(currentRuntime)));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(root, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DefaultCwAiRootPreservesValidExternalSelection()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(
|
||||||
|
Path.GetTempPath(),
|
||||||
|
"cw-ai-external-root-tests-" + Guid.NewGuid().ToString("N"));
|
||||||
|
var project = Path.Combine(root, "UnityProject");
|
||||||
|
var external = Path.Combine(root, "ExternalCWAI");
|
||||||
|
var currentPackage = Path.Combine(
|
||||||
|
project,
|
||||||
|
"Library",
|
||||||
|
"PackageCache",
|
||||||
|
"com.mingle.cw-ai@current-revision");
|
||||||
|
CreateCwAiRootMarker(external);
|
||||||
|
CreateCwAiRootMarker(Path.Combine(currentPackage, "RuntimeData~"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.That(
|
||||||
|
AICameraGeneratorWindow.ResolveDefaultCwAiRoot(
|
||||||
|
project,
|
||||||
|
external,
|
||||||
|
currentPackage),
|
||||||
|
Is.EqualTo(Path.GetFullPath(external)));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(root, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CliExecutableReplacesStalePackageCacheRevision()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(
|
||||||
|
Path.GetTempPath(),
|
||||||
|
"cw-ai-worker-path-tests-" + Guid.NewGuid().ToString("N"));
|
||||||
|
var project = Path.Combine(root, "UnityProject");
|
||||||
|
var staleWorker = Path.Combine(
|
||||||
|
project,
|
||||||
|
"Library",
|
||||||
|
"PackageCache",
|
||||||
|
"com.mingle.cw-ai@old-revision",
|
||||||
|
"Tools~",
|
||||||
|
"CWCameraWorker",
|
||||||
|
AICameraCliRunner.ExecutableName);
|
||||||
|
var currentPackage = Path.Combine(
|
||||||
|
project,
|
||||||
|
"Library",
|
||||||
|
"PackageCache",
|
||||||
|
"com.mingle.cw-ai@current-revision");
|
||||||
|
var currentWorker = Path.Combine(
|
||||||
|
currentPackage,
|
||||||
|
"Tools~",
|
||||||
|
"CWCameraWorker",
|
||||||
|
AICameraCliRunner.ExecutableName);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(staleWorker));
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(currentWorker));
|
||||||
|
File.WriteAllText(staleWorker, string.Empty);
|
||||||
|
File.WriteAllText(currentWorker, string.Empty);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.That(
|
||||||
|
AICameraGeneratorWindow.ResolveCliExecutablePath(
|
||||||
|
staleWorker,
|
||||||
|
string.Empty,
|
||||||
|
project,
|
||||||
|
currentPackage),
|
||||||
|
Is.EqualTo(Path.GetFullPath(currentWorker)));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(root, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CliExecutablePreservesValidExternalSelection()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(
|
||||||
|
Path.GetTempPath(),
|
||||||
|
"cw-ai-external-worker-tests-" + Guid.NewGuid().ToString("N"));
|
||||||
|
var project = Path.Combine(root, "UnityProject");
|
||||||
|
var externalWorker = Path.Combine(
|
||||||
|
root,
|
||||||
|
"ExternalWorker",
|
||||||
|
AICameraCliRunner.ExecutableName);
|
||||||
|
var currentPackage = Path.Combine(
|
||||||
|
project,
|
||||||
|
"Library",
|
||||||
|
"PackageCache",
|
||||||
|
"com.mingle.cw-ai@current-revision");
|
||||||
|
var currentWorker = Path.Combine(
|
||||||
|
currentPackage,
|
||||||
|
"Tools~",
|
||||||
|
"CWCameraWorker",
|
||||||
|
AICameraCliRunner.ExecutableName);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(externalWorker));
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(currentWorker));
|
||||||
|
File.WriteAllText(externalWorker, string.Empty);
|
||||||
|
File.WriteAllText(currentWorker, string.Empty);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.That(
|
||||||
|
AICameraGeneratorWindow.ResolveCliExecutablePath(
|
||||||
|
externalWorker,
|
||||||
|
string.Empty,
|
||||||
|
project,
|
||||||
|
currentPackage),
|
||||||
|
Is.EqualTo(Path.GetFullPath(externalWorker)));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(root, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void CliWorkerIsTheOnlyHighQualityBackend()
|
public void CliWorkerIsTheOnlyHighQualityBackend()
|
||||||
{
|
{
|
||||||
|
|||||||
BIN
CameraAI~/Tools~/CWCameraWorker/CWCameraWorker.exe
(Stored with Git LFS)
BIN
CameraAI~/Tools~/CWCameraWorker/CWCameraWorker.exe
(Stored with Git LFS)
Binary file not shown.
Binary file not shown.
@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": "cw-camera-worker-build-identity-v1",
|
"schemaVersion": "cw-camera-worker-build-identity-v1",
|
||||||
"workerVersion": "0.1.5",
|
"workerVersion": "0.1.6",
|
||||||
"createdUtc": "2026-08-08T20:03:04.908909+00:00",
|
"createdUtc": "2026-08-09T07:15:01.579387+00:00",
|
||||||
"python": "3.12.13",
|
"python": "3.12.13",
|
||||||
"sourceRootRelative": "cwai_sources/repository",
|
"sourceRootRelative": "cwai_sources/repository",
|
||||||
"sourceSha256": {
|
"sourceSha256": {
|
||||||
"MachineLearning/CameraDirector/adjacent_transition.py": "6c1b62c2996960af23d62e9c3acd3114600effda03273b006f7076c467c4bdfa",
|
"MachineLearning/CameraDirector/adjacent_transition.py": "6c1b62c2996960af23d62e9c3acd3114600effda03273b006f7076c467c4bdfa",
|
||||||
"MachineLearning/CameraDirector/build_hybrid_preparation_cache.py": "f948b1a68352893b078ac1f6ef458df2107565378eed32890276776ca0dfdad6",
|
"MachineLearning/CameraDirector/build_hybrid_preparation_cache.py": "f948b1a68352893b078ac1f6ef458df2107565378eed32890276776ca0dfdad6",
|
||||||
"MachineLearning/CameraDirector/camera_kinematics.py": "75816a55ac1dc655731432f061c2f4356cc3b583f3ce33797578431adcd9ba39",
|
"MachineLearning/CameraDirector/camera_kinematics.py": "e94eefcb56c4ebdccbc57ae8d3650109b2c912e3642892ac13add9409db21a7f",
|
||||||
"MachineLearning/CameraDirector/camera_runtime_data.py": "29fd30a1c97d384f7bb22d024a4f88178f6245858925c302b059a7a1afdeaac1",
|
"MachineLearning/CameraDirector/camera_runtime_data.py": "29fd30a1c97d384f7bb22d024a4f88178f6245858925c302b059a7a1afdeaac1",
|
||||||
"MachineLearning/CameraDirector/cw_camera_cli.py": "943870a4cdec7e4b830b9c945d690e700902cf2a37d1e49987bba85bf0b0ea87",
|
"MachineLearning/CameraDirector/cw_camera_cli.py": "943870a4cdec7e4b830b9c945d690e700902cf2a37d1e49987bba85bf0b0ea87",
|
||||||
"MachineLearning/CameraDirector/cw_camera_runtime.py": "c4cfbc61173cb541932632e3b0a0751ae75c9d6ca3b56680722caa78f74e797d",
|
"MachineLearning/CameraDirector/cw_camera_runtime.py": "62421300af470389b5e356267d91e4dcb31626762705cd1c1d20c3fa11d271f3",
|
||||||
"MachineLearning/CameraDirector/data_driven_cut_planner.py": "36cf86a80577278d2e966cb0ebf7c3110b5b1dda5abd1d8a6b0a76e31d10e32b",
|
"MachineLearning/CameraDirector/data_driven_cut_planner.py": "36cf86a80577278d2e966cb0ebf7c3110b5b1dda5abd1d8a6b0a76e31d10e32b",
|
||||||
"MachineLearning/CameraDirector/generate_hybrid.py": "4bf19ffdd8e244a82b4ea0656bad030a30470a9e2065bdccde8bfb9d99922fea",
|
"MachineLearning/CameraDirector/generate_hybrid.py": "b6b1e7d02d96794e57c0ae9867ad18de7c6698d7bac05da1834490e2c0f3b4e3",
|
||||||
"MachineLearning/CameraDirector/hybrid_candidate_cache.py": "a0a5b6a8f612f18cb2875394f17ed89950e1e62e38c1226457d9e35848f6e380",
|
"MachineLearning/CameraDirector/hybrid_candidate_cache.py": "a0a5b6a8f612f18cb2875394f17ed89950e1e62e38c1226457d9e35848f6e380",
|
||||||
"MachineLearning/CameraDirector/hybrid_cut_reference.py": "53521d9e31d315c011892961887baf0cde6451020bf39c9d080a5072d4707fcb",
|
"MachineLearning/CameraDirector/hybrid_cut_reference.py": "53521d9e31d315c011892961887baf0cde6451020bf39c9d080a5072d4707fcb",
|
||||||
"MachineLearning/CameraDirector/hybrid_preparation_cache.py": "1b0df53124109dee2d744777979a0553999cb276cb6f538a17ddce6516e3da8b",
|
"MachineLearning/CameraDirector/hybrid_preparation_cache.py": "1b0df53124109dee2d744777979a0553999cb276cb6f538a17ddce6516e3da8b",
|
||||||
@ -23,8 +23,8 @@
|
|||||||
"MachineLearning/CameraDirector/trajectory_quality.py": "20cd2ddc1b2349f8c33f6d1460580f37964a521eaa86ceee5b8ae457512ba49d"
|
"MachineLearning/CameraDirector/trajectory_quality.py": "20cd2ddc1b2349f8c33f6d1460580f37964a521eaa86ceee5b8ae457512ba49d"
|
||||||
},
|
},
|
||||||
"preparationLogicIdentifier": "acba2a46d9d979b013b7154bada6b6a8a7adfd7acd1721876ce6699ed4e03e84",
|
"preparationLogicIdentifier": "acba2a46d9d979b013b7154bada6b6a8a7adfd7acd1721876ce6699ed4e03e84",
|
||||||
"candidateLogicIdentifier": "7c771be40d4c6ebc6b15a96c64ed05062f6109d20605670be9b0e07587f2b25a",
|
"candidateLogicIdentifier": "bb9e883085df6a95de09f2181191c83c13cf1b3adff5b64e41b3d60d99350522",
|
||||||
"generationCodeIdentifier": "470c77fe5d69046aa7bcadfbbb1337d6f0025fe0be374cb2302ea41ae00cdc47",
|
"generationCodeIdentifier": "8973f4642cd18f5645d33871c544e5394bb538ec5db86d4abc0536f50a00a9ed",
|
||||||
"generationCodeFiles": [
|
"generationCodeFiles": [
|
||||||
"adjacent_transition.py",
|
"adjacent_transition.py",
|
||||||
"camera_kinematics.py",
|
"camera_kinematics.py",
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import numpy as np
|
|||||||
|
|
||||||
|
|
||||||
TRANSLATION_KINEMATIC_POLICY_VERSION = (
|
TRANSLATION_KINEMATIC_POLICY_VERSION = (
|
||||||
"yamo-angle-aware-turn-v6-natural-easing-impulse-c2"
|
"yamo-angle-aware-turn-v7-local-impulse-authored-dynamics-guard"
|
||||||
)
|
)
|
||||||
DEFAULT_REVERSAL_ANGLE_DEGREES = 40.0
|
DEFAULT_REVERSAL_ANGLE_DEGREES = 40.0
|
||||||
DEFAULT_STOP_SPEED_RATIO = 0.12
|
DEFAULT_STOP_SPEED_RATIO = 0.12
|
||||||
@ -38,25 +38,33 @@ DEFAULT_SPEED_RATIO_TOLERANCE = 0.025
|
|||||||
DEFAULT_ANGLE_COMPARISON_TOLERANCE_DEGREES = 1e-3
|
DEFAULT_ANGLE_COMPARISON_TOLERANCE_DEGREES = 1e-3
|
||||||
DEFAULT_MINIMUM_ORBIT_ANGULAR_SPEED_RADIANS_PER_SECOND = math.radians(2.0)
|
DEFAULT_MINIMUM_ORBIT_ANGULAR_SPEED_RADIANS_PER_SECOND = math.radians(2.0)
|
||||||
DEFAULT_MAXIMUM_ENDPOINT_ADJUSTMENT_METERS = 0.75
|
DEFAULT_MAXIMUM_ENDPOINT_ADJUSTMENT_METERS = 0.75
|
||||||
DEFAULT_MAXIMUM_ACCELERATION_REGRESSION_RATIO = 1.25
|
# A regularizer is a repair pass, not another motion authoring stage. The old
|
||||||
DEFAULT_MAXIMUM_ACCELERATION_REGRESSION_DELTA_MPS2 = 8.0
|
# 25 m/s2 and 350 m/s3 absolute floors let a quiet authored curve become several
|
||||||
DEFAULT_ACCELERATION_GUARD_FLOOR_MPS2 = 25.0
|
# times rougher while still reporting a successful repair (the reviewed
|
||||||
DEFAULT_MAXIMUM_JERK_REGRESSION_RATIO = 1.35
|
# Shot_066 class rose from roughly 255 to 548 m/s3). Keep only small numerical
|
||||||
DEFAULT_MAXIMUM_JERK_REGRESSION_DELTA_MPS3 = 350.0
|
# headroom: a rewrite may trade at most five percent of either peak, and the
|
||||||
DEFAULT_JERK_GUARD_FLOOR_MPS3 = 350.0
|
# absolute allowance is deliberately tiny for already-quiet paths.
|
||||||
|
DEFAULT_MAXIMUM_ACCELERATION_REGRESSION_RATIO = 1.05
|
||||||
|
DEFAULT_MAXIMUM_ACCELERATION_REGRESSION_DELTA_MPS2 = 1.0
|
||||||
|
DEFAULT_ACCELERATION_GUARD_FLOOR_MPS2 = 0.5
|
||||||
|
DEFAULT_MAXIMUM_JERK_REGRESSION_RATIO = 1.05
|
||||||
|
DEFAULT_MAXIMUM_JERK_REGRESSION_DELTA_MPS3 = 15.0
|
||||||
|
DEFAULT_JERK_GUARD_FLOOR_MPS3 = 10.0
|
||||||
# A second, deliberately narrow gate catches a concentrated turn that evades
|
# A second, deliberately narrow gate catches a concentrated turn that evades
|
||||||
# the 40-degree rule yet still carries an edit-visible sideways impulse. The
|
# the 40-degree rule yet still carries an edit-visible sideways impulse. The
|
||||||
# thresholds were calibrated against 2,479 authored turn events: the combined
|
# thresholds were calibrated against 2,479 authored turn events: the combined
|
||||||
# gate adds only 10 events (0.4%) beyond the existing abrupt-turn policy while
|
# gate adds only 10 events (0.4%) beyond the existing abrupt-turn policy while
|
||||||
# covering the reviewed Shot_066 profile (high stop ratio plus ~22 m/s^2 and
|
# covering the reviewed Shot_066 profile (high stop ratio plus ~22 m/s^2 and
|
||||||
# ~200 m/s^3 local lateral dynamics).
|
# ~200 m/s^3 local lateral dynamics). The zero-net-direction extension was
|
||||||
|
# separately scanned across 3,054 authored shots: it admitted one cut-boundary
|
||||||
|
# discontinuity, which the authored-dynamics guard correctly rejected.
|
||||||
DEFAULT_IMPULSIVE_TURN_ANGLE_DEGREES = 15.0
|
DEFAULT_IMPULSIVE_TURN_ANGLE_DEGREES = 15.0
|
||||||
DEFAULT_IMPULSIVE_STOP_SPEED_RATIO = 0.78
|
DEFAULT_IMPULSIVE_STOP_SPEED_RATIO = 0.78
|
||||||
DEFAULT_IMPULSIVE_CORNER_CONCENTRATION = 0.40
|
DEFAULT_IMPULSIVE_CORNER_CONCENTRATION = 0.40
|
||||||
DEFAULT_IMPULSIVE_LATERAL_ACCELERATION_MPS2 = 22.0
|
DEFAULT_IMPULSIVE_LATERAL_ACCELERATION_MPS2 = 22.0
|
||||||
DEFAULT_IMPULSIVE_LATERAL_JERK_MPS3 = 200.0
|
DEFAULT_IMPULSIVE_LATERAL_JERK_MPS3 = 200.0
|
||||||
DEFAULT_IMPULSIVE_TARGET_SPEED_RATIO = 0.72
|
DEFAULT_IMPULSIVE_TARGET_SPEED_RATIO = 0.72
|
||||||
DEFAULT_IMPULSIVE_REVERSAL_WINDOW_SECONDS = 0.50
|
DEFAULT_IMPULSIVE_REVERSAL_WINDOW_SECONDS = 0.55
|
||||||
# A generated curve can satisfy the broad monotonic-speed definition of
|
# A generated curve can satisfy the broad monotonic-speed definition of
|
||||||
# ``natural_easing`` while its turn is still concentrated enough to read as a
|
# ``natural_easing`` while its turn is still concentrated enough to read as a
|
||||||
# lateral kick. Keep this as a separate, narrower contradiction gate rather
|
# lateral kick. Keep this as a separate, narrower contradiction gate rather
|
||||||
@ -492,10 +500,16 @@ def analyze_translational_reversals(
|
|||||||
lateral_jerk_peaks[result_index] = float(
|
lateral_jerk_peaks[result_index] = float(
|
||||||
np.max(np.linalg.norm(lateral, axis=1))
|
np.max(np.linalg.norm(lateral, axis=1))
|
||||||
)
|
)
|
||||||
|
# A one- or two-frame sideways kick can return to the original travel
|
||||||
|
# direction, leaving the persistent flank angle close to zero. Use the
|
||||||
|
# absolute adjacent-frame turn as a second angle witness. The existing
|
||||||
|
# coherence, travel, concentration, acceleration and jerk gates keep a
|
||||||
|
# deliberate distributed dolly/truck/arc from being classified as an
|
||||||
|
# impulse; continuous orbit motion remains explicitly excluded.
|
||||||
impulsive_valid = (
|
impulsive_valid = (
|
||||||
(flank_speeds >= minimum_flank_speed_mps)
|
(flank_speeds >= minimum_flank_speed_mps)
|
||||||
& (
|
& (
|
||||||
angles
|
np.maximum(angles, peak_corner_angles)
|
||||||
>= DEFAULT_IMPULSIVE_TURN_ANGLE_DEGREES
|
>= DEFAULT_IMPULSIVE_TURN_ANGLE_DEGREES
|
||||||
- DEFAULT_ANGLE_COMPARISON_TOLERANCE_DEGREES
|
- DEFAULT_ANGLE_COMPARISON_TOLERANCE_DEGREES
|
||||||
)
|
)
|
||||||
@ -793,6 +807,26 @@ def _dynamics_regression_exceeds_guard(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _local_translation_dynamics_metrics(
|
||||||
|
positions_meters: np.ndarray,
|
||||||
|
left: int,
|
||||||
|
right: int,
|
||||||
|
sample_rate: float,
|
||||||
|
) -> dict[str, float]:
|
||||||
|
"""Measure a rewrite window including every finite-difference seam.
|
||||||
|
|
||||||
|
A shot-wide maximum can hide a new local kick when some unrelated frame is
|
||||||
|
already rougher. Three samples on either side cover the velocity,
|
||||||
|
acceleration and jerk stencils, so this local measurement makes the guard
|
||||||
|
sensitive to the exact neighbourhood changed by ``_ease_through_stop``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
positions = _validated_positions(positions_meters)
|
||||||
|
start = max(0, int(left) - 3)
|
||||||
|
stop = min(len(positions), int(right) + 4)
|
||||||
|
return translation_dynamics_metrics(positions[start:stop], sample_rate)
|
||||||
|
|
||||||
|
|
||||||
def _event_metadata(events: list[TranslationReversal]) -> dict[str, object]:
|
def _event_metadata(events: list[TranslationReversal]) -> dict[str, object]:
|
||||||
abrupt = [event for event in events if event.abrupt]
|
abrupt = [event for event in events if event.abrupt]
|
||||||
impulsive = [event for event in events if event.impulsive]
|
impulsive = [event for event in events if event.impulsive]
|
||||||
@ -859,6 +893,12 @@ def regularize_translational_reversals(
|
|||||||
applied_angles: list[float] = []
|
applied_angles: list[float] = []
|
||||||
applied_target_speed_ratios: list[float] = []
|
applied_target_speed_ratios: list[float] = []
|
||||||
applied_impulsive_frames: list[int] = []
|
applied_impulsive_frames: list[int] = []
|
||||||
|
applied_window_radii_frames: list[int] = []
|
||||||
|
attempted_regularized_frames: list[int] = []
|
||||||
|
dynamics_guard_rejected_frames: list[int] = []
|
||||||
|
adaptive_radius_frames: list[int] = []
|
||||||
|
dynamics_before = translation_dynamics_metrics(positions, sample_rate)
|
||||||
|
attempted_dynamics_after = dict(dynamics_before)
|
||||||
|
|
||||||
pass_count = 0
|
pass_count = 0
|
||||||
pending_events = abrupt_before if apply_policy else []
|
pending_events = abrupt_before if apply_policy else []
|
||||||
@ -884,11 +924,12 @@ def regularize_translational_reversals(
|
|||||||
# The high-impulse fallback is intentionally rare, so give its
|
# The high-impulse fallback is intentionally rare, so give its
|
||||||
# braking envelope a slightly longer C2 runway instead of forcing
|
# braking envelope a slightly longer C2 runway instead of forcing
|
||||||
# a stronger speed change into the normal 0.35-second window.
|
# a stronger speed change into the normal 0.35-second window.
|
||||||
neighbor_limit = (
|
preferred_radius = (
|
||||||
impulsive_requested_radius
|
impulsive_requested_radius
|
||||||
if event.impulsive
|
if event.impulsive
|
||||||
else requested_radius
|
else requested_radius
|
||||||
)
|
)
|
||||||
|
neighbor_limit = min(event.frame, len(output) - 1 - event.frame)
|
||||||
if event_index > 0:
|
if event_index > 0:
|
||||||
neighbor_limit = min(
|
neighbor_limit = min(
|
||||||
neighbor_limit,
|
neighbor_limit,
|
||||||
@ -899,23 +940,104 @@ def regularize_translational_reversals(
|
|||||||
neighbor_limit,
|
neighbor_limit,
|
||||||
(event_frames[event_index + 1] - event.frame) // 2,
|
(event_frames[event_index + 1] - event.frame) // 2,
|
||||||
)
|
)
|
||||||
radius = min(
|
preferred_radius = min(preferred_radius, neighbor_limit)
|
||||||
neighbor_limit,
|
if preferred_radius < 4:
|
||||||
event.frame,
|
|
||||||
len(output) - 1 - event.frame,
|
|
||||||
)
|
|
||||||
if radius < 4:
|
|
||||||
continue
|
continue
|
||||||
output = _ease_through_stop(
|
|
||||||
output,
|
attempted_regularized_frames.append(event.frame)
|
||||||
event.frame,
|
# First retain the established window. If its C2 rewrite is still
|
||||||
radius,
|
# locally rougher than the authored input, spend at most another
|
||||||
target_speed_ratio=event.target_speed_ratio,
|
# quarter second on the runway before giving up. This adaptive
|
||||||
turn_angle_degrees=event.angle_degrees,
|
# path fixes boundary-limited cases such as Shot_066 without
|
||||||
|
# weakening the dynamics contract or flattening every motion.
|
||||||
|
extension_step = max(2, int(round(0.05 * sample_rate)))
|
||||||
|
maximum_adaptive_radius = min(
|
||||||
|
neighbor_limit,
|
||||||
|
preferred_radius + max(4, int(round(0.25 * sample_rate))),
|
||||||
)
|
)
|
||||||
|
candidate_radii = list(
|
||||||
|
range(
|
||||||
|
preferred_radius,
|
||||||
|
maximum_adaptive_radius + 1,
|
||||||
|
extension_step,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if candidate_radii[-1] != maximum_adaptive_radius:
|
||||||
|
candidate_radii.append(maximum_adaptive_radius)
|
||||||
|
|
||||||
|
# Keep the authored comparison region fixed while trying wider
|
||||||
|
# runways. If this ROI grew with ``radius``, an unrelated rough
|
||||||
|
# frame newly entering (for example at r=27 after r=21 failed)
|
||||||
|
# could raise the baseline maximum and hide a fresh center kick.
|
||||||
|
fixed_local_before = _local_translation_dynamics_metrics(
|
||||||
|
output,
|
||||||
|
event.frame - preferred_radius,
|
||||||
|
event.frame + preferred_radius,
|
||||||
|
sample_rate,
|
||||||
|
)
|
||||||
|
accepted_output: np.ndarray | None = None
|
||||||
|
accepted_radius = preferred_radius
|
||||||
|
for radius in candidate_radii:
|
||||||
|
candidate = _ease_through_stop(
|
||||||
|
output,
|
||||||
|
event.frame,
|
||||||
|
radius,
|
||||||
|
target_speed_ratio=event.target_speed_ratio,
|
||||||
|
turn_angle_degrees=event.angle_degrees,
|
||||||
|
)
|
||||||
|
local_before = _local_translation_dynamics_metrics(
|
||||||
|
output,
|
||||||
|
event.frame - radius,
|
||||||
|
event.frame + radius,
|
||||||
|
sample_rate,
|
||||||
|
)
|
||||||
|
local_after = _local_translation_dynamics_metrics(
|
||||||
|
candidate,
|
||||||
|
event.frame - radius,
|
||||||
|
event.frame + radius,
|
||||||
|
sample_rate,
|
||||||
|
)
|
||||||
|
fixed_local_after = _local_translation_dynamics_metrics(
|
||||||
|
candidate,
|
||||||
|
event.frame - preferred_radius,
|
||||||
|
event.frame + preferred_radius,
|
||||||
|
sample_rate,
|
||||||
|
)
|
||||||
|
candidate_dynamics = translation_dynamics_metrics(
|
||||||
|
candidate,
|
||||||
|
sample_rate,
|
||||||
|
)
|
||||||
|
for key in attempted_dynamics_after:
|
||||||
|
attempted_dynamics_after[key] = max(
|
||||||
|
attempted_dynamics_after[key],
|
||||||
|
candidate_dynamics[key],
|
||||||
|
)
|
||||||
|
if _dynamics_regression_exceeds_guard(local_before, local_after):
|
||||||
|
continue
|
||||||
|
if _dynamics_regression_exceeds_guard(
|
||||||
|
fixed_local_before,
|
||||||
|
fixed_local_after,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
if _dynamics_regression_exceeds_guard(
|
||||||
|
dynamics_before,
|
||||||
|
candidate_dynamics,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
accepted_output = candidate
|
||||||
|
accepted_radius = radius
|
||||||
|
break
|
||||||
|
|
||||||
|
if accepted_output is None:
|
||||||
|
dynamics_guard_rejected_frames.append(event.frame)
|
||||||
|
continue
|
||||||
|
output = accepted_output
|
||||||
|
if accepted_radius != preferred_radius:
|
||||||
|
adaptive_radius_frames.append(event.frame)
|
||||||
applied_frames.append(event.frame)
|
applied_frames.append(event.frame)
|
||||||
applied_angles.append(event.angle_degrees)
|
applied_angles.append(event.angle_degrees)
|
||||||
applied_target_speed_ratios.append(event.target_speed_ratio)
|
applied_target_speed_ratios.append(event.target_speed_ratio)
|
||||||
|
applied_window_radii_frames.append(accepted_radius)
|
||||||
if event.impulsive:
|
if event.impulsive:
|
||||||
applied_impulsive_frames.append(event.frame)
|
applied_impulsive_frames.append(event.frame)
|
||||||
pass_count = int(bool(applied_frames))
|
pass_count = int(bool(applied_frames))
|
||||||
@ -934,25 +1056,38 @@ def regularize_translational_reversals(
|
|||||||
# bounded spatially and remain eligible for the unresolved-turn penalty.
|
# bounded spatially and remain eligible for the unresolved-turn penalty.
|
||||||
output = positions + (output - positions) * correction_scale
|
output = positions + (output - positions) * correction_scale
|
||||||
|
|
||||||
dynamics_before = translation_dynamics_metrics(positions, sample_rate)
|
final_attempted_dynamics = translation_dynamics_metrics(output, sample_rate)
|
||||||
attempted_dynamics_after = translation_dynamics_metrics(output, sample_rate)
|
for key in attempted_dynamics_after:
|
||||||
attempted_regularized_frames = list(applied_frames)
|
attempted_dynamics_after[key] = max(
|
||||||
dynamics_guard_triggered = bool(applied_frames) and (
|
attempted_dynamics_after[key],
|
||||||
|
final_attempted_dynamics[key],
|
||||||
|
)
|
||||||
|
final_dynamics_guard_triggered = bool(applied_frames) and (
|
||||||
_dynamics_regression_exceeds_guard(
|
_dynamics_regression_exceeds_guard(
|
||||||
dynamics_before,
|
dynamics_before,
|
||||||
attempted_dynamics_after,
|
final_attempted_dynamics,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if dynamics_guard_triggered:
|
dynamics_guard_triggered = bool(
|
||||||
|
dynamics_guard_rejected_frames or final_dynamics_guard_triggered
|
||||||
|
)
|
||||||
|
if final_dynamics_guard_triggered:
|
||||||
# A rejected rewrite remains an abrupt candidate and is therefore
|
# A rejected rewrite remains an abrupt candidate and is therefore
|
||||||
# demoted by the normal selection penalty. Keeping the untouched curve
|
# demoted by the normal selection penalty. Keeping the untouched curve
|
||||||
# is safer than publishing a numerically "resolved" turn with an
|
# is safer than publishing a numerically "resolved" turn with an
|
||||||
# edit-visible acceleration or jerk seam.
|
# edit-visible acceleration or jerk seam.
|
||||||
|
dynamics_guard_rejected_frames.extend(
|
||||||
|
frame
|
||||||
|
for frame in applied_frames
|
||||||
|
if frame not in dynamics_guard_rejected_frames
|
||||||
|
)
|
||||||
output = positions.copy()
|
output = positions.copy()
|
||||||
applied_frames.clear()
|
applied_frames.clear()
|
||||||
applied_angles.clear()
|
applied_angles.clear()
|
||||||
applied_target_speed_ratios.clear()
|
applied_target_speed_ratios.clear()
|
||||||
applied_impulsive_frames.clear()
|
applied_impulsive_frames.clear()
|
||||||
|
applied_window_radii_frames.clear()
|
||||||
|
adaptive_radius_frames.clear()
|
||||||
pass_count = 0
|
pass_count = 0
|
||||||
endpoint_adjustment_clamped = False
|
endpoint_adjustment_clamped = False
|
||||||
|
|
||||||
@ -981,6 +1116,10 @@ def regularize_translational_reversals(
|
|||||||
"translationKinematicAttemptedReversalFrames": (
|
"translationKinematicAttemptedReversalFrames": (
|
||||||
attempted_regularized_frames
|
attempted_regularized_frames
|
||||||
),
|
),
|
||||||
|
"translationKinematicDynamicsGuardRejectedFrames": (
|
||||||
|
dynamics_guard_rejected_frames
|
||||||
|
),
|
||||||
|
"translationKinematicAdaptiveRadiusFrames": adaptive_radius_frames,
|
||||||
"translationDirectionReversalCountBefore": before_metadata["count"],
|
"translationDirectionReversalCountBefore": before_metadata["count"],
|
||||||
"translationAbruptReversalCountBefore": before_metadata["abruptCount"],
|
"translationAbruptReversalCountBefore": before_metadata["abruptCount"],
|
||||||
"translationDirectionReversalFramesBefore": before_metadata["frames"],
|
"translationDirectionReversalFramesBefore": before_metadata["frames"],
|
||||||
@ -1012,6 +1151,9 @@ def regularize_translational_reversals(
|
|||||||
"translationRegularizedReversalFrames": applied_frames,
|
"translationRegularizedReversalFrames": applied_frames,
|
||||||
"translationRegularizedTurnAnglesDegrees": applied_angles,
|
"translationRegularizedTurnAnglesDegrees": applied_angles,
|
||||||
"translationRegularizedTargetSpeedRatios": (applied_target_speed_ratios),
|
"translationRegularizedTargetSpeedRatios": (applied_target_speed_ratios),
|
||||||
|
"translationRegularizedWindowRadiiFrames": (
|
||||||
|
applied_window_radii_frames
|
||||||
|
),
|
||||||
"translationImpulsiveTurnCountBefore": before_metadata[
|
"translationImpulsiveTurnCountBefore": before_metadata[
|
||||||
"impulsiveCount"
|
"impulsiveCount"
|
||||||
],
|
],
|
||||||
|
|||||||
@ -19,7 +19,7 @@ from typing import Any, Iterable
|
|||||||
|
|
||||||
BUILD_IDENTITY_SCHEMA_VERSION = "cw-camera-worker-build-identity-v1"
|
BUILD_IDENTITY_SCHEMA_VERSION = "cw-camera-worker-build-identity-v1"
|
||||||
BUILD_IDENTITY_FILE = "cw_camera_worker_build_identity.json"
|
BUILD_IDENTITY_FILE = "cw_camera_worker_build_identity.json"
|
||||||
WORKER_VERSION = "0.1.5"
|
WORKER_VERSION = "0.1.6"
|
||||||
|
|
||||||
|
|
||||||
def is_frozen_runtime() -> bool:
|
def is_frozen_runtime() -> bool:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
|||||||
../../Scripts/wheel.exe,sha256=A69vg7y22iGuDMaqOWKEJzU_3jcjugdke2okLg6B5Cg,108448
|
../../Scripts/wheel.exe,sha256=A40JvpbJRmAo327Gh4g5jNGRWEobaBMdktVtpIqzJAk,108448
|
||||||
wheel-0.45.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
wheel-0.45.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
wheel-0.45.1.dist-info/LICENSE.txt,sha256=MMI2GGeRCPPo6h0qZYx8pBe9_IkcmO8aifpP8MmChlQ,1107
|
wheel-0.45.1.dist-info/LICENSE.txt,sha256=MMI2GGeRCPPo6h0qZYx8pBe9_IkcmO8aifpP8MmChlQ,1107
|
||||||
wheel-0.45.1.dist-info/METADATA,sha256=mKz84H7m7jsxJyzeIcTVORiTb0NPMV39KvOIYhGgmjA,2313
|
wheel-0.45.1.dist-info/METADATA,sha256=mKz84H7m7jsxJyzeIcTVORiTb0NPMV39KvOIYhGgmjA,2313
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": "cw-camera-worker-distribution-build-v1",
|
"schemaVersion": "cw-camera-worker-distribution-build-v1",
|
||||||
"createdUtc": "2026-08-08T20:03:52.1316389Z",
|
"createdUtc": "2026-08-09T07:15:49.8790622Z",
|
||||||
"workerVersion": "0.1.5",
|
"workerVersion": "0.1.6",
|
||||||
"protocolVersion": "1",
|
"protocolVersion": "1",
|
||||||
"buildIdentitySchemaVersion": "cw-camera-worker-build-identity-v1",
|
"buildIdentitySchemaVersion": "cw-camera-worker-build-identity-v1",
|
||||||
"buildEnvironment": {
|
"buildEnvironment": {
|
||||||
@ -65,9 +65,9 @@
|
|||||||
],
|
],
|
||||||
"payload": {
|
"payload": {
|
||||||
"fileCount": 646,
|
"fileCount": 646,
|
||||||
"bytes": 267603627,
|
"bytes": 267657984,
|
||||||
"executableSha256": "055fb16b60454b0d131ed2201eccad8d11977bd99ef011468d44638b33e4a1ef",
|
"executableSha256": "09901a07746f98e94d3a4307df83b37044732e15b5001cdc9c7a67133d520847",
|
||||||
"buildIdentitySha256": "84428e90a523ec40aa54d45c7b795e586979fce6f4a0e139e0bd83fc3c0ce906",
|
"buildIdentitySha256": "34120be099cf1355cafb9af96cdb355ca9cd1b69651f20a050a1e30db1f3948e",
|
||||||
"sanityRange": {
|
"sanityRange": {
|
||||||
"minimumFileCount": 500,
|
"minimumFileCount": 500,
|
||||||
"maximumFileCount": 1000,
|
"maximumFileCount": 1000,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.mingle.cw-ai",
|
"name": "com.mingle.cw-ai",
|
||||||
"version": "0.4.9",
|
"version": "0.4.10",
|
||||||
"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",
|
||||||
|
|||||||
@ -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.15
|
https://kindnick-git.duckdns.org/mingle/streamingle-unity-utilities.git?path=/CameraAI~#v0.1.16
|
||||||
```
|
```
|
||||||
|
|
||||||
The Camera AI package includes the complete Windows x64
|
The Camera AI package includes the complete Windows x64
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user