Add Unity Pipeline package support
This commit is contained in:
+42
@@ -0,0 +1,42 @@
|
||||
using System.Linq;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.Baking
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared helpers for the CLI-215 bake commands (lighting / NavMesh / occlusion). All three bakes
|
||||
/// act on the <b>currently open scene(s)</b> — there is no per-asset target — so before triggering
|
||||
/// any bake we verify at least one open, saved scene exists. A bake against no open/saved scene is
|
||||
/// rejected up front with the documented <c>{ code: "no_scene" }</c> result (returned, not thrown,
|
||||
/// to match the CLI-215 spec), and nothing is started.
|
||||
/// </summary>
|
||||
internal static class BakeSceneGuard
|
||||
{
|
||||
/// <summary>The structured "no_scene" error payload returned by a trigger with no bakeable scene.</summary>
|
||||
internal static object NoSceneResult()
|
||||
{
|
||||
return new
|
||||
{
|
||||
code = "no_scene",
|
||||
message = "No open, saved scene to bake. Open a scene first (open_scene) and ensure it is saved to disk."
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when there is at least one open, loaded scene that has been saved to disk (a non-empty
|
||||
/// path). An untitled/never-saved scene has an empty <see cref="Scene.path"/> and cannot host a
|
||||
/// bake (its data has nowhere to live), so it does not count.
|
||||
/// </summary>
|
||||
internal static bool HasBakeableScene()
|
||||
{
|
||||
return OpenScenes().Any(s => s.isLoaded && !string.IsNullOrEmpty(s.path));
|
||||
}
|
||||
|
||||
/// <summary>Enumerate every open scene by index.</summary>
|
||||
internal static System.Collections.Generic.IEnumerable<Scene> OpenScenes()
|
||||
{
|
||||
for (int i = 0; i < SceneManager.sceneCount; i++)
|
||||
yield return SceneManager.GetSceneAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf05be2261cb475c9cf3d7fccbf65ff5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+640
@@ -0,0 +1,640 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Unity.Pipeline.Commands;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.Baking
|
||||
{
|
||||
/// <summary>
|
||||
/// CLI-215 Group A — Lightmapping bake commands. Trigger an async lightmap bake of the open
|
||||
/// scene(s), poll its status, cancel it, clear baked data, and read/configure the active
|
||||
/// <see cref="LightingSettings"/>.
|
||||
///
|
||||
/// Why async (trigger-then-poll, mirroring <see cref="BuildCommand"/>): a full lightmap bake runs
|
||||
/// for seconds-to-minutes. <see cref="Lightmapping.BakeAsync"/> kicks the bake off and returns
|
||||
/// immediately; <c>bake_lighting</c> therefore returns <c>{ status: "baking", bakeId }</c> right
|
||||
/// away and the caller polls <c>lighting_bake_status</c> until it reports <c>completed</c>.
|
||||
///
|
||||
/// In-flight state is held in static fields and is domain-reload aware: a bake can survive (or
|
||||
/// resume across) a domain reload, so on reload <see cref="OnReload"/> reconciles our recorded
|
||||
/// state against the live <see cref="Lightmapping.isRunning"/> flag and a small status file under
|
||||
/// Temp/ (which persists across the reload, mirroring RecompileCommand). Completion is detected via
|
||||
/// the <see cref="Lightmapping.bakeCompleted"/> callback and, defensively, by polling
|
||||
/// <see cref="Lightmapping.isRunning"/> flipping false in <see cref="EditorApplication.update"/>.
|
||||
/// </summary>
|
||||
[InitializeOnLoad]
|
||||
public static class LightingBakeCommands
|
||||
{
|
||||
const string StatusFile = "Temp/pipeline_lighting_bake_status.json";
|
||||
|
||||
static readonly object s_Lock = new object();
|
||||
|
||||
// True between trigger and the completion callback / isRunning flipping false.
|
||||
static bool s_Tracking;
|
||||
static string s_BakeId;
|
||||
static DateTime s_StartedUtc;
|
||||
|
||||
static LightingBakeCommands()
|
||||
{
|
||||
// Reconcile any in-flight bake recorded before a domain reload, then keep watching.
|
||||
OnReload();
|
||||
Lightmapping.bakeCompleted += OnBakeCompleted;
|
||||
EditorApplication.update += OnUpdate;
|
||||
}
|
||||
|
||||
#region bake_lighting
|
||||
|
||||
[CliCommand("bake_lighting", "Trigger an async lightmap bake of the open scene(s) via Lightmapping.BakeAsync(). Returns immediately; poll lighting_bake_status until completed.")]
|
||||
public static object BakeLighting(
|
||||
[CliArg("confirm", "Recommended (true): a bake overwrites existing lightmap data. Accepted for parity; not required.")] bool confirm = false,
|
||||
[CliArg("dry_run", "If true, validate there is an open bakeable scene and return the current lighting settings without baking.")] bool dryRun = false)
|
||||
{
|
||||
if (!BakeSceneGuard.HasBakeableScene())
|
||||
return BakeSceneGuard.NoSceneResult();
|
||||
|
||||
// Reconcile before the in-progress check so a stale flag from a finished/cancelled bake
|
||||
// doesn't wrongly report bake_in_progress.
|
||||
OnUpdate();
|
||||
|
||||
if (Lightmapping.isRunning)
|
||||
return new { code = "bake_in_progress", message = "A lighting bake is already running. Poll lighting_bake_status." };
|
||||
|
||||
if (dryRun)
|
||||
return new { status = "dry_run", wouldBake = true, settings = ReadLightingSettings() };
|
||||
|
||||
var bakeId = Guid.NewGuid().ToString("N");
|
||||
lock (s_Lock)
|
||||
{
|
||||
s_Tracking = true;
|
||||
s_BakeId = bakeId;
|
||||
s_StartedUtc = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
WriteStatus(new LightingBakeStatus
|
||||
{
|
||||
Status = "baking",
|
||||
BakeId = bakeId,
|
||||
IsBaking = true,
|
||||
StartedUtcTicks = s_StartedUtc.Ticks
|
||||
});
|
||||
|
||||
// BakeAsync returns false if the bake could not be started (e.g. another job in flight).
|
||||
if (!Lightmapping.BakeAsync())
|
||||
{
|
||||
lock (s_Lock) { s_Tracking = false; }
|
||||
WriteStatus(new LightingBakeStatus { Status = "idle" });
|
||||
return new { code = "bake_in_progress", message = "Lightmapping.BakeAsync() refused to start (a bake may already be running)." };
|
||||
}
|
||||
|
||||
return new { status = "baking", bakeId };
|
||||
}
|
||||
|
||||
[CliCommand("lighting_bake_status", "Get the status of the last lighting bake: idle | baking | completed.", MainThreadRequired = false)]
|
||||
public static string LightingBakeStatus()
|
||||
{
|
||||
if (File.Exists(StatusFile))
|
||||
return File.ReadAllText(StatusFile);
|
||||
return "{\"status\":\"idle\"}";
|
||||
}
|
||||
|
||||
[CliCommand("cancel_lighting_bake", "Cancel an in-progress lighting bake (Lightmapping.Cancel()).")]
|
||||
public static object CancelLightingBake()
|
||||
{
|
||||
var wasRunning = Lightmapping.isRunning;
|
||||
if (wasRunning)
|
||||
Lightmapping.Cancel();
|
||||
|
||||
lock (s_Lock)
|
||||
{
|
||||
if (s_Tracking)
|
||||
{
|
||||
var ms = (int)Math.Max(0, (DateTime.UtcNow - s_StartedUtc).TotalMilliseconds);
|
||||
WriteStatus(new LightingBakeStatus
|
||||
{
|
||||
Status = "completed",
|
||||
Result = "cancelled",
|
||||
BakeId = s_BakeId,
|
||||
BakeTimeMs = ms
|
||||
});
|
||||
s_Tracking = false;
|
||||
}
|
||||
}
|
||||
|
||||
return new { cancelled = wasRunning };
|
||||
}
|
||||
|
||||
[CliCommand("clear_baked_lighting", "Clear baked lightmap data for the open scene(s). Destructive: requires confirm=true.")]
|
||||
public static object ClearBakedLighting(
|
||||
[CliArg("confirm", "Must be true to actually clear (destructive, not undoable via Unity's Undo).")] bool confirm = false,
|
||||
[CliArg("include_disk_cache", "If true, also clear the GI disk cache (Lightmapping.ClearDiskCache()).")] bool includeDiskCache = false,
|
||||
[CliArg("dry_run", "If true, report what would be cleared without clearing.")] bool dryRun = false)
|
||||
{
|
||||
if (!confirm)
|
||||
throw new ArgumentException("Refusing to clear baked lighting. Pass confirm=true (destructive, not undoable via Unity's Undo).");
|
||||
|
||||
if (dryRun)
|
||||
return new { status = "dry_run", wouldClear = true, includeDiskCache };
|
||||
|
||||
Lightmapping.Clear();
|
||||
if (includeDiskCache)
|
||||
Lightmapping.ClearDiskCache();
|
||||
|
||||
return new { cleared = true, includeDiskCache };
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region settings
|
||||
|
||||
[CliCommand("get_lighting_settings", "Read the active LightingSettings (lightmapper, bounces, resolution, directional mode, AO, etc.).")]
|
||||
public static LightingSettingsResult GetLightingSettings()
|
||||
{
|
||||
return ReadLightingSettings();
|
||||
}
|
||||
|
||||
[CliCommand("set_lighting_settings", "Apply a subset of lighting settings to the active LightingSettings. Returns { applied[], unknown[] }.")]
|
||||
public static object SetLightingSettings(
|
||||
[CliArg("settings", "JSON object with a subset of lighting fields to set (same names/enums as get_lighting_settings).", Required = true)] JObject settings,
|
||||
[CliArg("dry_run", "If true, validate the keys and report applied/unknown without changing anything.")] bool dryRun = false)
|
||||
{
|
||||
if (settings == null)
|
||||
throw new ArgumentException("settings is required (a JSON object of lighting fields to set).");
|
||||
|
||||
// In dry_run we must not create or assign a LightingSettings asset (no writes). When no
|
||||
// settings exist yet, validate the requested keys against a transient throwaway instance
|
||||
// that is never assigned, so applied/unknown is still reported without persisting anything.
|
||||
var ls = ActiveLightingSettings(allowCreate: !dryRun, out var source);
|
||||
LightingSettings transient = null;
|
||||
if (ls == null)
|
||||
{
|
||||
transient = new LightingSettings { name = "PipelineLightingSettings (dry-run)" };
|
||||
ls = transient;
|
||||
source = "none";
|
||||
}
|
||||
|
||||
var applied = new System.Collections.Generic.List<string>();
|
||||
var unknown = new System.Collections.Generic.List<string>();
|
||||
|
||||
foreach (var prop in settings.Properties())
|
||||
{
|
||||
if (TryApply(ls, prop.Name, prop.Value, dryRun))
|
||||
applied.Add(prop.Name);
|
||||
else
|
||||
unknown.Add(prop.Name);
|
||||
}
|
||||
|
||||
if (!dryRun && applied.Count > 0)
|
||||
EditorUtility.SetDirty(ls);
|
||||
|
||||
// The transient dry-run instance is never assigned anywhere; destroy it so it doesn't leak.
|
||||
if (transient != null)
|
||||
UnityEngine.Object.DestroyImmediate(transient);
|
||||
|
||||
return new
|
||||
{
|
||||
applied = applied.ToArray(),
|
||||
unknown = unknown.ToArray(),
|
||||
settingsSource = source,
|
||||
dryRun
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region completion / reload bookkeeping
|
||||
|
||||
static void OnBakeCompleted()
|
||||
{
|
||||
lock (s_Lock)
|
||||
{
|
||||
if (!s_Tracking)
|
||||
return;
|
||||
Finish("succeeded");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defensive completion detector: if we are tracking a bake but Lightmapping reports it is no
|
||||
/// longer running and the completion callback didn't fire (e.g. a cancel, or a bake that ended
|
||||
/// during a frame we didn't get the callback), finalize the status here.
|
||||
/// </summary>
|
||||
static void OnUpdate()
|
||||
{
|
||||
lock (s_Lock)
|
||||
{
|
||||
if (s_Tracking && !Lightmapping.isRunning)
|
||||
Finish("succeeded");
|
||||
}
|
||||
}
|
||||
|
||||
// Must be called under s_Lock.
|
||||
static void Finish(string result)
|
||||
{
|
||||
var ms = (int)Math.Max(0, (DateTime.UtcNow - s_StartedUtc).TotalMilliseconds);
|
||||
var status = new LightingBakeStatus
|
||||
{
|
||||
Status = "completed",
|
||||
Result = result,
|
||||
BakeId = s_BakeId,
|
||||
BakeTimeMs = ms,
|
||||
Stats = result == "succeeded" ? CollectStats() : null
|
||||
};
|
||||
WriteStatus(status);
|
||||
s_Tracking = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconcile recorded in-flight state after a domain reload. The status file under Temp/ survives
|
||||
/// the reload; if it said "baking" but Lightmapping is no longer running, the bake finished while
|
||||
/// the domain was being reloaded, so mark it completed. If it is still running, resume tracking.
|
||||
/// </summary>
|
||||
static void OnReload()
|
||||
{
|
||||
if (!File.Exists(StatusFile))
|
||||
return;
|
||||
|
||||
LightingBakeStatus prior;
|
||||
try
|
||||
{
|
||||
prior = JsonConvert.DeserializeObject<LightingBakeStatus>(File.ReadAllText(StatusFile));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (prior == null || prior.Status != "baking")
|
||||
return;
|
||||
|
||||
lock (s_Lock)
|
||||
{
|
||||
s_BakeId = prior.BakeId;
|
||||
s_StartedUtc = prior.StartedUtcTicks > 0 ? new DateTime(prior.StartedUtcTicks, DateTimeKind.Utc) : DateTime.UtcNow;
|
||||
|
||||
if (Lightmapping.isRunning)
|
||||
{
|
||||
s_Tracking = true; // resume; OnUpdate/OnBakeCompleted will finalize.
|
||||
}
|
||||
else
|
||||
{
|
||||
s_Tracking = true;
|
||||
Finish("succeeded"); // ended during the reload window.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region settings read/write helpers
|
||||
|
||||
/// <summary>
|
||||
/// The active <see cref="LightingSettings"/> for the open scene. When the scene has no explicit
|
||||
/// LightingSettings asset assigned, Unity falls back to a hidden per-scene default that
|
||||
/// <see cref="Lightmapping.GetLightingSettingsForScene"/> / <see cref="Lightmapping.lightingSettings"/>
|
||||
/// still exposes; we read/write that. <paramref name="source"/> records which we used.
|
||||
///
|
||||
/// When none is available and <paramref name="allowCreate"/> is true, a new settings asset is
|
||||
/// created and assigned so a real (non-dry_run) set has a target. When <paramref name="allowCreate"/>
|
||||
/// is false (dry_run), this returns <c>null</c> rather than creating/assigning anything, so the
|
||||
/// dry_run "no writes" contract holds.
|
||||
/// </summary>
|
||||
static LightingSettings ActiveLightingSettings(bool allowCreate, out string source)
|
||||
{
|
||||
// Lightmapping.lightingSettings returns the active scene's settings (assigned asset or the
|
||||
// scene's embedded/default settings). It throws if there is genuinely none; guard for that.
|
||||
try
|
||||
{
|
||||
var ls = Lightmapping.lightingSettings;
|
||||
if (ls != null)
|
||||
{
|
||||
source = AssetDatabase.Contains(ls) ? "asset" : "scene-default";
|
||||
return ls;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through to (optionally) creating one
|
||||
}
|
||||
|
||||
if (!allowCreate)
|
||||
{
|
||||
source = "none";
|
||||
return null;
|
||||
}
|
||||
|
||||
// No settings available: create and assign one so set_lighting_settings has a target.
|
||||
var created = new LightingSettings { name = "PipelineLightingSettings" };
|
||||
Lightmapping.lightingSettings = created;
|
||||
source = "created";
|
||||
return created;
|
||||
}
|
||||
|
||||
static LightingSettingsResult ReadLightingSettings()
|
||||
{
|
||||
LightingSettings ls;
|
||||
try
|
||||
{
|
||||
ls = Lightmapping.lightingSettings;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ls = null;
|
||||
}
|
||||
|
||||
if (ls == null)
|
||||
return new LightingSettingsResult { Available = false };
|
||||
|
||||
return new LightingSettingsResult
|
||||
{
|
||||
Available = true,
|
||||
Lightmapper = ls.lightmapper.ToString(),
|
||||
BakedGI = ls.bakedGI,
|
||||
RealtimeGI = ls.realtimeGI,
|
||||
DirectSampleCount = ls.directSampleCount,
|
||||
IndirectSampleCount = ls.indirectSampleCount,
|
||||
EnvironmentSampleCount = ls.environmentSampleCount,
|
||||
Bounces = ls.maxBounces,
|
||||
LightmapResolution = ls.lightmapResolution,
|
||||
LightmapPadding = ls.lightmapPadding,
|
||||
MaxLightmapSize = ls.lightmapMaxSize,
|
||||
LightmapCompression = ls.lightmapCompression.ToString(),
|
||||
// Unity 6000 serialises directionalityMode as a flags enum whose .ToString() can
|
||||
// produce compound strings (e.g. "Single, Dual") for values that span multiple bits.
|
||||
// Map to the two-value contract the spec guarantees: 0 → NonDirectional, else → Directional.
|
||||
DirectionalMode = (int)ls.directionalityMode == 0 ? "NonDirectional" : "Directional",
|
||||
Ao = ls.ao,
|
||||
AoMaxDistance = ls.aoMaxDistance,
|
||||
FilteringMode = ls.filteringMode.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply one settings key/value to the LightingSettings. Returns false for unrecognized keys so
|
||||
/// the caller can report them as <c>unknown[]</c>. Enum-valued fields accept their string names
|
||||
/// (case-insensitive). In dry-run mode we still validate (parse) the value but do not assign.
|
||||
/// </summary>
|
||||
static bool TryApply(LightingSettings ls, string key, JToken value, bool dryRun)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "lightmapper":
|
||||
return SetEnum<LightingSettings.Lightmapper>(value, dryRun, v => ls.lightmapper = v);
|
||||
case "bakedGI":
|
||||
if (!dryRun) ls.bakedGI = value.Value<bool>();
|
||||
return true;
|
||||
case "realtimeGI":
|
||||
if (!dryRun) ls.realtimeGI = value.Value<bool>();
|
||||
return true;
|
||||
case "directSampleCount":
|
||||
if (!dryRun) ls.directSampleCount = value.Value<int>();
|
||||
return true;
|
||||
case "indirectSampleCount":
|
||||
if (!dryRun) ls.indirectSampleCount = value.Value<int>();
|
||||
return true;
|
||||
case "environmentSampleCount":
|
||||
if (!dryRun) ls.environmentSampleCount = value.Value<int>();
|
||||
return true;
|
||||
case "bounces":
|
||||
if (!dryRun) ls.maxBounces = value.Value<int>();
|
||||
return true;
|
||||
case "lightmapResolution":
|
||||
if (!dryRun) ls.lightmapResolution = value.Value<float>();
|
||||
return true;
|
||||
case "lightmapPadding":
|
||||
if (!dryRun) ls.lightmapPadding = value.Value<int>();
|
||||
return true;
|
||||
case "maxLightmapSize":
|
||||
if (!dryRun) ls.lightmapMaxSize = value.Value<int>();
|
||||
return true;
|
||||
case "lightmapCompression":
|
||||
return SetEnum<LightmapCompression>(value, dryRun, v => ls.lightmapCompression = v);
|
||||
case "directionalMode":
|
||||
// Accept the two spec names ("NonDirectional", "Directional") and also Unity's
|
||||
// internal name ("CombinedDirectional") for back-compat. Integer values pass through
|
||||
// as-is so callers can target any raw enum slot Unity exposes.
|
||||
if (value.Type == JTokenType.Integer)
|
||||
{
|
||||
if (!dryRun) ls.directionalityMode = (LightmapsMode)value.Value<int>();
|
||||
return true;
|
||||
}
|
||||
var modeStr = value.Value<string>() ?? string.Empty;
|
||||
if (modeStr.Equals("NonDirectional", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!dryRun) ls.directionalityMode = (LightmapsMode)0;
|
||||
return true;
|
||||
}
|
||||
if (modeStr.Equals("Directional", StringComparison.OrdinalIgnoreCase) ||
|
||||
modeStr.Equals("CombinedDirectional", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!dryRun) ls.directionalityMode = (LightmapsMode)1;
|
||||
return true;
|
||||
}
|
||||
throw new ArgumentException(
|
||||
$"Value '{modeStr}' is not valid for directionalMode. Valid: NonDirectional, Directional.");
|
||||
case "ao":
|
||||
if (!dryRun) ls.ao = value.Value<bool>();
|
||||
return true;
|
||||
case "aoMaxDistance":
|
||||
if (!dryRun) ls.aoMaxDistance = value.Value<float>();
|
||||
return true;
|
||||
case "filteringMode":
|
||||
return SetEnum<LightingSettings.FilterMode>(value, dryRun, v => ls.filteringMode = v);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool SetEnum<TEnum>(JToken value, bool dryRun, Action<TEnum> assign) where TEnum : struct
|
||||
{
|
||||
// Accept either the enum's string name (case-insensitive) or its integer value.
|
||||
if (value.Type == JTokenType.Integer)
|
||||
{
|
||||
var iv = value.Value<int>();
|
||||
if (!Enum.IsDefined(typeof(TEnum), iv))
|
||||
throw new ArgumentException($"Value '{iv}' is not valid for {typeof(TEnum).Name}.");
|
||||
if (!dryRun) assign((TEnum)Enum.ToObject(typeof(TEnum), iv));
|
||||
return true;
|
||||
}
|
||||
|
||||
var name = value.Value<string>();
|
||||
if (!Enum.TryParse<TEnum>(name, ignoreCase: true, out var parsed))
|
||||
throw new ArgumentException(
|
||||
$"Value '{name}' is not valid for {typeof(TEnum).Name}. Valid: {string.Join(", ", Enum.GetNames(typeof(TEnum)))}.");
|
||||
if (!dryRun) assign(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region stats
|
||||
|
||||
/// <summary>
|
||||
/// Collect post-bake lightmap statistics from <see cref="LightmapSettings.lightmaps"/>: the
|
||||
/// number of baked lightmaps, their total texture size in bytes (color maps), the atlas size,
|
||||
/// the active lightmap resolution, and the baked light-probe count.
|
||||
/// </summary>
|
||||
static LightingBakeStats CollectStats()
|
||||
{
|
||||
var stats = new LightingBakeStats();
|
||||
|
||||
var maps = LightmapSettings.lightmaps;
|
||||
if (maps != null)
|
||||
{
|
||||
stats.LightmapCount = maps.Length;
|
||||
long total = 0;
|
||||
int atlas = 0;
|
||||
foreach (var data in maps)
|
||||
{
|
||||
var tex = data?.lightmapColor;
|
||||
if (tex == null)
|
||||
continue;
|
||||
atlas = Mathf.Max(atlas, Mathf.Max(tex.width, tex.height));
|
||||
total += EstimateTextureBytes(tex);
|
||||
}
|
||||
stats.TotalLightmapSizeBytes = total;
|
||||
stats.AtlasSize = atlas;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
stats.LightmapResolution = Lightmapping.lightingSettings != null
|
||||
? Lightmapping.lightingSettings.lightmapResolution
|
||||
: 0f;
|
||||
}
|
||||
catch
|
||||
{
|
||||
stats.LightmapResolution = 0f;
|
||||
}
|
||||
|
||||
var probes = LightmapSettings.lightProbes;
|
||||
stats.BakedProbeCount = probes != null ? probes.count : 0;
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/// <summary>Rough byte estimate for a baked lightmap texture (4 bytes/pixel; good enough for a size signal).</summary>
|
||||
static long EstimateTextureBytes(Texture tex)
|
||||
{
|
||||
return (long)tex.width * tex.height * 4;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
static void WriteStatus(LightingBakeStatus status)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(StatusFile, JsonConvert.SerializeObject(status));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[LightingBake] Failed to write status file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Status/result payload for <c>bake_lighting</c> / <c>lighting_bake_status</c>.</summary>
|
||||
[Serializable]
|
||||
public class LightingBakeStatus
|
||||
{
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
[JsonProperty("bakeId")]
|
||||
public string BakeId { get; set; }
|
||||
|
||||
[JsonProperty("result", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Result { get; set; }
|
||||
|
||||
[JsonProperty("isBaking")]
|
||||
public bool IsBaking { get; set; }
|
||||
|
||||
[JsonProperty("bakeTimeMs")]
|
||||
public int BakeTimeMs { get; set; }
|
||||
|
||||
[JsonProperty("stats", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public LightingBakeStats Stats { get; set; }
|
||||
|
||||
// Internal bookkeeping so a bake can be reconciled across a domain reload. Not part of the
|
||||
// documented schema, but harmless to expose.
|
||||
[JsonProperty("startedUtcTicks")]
|
||||
public long StartedUtcTicks { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Post-bake lightmap statistics (CLI-215 acceptance: lightmapCount >= 1, bakeTimeMs > 0).</summary>
|
||||
[Serializable]
|
||||
public class LightingBakeStats
|
||||
{
|
||||
[JsonProperty("lightmapCount")]
|
||||
public int LightmapCount { get; set; }
|
||||
|
||||
[JsonProperty("totalLightmapSizeBytes")]
|
||||
public long TotalLightmapSizeBytes { get; set; }
|
||||
|
||||
[JsonProperty("atlasSize")]
|
||||
public int AtlasSize { get; set; }
|
||||
|
||||
[JsonProperty("lightmapResolution")]
|
||||
public float LightmapResolution { get; set; }
|
||||
|
||||
[JsonProperty("bakedProbeCount")]
|
||||
public int BakedProbeCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Result of <c>get_lighting_settings</c> (and the dry-run payload of bake_lighting).</summary>
|
||||
[Serializable]
|
||||
public class LightingSettingsResult
|
||||
{
|
||||
/// <summary>False when no LightingSettings could be read for the active scene.</summary>
|
||||
[JsonProperty("available")]
|
||||
public bool Available { get; set; }
|
||||
|
||||
[JsonProperty("lightmapper", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Lightmapper { get; set; }
|
||||
|
||||
[JsonProperty("bakedGI")]
|
||||
public bool BakedGI { get; set; }
|
||||
|
||||
[JsonProperty("realtimeGI")]
|
||||
public bool RealtimeGI { get; set; }
|
||||
|
||||
[JsonProperty("directSampleCount")]
|
||||
public int DirectSampleCount { get; set; }
|
||||
|
||||
[JsonProperty("indirectSampleCount")]
|
||||
public int IndirectSampleCount { get; set; }
|
||||
|
||||
[JsonProperty("environmentSampleCount")]
|
||||
public int EnvironmentSampleCount { get; set; }
|
||||
|
||||
[JsonProperty("bounces")]
|
||||
public int Bounces { get; set; }
|
||||
|
||||
[JsonProperty("lightmapResolution")]
|
||||
public float LightmapResolution { get; set; }
|
||||
|
||||
[JsonProperty("lightmapPadding")]
|
||||
public int LightmapPadding { get; set; }
|
||||
|
||||
[JsonProperty("maxLightmapSize")]
|
||||
public int MaxLightmapSize { get; set; }
|
||||
|
||||
[JsonProperty("lightmapCompression", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string LightmapCompression { get; set; }
|
||||
|
||||
[JsonProperty("directionalMode", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string DirectionalMode { get; set; }
|
||||
|
||||
[JsonProperty("ao")]
|
||||
public bool Ao { get; set; }
|
||||
|
||||
[JsonProperty("aoMaxDistance")]
|
||||
public float AoMaxDistance { get; set; }
|
||||
|
||||
[JsonProperty("filteringMode", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string FilteringMode { get; set; }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dd7529c9d1f14ff98b9eb3e9a1e8ba5b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+424
@@ -0,0 +1,424 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Unity.Pipeline.Commands;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AI;
|
||||
using UnityEngine;
|
||||
|
||||
// CLI-215 deliberately targets the built-in legacy UnityEditor.AI.NavMeshBuilder (see class doc). It is
|
||||
// [Obsolete] in Unity 6000.x (the modern path is the com.unity.ai.navigation package), but is the
|
||||
// intentional v1 baker here, so suppress CS0618 file-wide rather than migrate.
|
||||
#pragma warning disable CS0618
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.Baking
|
||||
{
|
||||
/// <summary>
|
||||
/// CLI-215 Group B — NavMesh bake commands, targeting the <b>built-in legacy</b> baker
|
||||
/// <see cref="UnityEditor.AI.NavMeshBuilder"/> (always available; bakes a single scene NavMesh from
|
||||
/// the Navigation-static geometry of the open scene). Trigger an async build, poll its status,
|
||||
/// cancel it, clear the baked NavMesh, and read/configure the default agent's bake settings.
|
||||
///
|
||||
/// The newer AI Navigation package (<c>com.unity.ai.navigation</c>, <c>NavMeshSurface</c>
|
||||
/// components) is OUT for v1 — we do NOT add an asmdef reference to it. <c>bake_navmesh_surfaces</c>
|
||||
/// is a guarded stub that probes for the package by reflection and returns
|
||||
/// <c>{ code: "package_not_found" }</c> when it is absent (full support is a follow-up).
|
||||
///
|
||||
/// Async pattern mirrors <see cref="BuildCommand"/> / <see cref="LightingBakeCommands"/>: trigger
|
||||
/// returns immediately, completion is detected by <see cref="NavMeshBuilder.isRunning"/> flipping
|
||||
/// false (polled in <see cref="EditorApplication.update"/>), and in-flight state is held in static
|
||||
/// fields reconciled against a Temp/ status file across domain reloads.
|
||||
/// </summary>
|
||||
[InitializeOnLoad]
|
||||
public static class NavMeshBakeCommands
|
||||
{
|
||||
const string StatusFile = "Temp/pipeline_navmesh_bake_status.json";
|
||||
|
||||
// The serialized NavMesh bake-settings live on the Navigation window's settings object, keyed by
|
||||
// these property names (stable across 2019+ / Unity 6).
|
||||
const string PropAgentRadius = "m_BuildSettings.agentRadius";
|
||||
const string PropAgentHeight = "m_BuildSettings.agentHeight";
|
||||
const string PropAgentSlope = "m_BuildSettings.agentSlope";
|
||||
const string PropAgentClimb = "m_BuildSettings.agentClimb";
|
||||
const string PropMinRegionArea = "m_BuildSettings.minRegionArea";
|
||||
const string PropManualCellSize = "m_BuildSettings.manualCellSize";
|
||||
const string PropCellSize = "m_BuildSettings.cellSize";
|
||||
|
||||
static readonly object s_Lock = new object();
|
||||
static bool s_Tracking;
|
||||
static string s_BakeId;
|
||||
static DateTime s_StartedUtc;
|
||||
|
||||
static NavMeshBakeCommands()
|
||||
{
|
||||
OnReload();
|
||||
EditorApplication.update += OnUpdate;
|
||||
}
|
||||
|
||||
#region bake_navmesh
|
||||
|
||||
[CliCommand("bake_navmesh", "Trigger an async legacy NavMesh bake of the open scene(s) via UnityEditor.AI.NavMeshBuilder. Returns immediately; poll navmesh_bake_status until completed.")]
|
||||
public static object BakeNavMesh(
|
||||
[CliArg("confirm", "Accepted for parity (a bake overwrites the existing NavMesh); not required.")] bool confirm = false,
|
||||
[CliArg("dry_run", "If true, validate there is an open scene and return current NavMesh settings without baking.")] bool dryRun = false)
|
||||
{
|
||||
if (!BakeSceneGuard.HasBakeableScene())
|
||||
return BakeSceneGuard.NoSceneResult();
|
||||
|
||||
OnUpdate(); // reconcile a possibly-stale tracking flag first.
|
||||
|
||||
if (NavMeshBuilder.isRunning)
|
||||
return new { code = "bake_in_progress", message = "A NavMesh bake is already running. Poll navmesh_bake_status." };
|
||||
|
||||
if (dryRun)
|
||||
return new { status = "dry_run", wouldBake = true, settings = ReadNavMeshSettings() };
|
||||
|
||||
var bakeId = Guid.NewGuid().ToString("N");
|
||||
lock (s_Lock)
|
||||
{
|
||||
s_Tracking = true;
|
||||
s_BakeId = bakeId;
|
||||
s_StartedUtc = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
WriteStatus(new NavMeshBakeStatus
|
||||
{
|
||||
Status = "baking",
|
||||
BakeId = bakeId,
|
||||
StartedUtcTicks = s_StartedUtc.Ticks
|
||||
});
|
||||
|
||||
NavMeshBuilder.BuildNavMeshAsync();
|
||||
return new { status = "baking", bakeId };
|
||||
}
|
||||
|
||||
[CliCommand("navmesh_bake_status", "Get the status of the last NavMesh bake: idle | baking | completed.", MainThreadRequired = false)]
|
||||
public static string NavMeshBakeStatus()
|
||||
{
|
||||
if (File.Exists(StatusFile))
|
||||
return File.ReadAllText(StatusFile);
|
||||
return "{\"status\":\"idle\"}";
|
||||
}
|
||||
|
||||
[CliCommand("cancel_navmesh_bake", "Cancel an in-progress NavMesh bake (NavMeshBuilder.Cancel()).")]
|
||||
public static object CancelNavMeshBake()
|
||||
{
|
||||
var wasRunning = NavMeshBuilder.isRunning;
|
||||
if (wasRunning)
|
||||
NavMeshBuilder.Cancel();
|
||||
|
||||
lock (s_Lock)
|
||||
{
|
||||
if (s_Tracking)
|
||||
{
|
||||
var ms = (int)Math.Max(0, (DateTime.UtcNow - s_StartedUtc).TotalMilliseconds);
|
||||
WriteStatus(new NavMeshBakeStatus
|
||||
{
|
||||
Status = "completed",
|
||||
Result = "cancelled",
|
||||
BakeId = s_BakeId,
|
||||
BakeTimeMs = ms
|
||||
});
|
||||
s_Tracking = false;
|
||||
}
|
||||
}
|
||||
|
||||
return new { cancelled = wasRunning };
|
||||
}
|
||||
|
||||
[CliCommand("clear_navmesh", "Clear the baked NavMesh for the open scene(s). Destructive: requires confirm=true.")]
|
||||
public static object ClearNavMesh(
|
||||
[CliArg("confirm", "Must be true to actually clear (destructive, not undoable via Unity's Undo).")] bool confirm = false,
|
||||
[CliArg("dry_run", "If true, report what would be cleared without clearing.")] bool dryRun = false)
|
||||
{
|
||||
if (!confirm)
|
||||
throw new ArgumentException("Refusing to clear the NavMesh. Pass confirm=true (destructive, not undoable via Unity's Undo).");
|
||||
|
||||
if (dryRun)
|
||||
return new { status = "dry_run", wouldClear = true };
|
||||
|
||||
NavMeshBuilder.ClearAllNavMeshes();
|
||||
return new { cleared = true };
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region settings
|
||||
|
||||
[CliCommand("get_navmesh_settings", "Read the default agent's legacy NavMesh bake settings (agentRadius/Height/Slope/Climb, minRegionArea, voxelSize).")]
|
||||
public static NavMeshSettingsResult GetNavMeshSettings()
|
||||
{
|
||||
return ReadNavMeshSettings();
|
||||
}
|
||||
|
||||
[CliCommand("set_navmesh_settings", "Apply a subset of legacy NavMesh bake settings to the default agent. Returns { applied[], unknown[] }.")]
|
||||
public static object SetNavMeshSettings(
|
||||
[CliArg("settings", "JSON object with a subset of NavMesh fields to set (same names as get_navmesh_settings).", Required = true)] JObject settings,
|
||||
[CliArg("dry_run", "If true, validate the keys and report applied/unknown without changing anything.")] bool dryRun = false)
|
||||
{
|
||||
if (settings == null)
|
||||
throw new ArgumentException("settings is required (a JSON object of NavMesh fields to set).");
|
||||
|
||||
// NavMeshBuilder.navMeshSettingsObject is a UnityEngine.Object (the Navigation window's
|
||||
// settings holder); wrap it in a SerializedObject to read/write the m_BuildSettings fields.
|
||||
var settingsObject = NavMeshBuilder.navMeshSettingsObject;
|
||||
if (settingsObject == null)
|
||||
return new { code = "settings_unavailable", message = "Could not access the legacy NavMesh settings object." };
|
||||
|
||||
var so = new SerializedObject(settingsObject);
|
||||
so.Update();
|
||||
|
||||
var applied = new List<string>();
|
||||
var unknown = new List<string>();
|
||||
|
||||
foreach (var prop in settings.Properties())
|
||||
{
|
||||
if (TryApply(so, prop.Name, prop.Value, dryRun))
|
||||
applied.Add(prop.Name);
|
||||
else
|
||||
unknown.Add(prop.Name);
|
||||
}
|
||||
|
||||
if (!dryRun && applied.Count > 0)
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
return new { applied = applied.ToArray(), unknown = unknown.ToArray(), dryRun };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Guarded stub for the AI Navigation package (<c>com.unity.ai.navigation</c>) NavMeshSurface
|
||||
/// bake path. We never reference that assembly, so probe for the type by reflection; absent → the
|
||||
/// documented <c>package_not_found</c> result. Full multi-surface baking is a follow-up.
|
||||
/// </summary>
|
||||
[CliCommand("bake_navmesh_surfaces", "Bake NavMeshSurface components (AI Navigation package). v1 stub: returns package_not_found when the package is absent.")]
|
||||
public static object BakeNavMeshSurfaces()
|
||||
{
|
||||
var surfaceType = Type.GetType("Unity.AI.Navigation.NavMeshSurface, Unity.AI.Navigation")
|
||||
?? Type.GetType("UnityEngine.AI.NavMeshSurface, Unity.AI.Navigation");
|
||||
|
||||
if (surfaceType == null)
|
||||
return new
|
||||
{
|
||||
code = "package_not_found",
|
||||
message = "The AI Navigation package (com.unity.ai.navigation) is not installed. " +
|
||||
"NavMeshSurface-based baking is out of scope for v1; use bake_navmesh for the built-in baker."
|
||||
};
|
||||
|
||||
return new
|
||||
{
|
||||
code = "not_implemented",
|
||||
message = "AI Navigation package detected, but NavMeshSurface baking is not implemented in v1 (follow-up). Use bake_navmesh."
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region completion / reload bookkeeping
|
||||
|
||||
static void OnUpdate()
|
||||
{
|
||||
lock (s_Lock)
|
||||
{
|
||||
if (s_Tracking && !NavMeshBuilder.isRunning)
|
||||
Finish("succeeded");
|
||||
}
|
||||
}
|
||||
|
||||
// Must be called under s_Lock.
|
||||
static void Finish(string result)
|
||||
{
|
||||
var ms = (int)Math.Max(0, (DateTime.UtcNow - s_StartedUtc).TotalMilliseconds);
|
||||
WriteStatus(new NavMeshBakeStatus
|
||||
{
|
||||
Status = "completed",
|
||||
Result = result,
|
||||
BakeId = s_BakeId,
|
||||
BakeTimeMs = ms
|
||||
});
|
||||
s_Tracking = false;
|
||||
}
|
||||
|
||||
static void OnReload()
|
||||
{
|
||||
if (!File.Exists(StatusFile))
|
||||
return;
|
||||
|
||||
NavMeshBakeStatus prior;
|
||||
try
|
||||
{
|
||||
prior = JsonConvert.DeserializeObject<NavMeshBakeStatus>(File.ReadAllText(StatusFile));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (prior == null || prior.Status != "baking")
|
||||
return;
|
||||
|
||||
lock (s_Lock)
|
||||
{
|
||||
s_BakeId = prior.BakeId;
|
||||
s_StartedUtc = prior.StartedUtcTicks > 0 ? new DateTime(prior.StartedUtcTicks, DateTimeKind.Utc) : DateTime.UtcNow;
|
||||
s_Tracking = true;
|
||||
if (!NavMeshBuilder.isRunning)
|
||||
Finish("succeeded");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region settings read/write helpers
|
||||
|
||||
static NavMeshSettingsResult ReadNavMeshSettings()
|
||||
{
|
||||
var settingsObject = NavMeshBuilder.navMeshSettingsObject;
|
||||
if (settingsObject == null)
|
||||
return new NavMeshSettingsResult { Available = false };
|
||||
|
||||
var so = new SerializedObject(settingsObject);
|
||||
so.Update();
|
||||
var manual = FindBool(so, PropManualCellSize, false);
|
||||
return new NavMeshSettingsResult
|
||||
{
|
||||
Available = true,
|
||||
AgentRadius = FindFloat(so, PropAgentRadius, 0.5f),
|
||||
AgentHeight = FindFloat(so, PropAgentHeight, 2f),
|
||||
AgentSlope = FindFloat(so, PropAgentSlope, 45f),
|
||||
AgentClimb = FindFloat(so, PropAgentClimb, 0.4f),
|
||||
MinRegionArea = FindFloat(so, PropMinRegionArea, 2f),
|
||||
VoxelSize = FindFloat(so, PropCellSize, 0f),
|
||||
ManualVoxelSize = manual
|
||||
};
|
||||
}
|
||||
|
||||
static bool TryApply(SerializedObject so, string key, JToken value, bool dryRun)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "agentRadius":
|
||||
return SetFloat(so, PropAgentRadius, value, dryRun);
|
||||
case "agentHeight":
|
||||
return SetFloat(so, PropAgentHeight, value, dryRun);
|
||||
case "agentSlope":
|
||||
return SetFloat(so, PropAgentSlope, value, dryRun);
|
||||
case "agentClimb":
|
||||
return SetFloat(so, PropAgentClimb, value, dryRun);
|
||||
case "minRegionArea":
|
||||
return SetFloat(so, PropMinRegionArea, value, dryRun);
|
||||
case "voxelSize":
|
||||
// Setting an explicit voxel size implies manual override on.
|
||||
{
|
||||
var ok = SetFloat(so, PropCellSize, value, dryRun);
|
||||
if (ok && !dryRun)
|
||||
{
|
||||
var manual = so.FindProperty(PropManualCellSize);
|
||||
if (manual != null) manual.boolValue = true;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
case "manualVoxelSize":
|
||||
return SetBool(so, PropManualCellSize, value, dryRun);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool SetFloat(SerializedObject so, string propPath, JToken value, bool dryRun)
|
||||
{
|
||||
var prop = so.FindProperty(propPath);
|
||||
if (prop == null)
|
||||
return false;
|
||||
var f = value.Value<float>();
|
||||
if (!dryRun) prop.floatValue = f;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool SetBool(SerializedObject so, string propPath, JToken value, bool dryRun)
|
||||
{
|
||||
var prop = so.FindProperty(propPath);
|
||||
if (prop == null)
|
||||
return false;
|
||||
var b = value.Value<bool>();
|
||||
if (!dryRun) prop.boolValue = b;
|
||||
return true;
|
||||
}
|
||||
|
||||
static float FindFloat(SerializedObject so, string propPath, float fallback)
|
||||
{
|
||||
var prop = so.FindProperty(propPath);
|
||||
return prop != null ? prop.floatValue : fallback;
|
||||
}
|
||||
|
||||
static bool FindBool(SerializedObject so, string propPath, bool fallback)
|
||||
{
|
||||
var prop = so.FindProperty(propPath);
|
||||
return prop != null ? prop.boolValue : fallback;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
static void WriteStatus(NavMeshBakeStatus status)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(StatusFile, JsonConvert.SerializeObject(status));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[NavMeshBake] Failed to write status file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Status/result payload for <c>bake_navmesh</c> / <c>navmesh_bake_status</c>.</summary>
|
||||
[Serializable]
|
||||
public class NavMeshBakeStatus
|
||||
{
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
[JsonProperty("bakeId")]
|
||||
public string BakeId { get; set; }
|
||||
|
||||
[JsonProperty("result", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Result { get; set; }
|
||||
|
||||
[JsonProperty("bakeTimeMs")]
|
||||
public int BakeTimeMs { get; set; }
|
||||
|
||||
[JsonProperty("startedUtcTicks")]
|
||||
public long StartedUtcTicks { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Result of <c>get_navmesh_settings</c> (default-agent legacy bake settings).</summary>
|
||||
[Serializable]
|
||||
public class NavMeshSettingsResult
|
||||
{
|
||||
[JsonProperty("available")]
|
||||
public bool Available { get; set; }
|
||||
|
||||
[JsonProperty("agentRadius")]
|
||||
public float AgentRadius { get; set; }
|
||||
|
||||
[JsonProperty("agentHeight")]
|
||||
public float AgentHeight { get; set; }
|
||||
|
||||
[JsonProperty("agentSlope")]
|
||||
public float AgentSlope { get; set; }
|
||||
|
||||
[JsonProperty("agentClimb")]
|
||||
public float AgentClimb { get; set; }
|
||||
|
||||
[JsonProperty("minRegionArea")]
|
||||
public float MinRegionArea { get; set; }
|
||||
|
||||
[JsonProperty("voxelSize")]
|
||||
public float VoxelSize { get; set; }
|
||||
|
||||
[JsonProperty("manualVoxelSize")]
|
||||
public bool ManualVoxelSize { get; set; }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5173e39c97c44308ad4b80462eaf7982
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Unity.Pipeline.Commands;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.Baking
|
||||
{
|
||||
/// <summary>
|
||||
/// CLI-215 Group C — Occlusion Culling bake commands. Trigger an async occlusion bake of the open
|
||||
/// scene(s) via <see cref="StaticOcclusionCulling.GenerateInBackground"/>, poll its status, cancel
|
||||
/// it, and clear the baked occlusion data.
|
||||
///
|
||||
/// Async pattern mirrors <see cref="BuildCommand"/> / <see cref="LightingBakeCommands"/>: trigger
|
||||
/// returns immediately; completion is detected by <see cref="StaticOcclusionCulling.isRunning"/>
|
||||
/// flipping false (polled in <see cref="EditorApplication.update"/>); in-flight state is held in
|
||||
/// static fields and reconciled against a Temp/ status file across domain reloads.
|
||||
/// </summary>
|
||||
[InitializeOnLoad]
|
||||
public static class OcclusionBakeCommands
|
||||
{
|
||||
const string StatusFile = "Temp/pipeline_occlusion_bake_status.json";
|
||||
|
||||
// Sentinel for "argument omitted" on the float bake parameters. float.NaN is not a
|
||||
// compile-time constant (so it can't be a default parameter value), but float.MinValue is, and
|
||||
// it is not a plausible real value for any of these positive bake parameters.
|
||||
const float Unset = float.MinValue;
|
||||
|
||||
static readonly object s_Lock = new object();
|
||||
static bool s_Tracking;
|
||||
static string s_BakeId;
|
||||
static DateTime s_StartedUtc;
|
||||
|
||||
static OcclusionBakeCommands()
|
||||
{
|
||||
OnReload();
|
||||
EditorApplication.update += OnUpdate;
|
||||
}
|
||||
|
||||
#region bake_occlusion_culling
|
||||
|
||||
[CliCommand("bake_occlusion_culling", "Trigger an async occlusion-culling bake of the open scene(s) via StaticOcclusionCulling.GenerateInBackground(). Returns immediately; poll occlusion_bake_status until completed.")]
|
||||
public static object BakeOcclusionCulling(
|
||||
[CliArg("smallest_occluder", "Smallest object that will occlude others (meters). Defaults to Unity's current value.")] float smallestOccluder = Unset,
|
||||
[CliArg("smallest_hole", "Smallest gap geometry can have that the view can see through (meters). Defaults to Unity's current value.")] float smallestHole = Unset,
|
||||
[CliArg("backface_threshold", "Backface threshold (1-100); lower trims more backfaces. Defaults to Unity's current value.")] float backfaceThreshold = Unset,
|
||||
[CliArg("confirm", "Accepted for parity (a bake overwrites existing occlusion data); not required.")] bool confirm = false,
|
||||
[CliArg("dry_run", "If true, validate there is an open scene and report the parameters that would be used without baking.")] bool dryRun = false)
|
||||
{
|
||||
if (!BakeSceneGuard.HasBakeableScene())
|
||||
return BakeSceneGuard.NoSceneResult();
|
||||
|
||||
OnUpdate(); // reconcile any stale tracking flag first.
|
||||
|
||||
if (StaticOcclusionCulling.isRunning)
|
||||
return new { code = "bake_in_progress", message = "An occlusion bake is already running. Poll occlusion_bake_status." };
|
||||
|
||||
// Validate explicitly provided values before resolving. An omitted arg is the Unset
|
||||
// sentinel (float.MinValue); NaN is never <= Unset, so a NaN arg is treated as provided
|
||||
// and rejected here. smallest_occluder / smallest_hole must be positive; backface_threshold
|
||||
// is documented as 1-100.
|
||||
if (!IsOmitted(smallestOccluder) && !(smallestOccluder > 0f))
|
||||
throw new ArgumentException($"smallest_occluder must be greater than 0 (got {smallestOccluder}).");
|
||||
if (!IsOmitted(smallestHole) && !(smallestHole > 0f))
|
||||
throw new ArgumentException($"smallest_hole must be greater than 0 (got {smallestHole}).");
|
||||
if (!IsOmitted(backfaceThreshold) && !(backfaceThreshold >= 1f && backfaceThreshold <= 100f))
|
||||
throw new ArgumentException($"backface_threshold must be between 1 and 100 (got {backfaceThreshold}).");
|
||||
|
||||
// Resolve effective parameters: an omitted arg (the Unset sentinel) keeps Unity's current value.
|
||||
var occluder = IsOmitted(smallestOccluder) ? StaticOcclusionCulling.smallestOccluder : smallestOccluder;
|
||||
var hole = IsOmitted(smallestHole) ? StaticOcclusionCulling.smallestHole : smallestHole;
|
||||
var backface = IsOmitted(backfaceThreshold) ? StaticOcclusionCulling.backfaceThreshold : backfaceThreshold;
|
||||
|
||||
if (dryRun)
|
||||
{
|
||||
return new
|
||||
{
|
||||
status = "dry_run",
|
||||
wouldBake = true,
|
||||
parameters = new { smallestOccluder = occluder, smallestHole = hole, backfaceThreshold = backface }
|
||||
};
|
||||
}
|
||||
|
||||
// Apply the (possibly overridden) bake parameters before generating.
|
||||
StaticOcclusionCulling.smallestOccluder = occluder;
|
||||
StaticOcclusionCulling.smallestHole = hole;
|
||||
StaticOcclusionCulling.backfaceThreshold = backface;
|
||||
|
||||
var bakeId = Guid.NewGuid().ToString("N");
|
||||
lock (s_Lock)
|
||||
{
|
||||
s_Tracking = true;
|
||||
s_BakeId = bakeId;
|
||||
s_StartedUtc = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
WriteStatus(new OcclusionBakeStatus
|
||||
{
|
||||
Status = "baking",
|
||||
BakeId = bakeId,
|
||||
StartedUtcTicks = s_StartedUtc.Ticks
|
||||
});
|
||||
|
||||
StaticOcclusionCulling.GenerateInBackground();
|
||||
return new { status = "baking", bakeId };
|
||||
}
|
||||
|
||||
[CliCommand("occlusion_bake_status", "Get the status of the last occlusion bake: idle | baking | completed.", MainThreadRequired = false)]
|
||||
public static string OcclusionBakeStatus()
|
||||
{
|
||||
if (File.Exists(StatusFile))
|
||||
return File.ReadAllText(StatusFile);
|
||||
return "{\"status\":\"idle\"}";
|
||||
}
|
||||
|
||||
[CliCommand("cancel_occlusion_bake", "Cancel an in-progress occlusion bake (StaticOcclusionCulling.Cancel()).")]
|
||||
public static object CancelOcclusionBake()
|
||||
{
|
||||
var wasRunning = StaticOcclusionCulling.isRunning;
|
||||
if (wasRunning)
|
||||
StaticOcclusionCulling.Cancel();
|
||||
|
||||
lock (s_Lock)
|
||||
{
|
||||
if (s_Tracking)
|
||||
{
|
||||
var ms = (int)Math.Max(0, (DateTime.UtcNow - s_StartedUtc).TotalMilliseconds);
|
||||
WriteStatus(new OcclusionBakeStatus
|
||||
{
|
||||
Status = "completed",
|
||||
Result = "cancelled",
|
||||
BakeId = s_BakeId,
|
||||
BakeTimeMs = ms
|
||||
});
|
||||
s_Tracking = false;
|
||||
}
|
||||
}
|
||||
|
||||
return new { cancelled = wasRunning };
|
||||
}
|
||||
|
||||
[CliCommand("clear_occlusion_culling", "Clear baked occlusion-culling data for the open scene(s). Destructive: requires confirm=true.")]
|
||||
public static object ClearOcclusionCulling(
|
||||
[CliArg("confirm", "Must be true to actually clear (destructive, not undoable via Unity's Undo).")] bool confirm = false,
|
||||
[CliArg("dry_run", "If true, report what would be cleared without clearing.")] bool dryRun = false)
|
||||
{
|
||||
if (!confirm)
|
||||
throw new ArgumentException("Refusing to clear occlusion culling data. Pass confirm=true (destructive, not undoable via Unity's Undo).");
|
||||
|
||||
if (dryRun)
|
||||
return new { status = "dry_run", wouldClear = true };
|
||||
|
||||
StaticOcclusionCulling.Clear();
|
||||
return new { cleared = true };
|
||||
}
|
||||
|
||||
// True when the caller omitted a float bake arg (left it at the Unset sentinel). NaN is never
|
||||
// <= Unset, so NaN counts as provided and is caught by the range validation above.
|
||||
static bool IsOmitted(float value) => value <= Unset;
|
||||
|
||||
#endregion
|
||||
|
||||
#region completion / reload bookkeeping
|
||||
|
||||
static void OnUpdate()
|
||||
{
|
||||
lock (s_Lock)
|
||||
{
|
||||
if (s_Tracking && !StaticOcclusionCulling.isRunning)
|
||||
Finish("succeeded");
|
||||
}
|
||||
}
|
||||
|
||||
// Must be called under s_Lock.
|
||||
static void Finish(string result)
|
||||
{
|
||||
var ms = (int)Math.Max(0, (DateTime.UtcNow - s_StartedUtc).TotalMilliseconds);
|
||||
WriteStatus(new OcclusionBakeStatus
|
||||
{
|
||||
Status = "completed",
|
||||
Result = result,
|
||||
BakeId = s_BakeId,
|
||||
BakeTimeMs = ms,
|
||||
Stats = result == "succeeded"
|
||||
? new OcclusionBakeStats { UmbraDataSizeBytes = StaticOcclusionCulling.umbraDataSize }
|
||||
: null
|
||||
});
|
||||
s_Tracking = false;
|
||||
}
|
||||
|
||||
static void OnReload()
|
||||
{
|
||||
if (!File.Exists(StatusFile))
|
||||
return;
|
||||
|
||||
OcclusionBakeStatus prior;
|
||||
try
|
||||
{
|
||||
prior = JsonConvert.DeserializeObject<OcclusionBakeStatus>(File.ReadAllText(StatusFile));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (prior == null || prior.Status != "baking")
|
||||
return;
|
||||
|
||||
lock (s_Lock)
|
||||
{
|
||||
s_BakeId = prior.BakeId;
|
||||
s_StartedUtc = prior.StartedUtcTicks > 0 ? new DateTime(prior.StartedUtcTicks, DateTimeKind.Utc) : DateTime.UtcNow;
|
||||
s_Tracking = true;
|
||||
if (!StaticOcclusionCulling.isRunning)
|
||||
Finish("succeeded");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
static void WriteStatus(OcclusionBakeStatus status)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(StatusFile, JsonConvert.SerializeObject(status));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[OcclusionBake] Failed to write status file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Status/result payload for <c>bake_occlusion_culling</c> / <c>occlusion_bake_status</c>.</summary>
|
||||
[Serializable]
|
||||
public class OcclusionBakeStatus
|
||||
{
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
[JsonProperty("bakeId")]
|
||||
public string BakeId { get; set; }
|
||||
|
||||
[JsonProperty("result", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Result { get; set; }
|
||||
|
||||
[JsonProperty("bakeTimeMs")]
|
||||
public int BakeTimeMs { get; set; }
|
||||
|
||||
[JsonProperty("stats", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public OcclusionBakeStats Stats { get; set; }
|
||||
|
||||
[JsonProperty("startedUtcTicks")]
|
||||
public long StartedUtcTicks { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Post-bake occlusion statistics (umbra data size in bytes).</summary>
|
||||
[Serializable]
|
||||
public class OcclusionBakeStats
|
||||
{
|
||||
[JsonProperty("umbraDataSizeBytes")]
|
||||
public long UmbraDataSizeBytes { get; set; }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00b06ed57bce4c0bb281527e016889dd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user