Live Studio
Change history for jp.lilium.livestudio.
[0.26.1] - 2026-10-25
Changed
- Renaming a deck tab no longer renames its file. A deck is now identified by its file name, fixed when the deck is created, and its display name is stored inside the file, so two decks can share a name; a deck file from an earlier build shows its file name until it is next written. Code that read
DeckControl.deckNamenow readsDeckControl.deckId. - The snapshot list no longer rescans the Recordings folder on every refresh while its page is open. Files added to the folder from outside the app are still picked up.
[0.26.0] - 2026-09-27
Added
- A live scene written before the asset catalog left it is read in the terms of it. Until the catalog stopped being saved, the shadow it was saved as (
_persistedAssets) was where the show was read back from: which avatar was out, which stage was standing and which props were on it. Those answers moved onto the objects that own them, and a restore drops an entry naming a member that is gone without a word — so such a scene opened stripped of its avatar, its stage and its props, looking merely empty.LegacyAssetCatalogMigrationnow says what those entries said in the members that hold those answers today, before anything reads the file: an enabled avatar entry becomesExternalAvatarSource.selectedAvatar, a set becomes an element ofStageManager.loadedSets(andactiveSetwhen it was the one standing — absent still means the bootstrap scene was up, as it did then), and a prop is stood up as the scene instance it would be today. The prop case needs no mapping table: the old shadow moored a prop's saved settings to anobjectIdand the file still holds them as entries addressed{objectId}.components[Item], so giving the migrated instance that same@idre-attaches them, and a prop comes back on its socket at the offset it was placed at. A prop needs the entry to carrypath, which they began doing in 0.23.5; an avatar and a set are addressed by name, which every generation wrote, so an older file still comes back with both and loses only its props. Reading only and one way — the file on disk is untouched until the operator saves, and saving once settles it. ⚠ A scene already re-saved by a build without the catalog cannot be recovered: the shadow is gone from the file, and what is left of a prop is a settings entry addressed to an id nothing carries any more. - What the studio does reaches a take. Deck operations go through
FrameGatethe same way a remote write does (OperationRequestrebuilds the request a network call would have sent), so a key press is recorded like a tap in the remote app. Values that move continuously ride the state lane instead of writing an event per change: the free and orbital-follow cameras' yaw / pitch / position / distance / fov / screen position (their getters now read the live rig rather than a shadow copy synced only on save), a camera'spriority, the environment light,OperationSet's manual value while a slider is dragged, andTransformRef's bone path.ExternalAssetManagerholds a replay while an asset it needs is still loading. The avatar's pose is carried byMotionSourceBase(see Changed). Machine-local settings — the screen's output settings, render quality, the project manager's poll intervals — stay out of takes. - Command-line options for runs that must not touch the operator's own files.
LaunchArgsreads-name valueoptions for what has to be settled before anything else runs:-savedBase <dir>moves where every saved file goes (SavedPaths);-project <dir>opens that project instead of the remembered one, and is never written back, so the next ordinary launch is unaffected;-noRemoteApp/-noCompanionAppstop the build from starting its own remote app / companion app (Fusion), so an external driver can be the only client.StartupSceneSwitchernow asksProjectManager.ResolveStartupProjectPathinstead of readingPlayerPrefsitself, so both startup paths agree on which project's base scene to load. These were added for the integration test harness; options the class does not know are ignored, so engine options can share the command line. - A scene shipped inside the app can be offered as a set, the same as an external
*.set.lsb. A project declares which of its scenes are sets in aBuiltinSetListasset underAssets/Resources/(create it from Assets ▸ Lilium Live Studio ▸ Create Built-in Set List), and each declared scene lists on the project page as a built-in asset and on the stage page as a set that loads, unloads and activates through exactly the path a bundle set does —StageManagerreconciles throughISetAsset, so there was nothing to teach it. Declaring is deliberately explicit rather than "every scene in the build is a set": a build also carries scenes that are not stages, and offering the bootstrap scene as a set would load the whole studio on top of itself, a secondAvatarControllerincluded. The declaration is also where a set gets what a bare scene path cannot carry — a display name and a preview image — and its inspector reports the two things that make a declaration fail in the field: a scene missing from the build (with a button to add it) and two sets sharing a display name, which would make the stage's saved and recorded state ambiguous, since a set is recorded by name. A set's identity is its scene's GUID, so moving or renaming the scene keeps a saved live scene working; the scene path travels alongside as a cache and is re-derived whenever the scene moves.SetAssetBasejoinsSetBundleAssetandBuiltinSetAssetunder one base so the remote app can list "every set" from the inheritance chain inGET /live/typesrather than naming each concrete type — an interface cannot serve there, since only base classes are published.AssetBase.isBuiltinis exposed read-only for the same reason (a saved scene is unchanged: persistence skips read-only properties). When a set bundle happens to be called the same thing as a built-in set, the built-in one wins every name lookup — a saved or recorded stage names the set it wants, and the built-in copy is the one every machine running this app is guaranteed to have, where a*.set.lsbis whatever that machine has on disk. The stage page marks which is which, and a warning names any stages sharing a name: settling the tie makes a restore predictable, it does not make two stages with one name a good idea. - One deck is one file (
*.deck.json), and every deck file in the project is a tab. The authored operation layer — everyOperationSetandDeck— used to be inlined in the live scene, so a deck could not be reused across scenes, handed to someone else, or seen in the project listing. A deck is now a file in the project'sDecksfolder holding that deck's grid width and the operation sets placed on it, and the operations page's tabs are exactly the deck files the project crawl found. The file name is the deck's name (nothing inside the file repeats it), so renaming a tab renames the file; adding a tab creates one; deleting a tab deletes it along with the operations it held (the remote app confirms first). There is no save button and no unsaved state: every edit — from the deck functions and from the generic property REST alike — writes the deck it landed in, and only that one. Dropping a*.deck.jsoninto the project folder and re-scanning adds it as a tab.OperationManager.operationSets/decksarePersistScope.Custom, so the live scene carries nothing about decks at all; switching scenes within a project keeps the decks, and switching projects replaces them. One consequence worth knowing: a snapshot (andPOST /live/scene/export) does not carry decks, since both write Scene scope. MotionSourceBase.OnSampleResolved(sample, supplied)lets a source derive its reference point from the sample itself. It runs once the frame's sample is resolved and before the sample is placed, whether the sample was taken live or read back off a supplied frame. On a supplied frame this is the only place a source sees the capture at all, so a placement derived from it (a camera fit, say) belongs here.VirgoMotionSourceuses it to fit a replay to the take's own capture camera.
Changed
- The asset catalog is no longer written to the live scene, and a prop is put out as a scene object like everything else.
ExternalAssetManagerkept a persistence shadow of its catalog (_persistedAssets) holding every entry that was in use, which made the live scene carry a second, quieter answer to a question the show already answered: which avatar is out isExternalAvatarSource.selectedAvatar, which stage is standing isStageManager.activeSet/loadedSets, and a prop that is out is the instance in the scene. For props there was no such answer, so the entry doubled as one — and as the mooring its saved overrides hung from, through anobjectIdthe loaded wrapper was re-keyed to. A prop now reaches the stage only as a scene instance, spawned from the scene page's "+" and persisted by its@prefab(IInstantiableProp.instanceKey) like a camera or any other object, which is also what lets the same prop be out several times. The manager no longer implements the (de)serialize callbacks at all, so nothing about the catalog — which files this machine has on disk — reaches a saved scene.AssetBase.state,AssetBase.objectId's persistence andreloadsOnAvatarChangego with it (a scene-level instance is not destroyed with the avatar, so nothing has to be reloaded onto the next one), replaced byAssetBase.isLoadablefor a catalog whose entries are not placements. The prop page is a list of the prop files the project has, with no load toggle and no per-entry property pane; both moved to the scene page, where the instance is. A scene saved by an earlier build is brought forward on load rather than read as-is (see A live scene written before this is read in the terms of it below). A free-standing*.glb/*.gltfis put out as a GLTF Model object (itspathpicks the file), and a prop*.preset.jsonis no longer loadable at all; prop presets could not be created since Save as Preset was removed, and reading them was the last thing keeping the shadow alive. - "Save as Preset" is gone. It wrote a named
*.preset.jsonfrom whatever was loaded, and for avatars it had stopped working anyway — the id the remote app hands over names the avatar's GameObject wrapper, which the resolver compared againstAvatarControllerand rejected. What it existed for is now automatic: an avatar's settings are kept in its own preset beside it (see above), written without anyone pressing anything.ExternalAssetManager.SaveAsPresetand the remote app's button, name dialog and its translations are removed;AssetStateSnapshot.CaptureFullDeltaandPropAsset.CaptureDeltaState, which existed for it, go with them. Reading*.preset.jsonis untouched: presets already in a project still list and load as before. - An avatar's settings belong to the avatar, and are kept in a preset file of its own rather than in the live scene. The expression config (input weight adjustments, blink sync, the neutral expression, the expression list), the mesh visibility overrides, the animator parameter overrides, the body override clip, the avatar layer and the lower-body lock are all about one model — its meshes, its animator, its blend shapes. Saved with the scene they were lost the moment another scene was opened, and carried over onto whichever avatar came next. These members of
AvatarControllerare nowPersistScope.Custom, so the live scene writes none of them, and the controller writes them instead into a preset (*.preset.json, the same format a named preset uses) holding the delta from the avatar's defaults. An avatar that is a file is answered by a file beside it under the same base name —Avatars/hoge.vrmbyAvatars/hoge.preset.json— so copying an avatar and its settings elsewhere is moving two files that already look like a pair; a built-in avatar, which has no file of its own, is answered from{project}/Settings/AvatarPresets/keyed by its catalog GUID, as is the scene's own avatar (default) and an avatar file outside the project (whose folder is not ours to write into). Such a file is not an entry of its own in the project: it is the avatar beside it, and the crawl skips it by path so one avatar does not list twice. Like a deck file there is no save button — an edit is written half a second after the last one, what the controller holds is written for the outgoing avatar on every switch and when the controller is disabled, and an avatar left at its defaults writes no file at all (resetting everything removes it). Switching avatars now puts these members back to their defaults before applying the incoming avatar's file, where before the previous avatar's values simply stayed on the controller. A live scene saved by an earlier build still restores them, and the next write moves them into the avatar's own file; saving that scene again drops them from it. A named preset (Save as Preset) now carries both scopes, so it brings back the avatar's settings along with the placement the live scene holds. When a scene holds more than oneAvatarController— an additively loaded set can bring one — only the controller avatar loads are routed to keeps a file: which avatar is out is answered once, about that one controller, so both keeping settings would have them write the same file and overwrite each other. The other leaves its members where its scene put them. Recording is unchanged: the members a take carried still declare their lane, so hiding a mesh or locking the lower body mid-show is still recorded. - The screen follows whichever camera the standing set brings, and
ScreenControlleris gone — its work belongs toScreenManager. The output settings sat on a component on one camera in the bootstrap scene, which made that camera everybody's: a set could not bring its own framing, post-processing or background without fighting the one the app had already put on air.ScreenManageris a persistent object in the Live Studio System prefab, alongsideStageManagerandEnvironmentLight, and it holds the settings while finding the camera — the camera in the scene of the set that is standing, falling back to the bootstrap scene's. Resolution and full screen are re-applied once and never move, because they belong to the window this app owns; only the background and Spout follow the camera across a switch, and the camera being left behind has its target texture and sender taken back off it. Exactly one camera is left running: every other camera carrying aCinemachineBrainis switched off along with its audio listener, since otherwise two brains follow the same virtual cameras and draw the same picture twice over, and two enabled listeners is a warning and an undefined listening position. A camera without a brain — a render-texture feed, an overlay — is somebody else's and is left alone, and a camera on an inactive GameObject is read as the author having switched it off deliberately. The Spout stream is now published under a fixed name rather than the camera's, so a set switch no longer drops the source out of OBS. The first camera resolved still seeds the starting background, so a fresh project inherits what the scene author set up; only the first, or each set's authored background would overwrite the operator's saved setting the moment that set loaded.ScreenControlleritself stays as an empty[Obsolete]component so a scene or prefab still referencing it opens without a missing script — the reference resolves, the component does nothing, and it can be removed at leisure. It exposes nothing, sinceScreenas a live class isScreenManager's name now and two types claiming it would fight over the settings file. An existing project'sScreen.settings.jsoncarries over unchanged (seejp.lilium.remotecontrol), and the shippedScreenprefab is now a plain camera, so every scene referencing it follows without being touched. - Picking a stage on the stage page is a complete switch. Selecting a set flagged it active and left every other loaded set standing, which is the multi-scene reading of the operation; from the operator's side, choosing a stage is replacing the stage. The page now calls
StageManager.SwitchToSet, which unloads the others and loads the target on demand — the same path the deck's switch-stage operation has always taken. Holding several sets at once is unchanged and still reachable: the per-set load toggles are where they were, andSetActiveSetstill moves only the active flag for callers that want it.SwitchToSettakes an id whereSwitchToSetByNametakes a name, because names are not unique — two bundles can carry the same one — and anything holding the entry itself should say which set it means. - The Camera, GLTF Model and External Avatar Controller prefabs are declared in the shipped
UnityBasicLiveClassesasset, following the move injp.lilium.remotecontrol. The scene page's "+" offers the same three in the same order, at the same access levels; the camera page's "+" now offers the camera, which it could not before because the list lived on the scene page. A project of your own adds its prefabs to its own live class asset -- the shipped one stays read-only -- and the Studio UI definition no longer carries a factory list at all. FrameRecorderControllermoved tojp.lilium.remotecontrol(Lilium.RemoteControl), and this package now only tells it where takes go. Everything the component did was that package's — the gate, the recorder, the replayer, retaining the state and structure systems for the length of a take, putting the inventory back on a supplied frame — except one thing: that a take belongs to the open project, the same way scenes, decks and avatars do. That answer stays here asFrameRecorderProject, which installs itself as the recorder'srecordingFolderProvideron both a domain reload and a play — pointing it at the project'sRecordingsfolder — and takes a take's picture when recording begins. The recorder is no longer a component either (seejp.lilium.remotecontrol): it is a registered object with a fixed id, so the Live Studio System prefab no longer carries it, and its exposed name is stillFrameRecorder. Breaking only for source namingLilium.LiveStudio.FrameRecorderController; nothing has shipped it.ResolveRecordingFolderand its tests came along toFrameRecorderProject.- A light is an ordinary GameObject that happens to carry a
Lightcomponent —LiveLightis gone. The proxy class was a copy ofLiveGameObjectWithTransform(the sametransformproperty, the sameTransformRefparent, the same hierarchy re-attach) with fourLightproperties bolted on, so a light no longer needs a class of its own: the scene entry is a plainLiveGameObjectWithTransform, andenabled/color/intensity/shadowscome from a live class asset shipped with this package (Runtime/Resources/Settings/UnityBasicLiveClasses.asset), applied byLiveStudioLiveClassRegistryat load. Nothing else had to be built: aLiveGameObjectalready lists every component whose type has a registered live class, and the serializer already writes those as@sourceentries, so the values are saved and restored exactly as before. The declarations register globally rather than perRemoteControlContainerbecause an unregistered type is skipped by the serializer — wiring them per scene would silently drop a light's values in any scene that forgot the reference; assets that travel with a set bundle still go through the container list. Two things look different in the remote app: a light's own properties render one level in, inside the component pane, andshadowsis the realLightShadowsdropdown (registered through[assembly: LiveExternalEnum]) rather than the old on/offshadowtoggle. Every scene shipped here was migrated in place and kept its light's live-object id, soenabled,color,intensityand the transform still restore from an existing live scene;shadowand the light'sparentdo not carry over, since neither name survives.LiveLightFactorygoes with it — no UI definition referenced it and the "+" menu never offered a light. - Takes and snapshots are project assets side by side, on one Recordings page. A snapshot is live data one frame long and a take is a run of it, but they lived in separate folders with separate pages.
RecordingAssetlists every*.live.bin(and legacy*.livedata) in the project the waySnapshotAssetlists snapshots, so a take appears on the project page like any other asset — with its picture served by the genericGET /live/asset/{key}/@image, aPlaybutton, and the ordinary delete.RecordingManageris what the remote app drives for takes: the transport — record, replay, one stop,loop, andseekProgress, a 0..1 slider that scrubs the take and pauses it while being dragged, since a running playhead and a drag fight over the same frame — plus the take list, in the same shape asSnapshotManager's so one card can draw either. When recording starts, the live camera's view is written beside the take as its picture (LiveCameraThumbnail, shared with snapshots, triggered byFrameRecorderController.onRecordingStarted). The status readouts (isRecording,recordedFrames,isReplaying, …) are on the recorder object itself, andkeyframeInterval/compressare project settings. ⚠ Snapshots are now written to the project'sRecordingsfolder, next to the takes, instead ofSnapshots. The snapshot list reads only that folder, so move existing*.snapshot.json/*.snapshot.pngfiles across to keep them on the page (the project listing still finds them where they are). - ⚠ A motion source records and replays the pose it produces, and the pose is in muscle space.
HumanoidPoseDatais now a humanoid pose in muscle space —bodyPosition(divided by the avatar's scale),bodyRotationand the 95 muscles (kMuscleCount), each with a presence value alongside — instead of per-bone rotations and a hip position. A bone rotation only means something against the rest pose it was authored on, so a take in that form could only replay on the model it was shot with; muscles are normalized per joint, so the same numbers describe the same pose on a different body.MotionSourceBasenow owns putting the pose on the state lane and reading it back, so a source only has to sample: implementTrySample(out AvatarAnimationData, out long sampledFrom)andframeSource, and optionallyOnFrameHeadBeginandPlacementMatrix. The placement is applied after the pose reaches the lane, so what is recorded is the unplaced pose and the reference point can still be edited during a replay — and a scene replaying a take needs no capture machinery at all. Breaking for every custom motion source and for code readingpose.hipPosition/boneRotations/bonePresences; a derived class that overridesOnEnable/OnDisablemust now call the base.AvatarPoseWriterapplies the muscle pose to avatars that drive themselves (StandardAvatar,VRM0Avatar), andHumanMusclenames the 95 muscles, so a recorded pose reads as joints rather than numbered floats. - The selected avatar and the standing stage are saved in the live scene and restored as soon as their assets turn up.
ExternalAvatarSource.selectedAvatarandStageManager.activeSet/loadedSetsused to read the catalog's current state, which is empty right after a scene restores and before the asset crawl has finished. They now hold the operator's choice, saved to the scene and carried on the state lane by the same declaration, and reconcile against the catalog as it fills in.AvatarController.bodyOverrideClipKeyis a single key for the untracked-body override clip (a baked clip or a pack member), so that override reaches a take as well. - The screen's background type and color are project settings now, like the rest of its output settings (resolution, full screen, Spout): the compositing background is chosen per machine, and is not part of the stage being recorded.
- The Project Settings page is
Project/Lilium Live Studio(wasProject/Live Studio). - The Transform manipulator moved into
jp.lilium.remotecontrol; this package now registers no REST route at all. The manipulator only ever touched RemoteControl's own concepts — live objects,TransformValue, property paths — and its session/frame/pose routes were the last bespoke REST this package carried, so the whole stack (ManipulatorApiHandler,ManipulatorCameraService,IManipulatorTarget) now lives in the base package and every host application gets gizmo editing for free. The wire is unchanged: same four/live/manipulator/*routes, same payloads.PropAttachmentkeeps implementingIManipulatorTarget, now as a RemoteControl interface — the dependency finally points the right way. With the camera family gone (see below) and this move, LiveStudio's REST reference disappears entirely: everything the package adds is live objects on the generic surface. - Load and build notifications are C# events now; the observer interfaces are gone.
IVRMLoadObserverand theVRMLoadObserverbroadcast wrapper are replaced by static events onVRMLoader(onLoadStarted,onLoaded,onLoadError,onLoadProgress), andIAvatarBuildObserverbyAvatarBuildNotifier.onAvatarBuilt. Both interfaces existed only to receive notifications, yet lived in theService<T>locator registry — so every observer had to implement all four VRM callbacks (usually as empty stubs) and it was never visible from aService<T>use whether the registered object was something to look up or merely a listener. Events subscribe per notification, tolerate unsubscription from inside a callback by language guarantee, and leaveService<T>for actual lookups. Breaking for implementors of either interface: subscribe to the events instead (AvatarBuildNotifier.BuildAndNotifycallers are unaffected). /live/snapshot/imageis gone too; a snapshot's screenshot comes from the same/live/asset/{key}/@image. A snapshot file is one of the project's asset kinds, so its picture is an asset's picture — there was no reason for a second image route, keyed differently (by snapshot name rather than asset reference) and reading its own folder.AssetBasegainsthumbnailFilePathfor kinds whose preview is a plain file beside the asset rather than something packed inside it;SnapshotAssetreturns its*.snapshot.pngand the provider serves it straight from disk (deliberately not throughThumbnailCache— the file already is the cache).SnapshotInfogainsreference, the snapshot's project-relative asset reference, which is what a client now passes asid. A side effect worth having: snapshots listed on the project page show their screenshot too, where before only the dedicated snapshot page did. Breaking, with no compatibility shim./live/avatar/imageis gone; asset thumbnails are served by RemoteControl's/live/asset/{key}/@image. The endpoint accepted any asset kind from the day props and sets grew previews — only the path still said "avatar", and the UE port had to carry a comment explaining that the word meant nothing. Serving pictures for assets is not avatar work, and it is not even LiveStudio work: it belongs beside/live/asset/{key}and/live/assets, which are RemoteControl's.AvatarImageHandleris therefore replaced byAssetThumbnailProvider, which registers the same lookup (external asset →ThumbnailCache, with first-request VRM/glb extraction) throughAssetRegistry.SetThumbnailProvider. Behaviour is unchanged apart from the path and a missing key now answering 400 instead of 404. Breaking, with no compatibility shim — the remote app moves with it.- Following RemoteControl's namespace move, every route this package serves is under
/live/*instead of/api/*.jp.lilium.livestudiois a general-purpose base meant to be embedded by applications that are not VirgoMotion, so the same reasoning applies to it:camera,assets,avatar,expressionsandcommandsare names a host application wants for itself, and this package shares the host's route table./api/camera→/live/camera(with/switchand/image),/api/vrm/loadand/api/vrm/reset→/live/vrm/*,/api/avatar/image→/live/avatar/image,/api/expressions→/live/expressions,/api/assets→/live/assets,/api/snapshot/image→/live/snapshot/image,/api/manipulator/*→/live/manipulator/*,/api/input-actionsand/api/input-actions/bind→/live/input-actionsand/live/input-actions/bind,/api/commands→/live/commands. Response bodies and query parameters are untouched — only the prefix moves. Breaking, with no compatibility shim. ICameraControlleris renamedCameraControllerBase: it has always been anabstract class, not an interface, and theIprefix misled — the new name matches the package's other abstract bases (AssetBase,OperationBase,MotionSourceBase). No serialized data is affected:[SerializeReference]and the live@typeboth record concrete controller types only, and the base is never instantiated.- Snapshots are one of the project's asset kinds, so a
*.snapshot.jsonfile is listed on the project page beside the live scenes and decks that already were — with aRestorebutton on it and the ordinary delete (which takes the*.snapshot.pngthumbnail with it, since which files an entry owns is now the kind's own business throughAssetBase.DeleteFiles). Snapshots were the one file the app itself authors that the project listing could not see:SnapshotManagerscanned theSnapshotsfolder on its own and never went throughAssetTypeRegistry. It still owns the dedicated snapshot page — the thumbnails, the capture-time ordering and the take/restore/delete flow are unchanged — and now also asks for a re-crawl when a snapshot appears or goes away, so both views agree. - The Remote app toolbar button is hosted by Remote Control's shared toolbar host and takes its tints from there, so it and the new Remote Control server toggle read as one pair: the Remote app button sits immediately right of the server toggle whatever order the two assemblies happen to initialize in. The button itself is unchanged.
- The package's asset-creation entries share a single Live Studio submenu.
Assets ▸ Createlisted three roots for one package —Live Studio(VRM Avatar Setup Settings),LiveStudio(AvatarExpressionConfig) andLilium Live Studio(Bundle Thumbnail) — which read as three unrelated packages. The two odd ones out now sit underLive Studio, andAvatarExpressionConfigis spelledAvatar Expression Configto match its siblings. Menu paths only affect where the command appears; existing assets and their serialized types are untouched. - Following RemoteControl's Exposed → Live rename:
ExposedCamera/ExposedLightare nowLiveCamera/LiveLight. Their serialized@typenames change with the class names, so both carry[FormerlyNamedAs]and scenes saved under the old names still restore (and re-save under the new names). The factories areLiveCameraFactory/LiveLightFactorywith[MovedFrom]covering Unity-serialized references, and the runtime folderExposedType/is nowLiveType/. - Expression weights no longer depend on face tracking.
VRM1Avatar.Updateused to return early whenever the body driver reported no tracking, which skippedIExpressionResolver.Resolveentirely — sosmoothedOutputsnever updated and every expression read 0. An operator driving expressions from a gamepad binding or the remote app therefore got nothing at all while the avatar was still on screen in its base pose. Resolve and the expression apply now run every frame (with neutral ARKit weights when untracked) in all three avatar backends —VRM1Avatar,VRCAvatarandVRCFTAvatar. Everything that IS the tracking input stays gated on tracking: blink, look-at, visemes, the ARKit-derived expressions and the face-tracking parameter writes. Applying neutral values for those would fight the base pose animation and the FX clips the avatar hands them to when untracked. - The animation bundle is now a general-purpose asset pack:
*.anim.lsbbecomes*.pack.lsb, and a pack may hold assets of any type rather thanAnimationCliponly. The kind-bearing extensions are unchanged —*.set.lsb,*.avatar.lsband*.prop.lsbeach name what they contain, and renaming them would delete the only content-free way to classify them — so the pack takes the catch-all slot instead: its name deliberately says nothing about its payload.PackBundleLoader(wasAnimationBundleLoader) therefore loads members untyped and registers each under itsfile:<path>#<assetName>key, letting the runtime type of each member decide which selectors offer it; becauseGET /live/assets?type=already filters the registry by type name, a new selectable asset type needs no change here. Since a pack's name no longer hints at its contents, that endpoint can no longer skip the pre-warm for non-clip types and now opens every pack once per session (the existing per-file cache keeps that a one-time cost). Existing*.anim.lsbfiles keep working as legacy input — the extension stays registered, so neither the files nor thefile:references saved against them need touching — and the export menu is nowAssets/Lilium Live Studio/Export Asset Pack (.pack.lsb), accepting any selected main asset (folders and scenes excluded) with the pack-wide unique-name requirement now applied across types. Imported packs land inPacks/rather thanAnimations/; the project crawl is recursive, so files already underAnimations/are still found.AnimationBundleAssetis renamedPackBundleAssetand carries[FormerlyNamedAs("AnimationBundleAsset")]so scenes saved under the old@typestill restore, andExternalAssetKey.BuildClipKey/TryParseClipKeyare renamedBuildMemberKey/TryParseMemberKey(the key format itself is unchanged). Listing the members of a pack is now a second filter axis onGET /live/assets(see below) and returns every member whatever its type. GET /live/animation/clipsis gone; listing one pack's members is nowGET /live/assets?pack=<assetId>. The two endpoints had drifted into the same job under different names: both answer "what can I write into this asset reference slot", both hand backfile:<path>#<name>keys built the same way, and both open a pack throughPackBundleLoaderas a side effect of listing it. All that actually differed was the filter axis — by type, or by containing pack — and the response shape wrapped around it./live/assetsnow takes both, and they combine:?pack=X&type=AnimationCliplists just the clips inside X. The reply is the ordinary{ "type", "assets": [{ "key", "name", "type" }, ...] }either way, so a member now arrives with its runtime type where the oldclipsarray gave only a name and a key — which is what a general-purpose pack needs, since its contents are no longer clips by construction. Drilling into a pack stays as cheap as it was:?pack=opens that one pack and skips the catalog-wide pre-warm entirely, and its keys are read back fromAssetRegistryrather than rebuilt, so the same member lists byte-identically on both axes. An unknownpackid, or an id that is not a pack, is a 404, while an unmatchedtypeis still just an empty catalog. Breaking, with no compatibility shim: a remote app build that still calls/live/animation/clipsgets a 404 for the pack expansion.PackBundleAsset.GetMemberNamesAsync()is replaced byGetMembersAsync(), which returns the members themselves.
Removed
- The whole
/live/camerafamily is gone —GET /live/camera,POST /live/camera,POST /live/camera/switchandGET /live/camera/image; a camera is reached through the generic live-object surface like anything else. A camera has been an ordinary live object (LiveCamera) for a long time, so listing one isGET /live/objectand going live is itsSwitchlive function — the remote app already drew its camera page from the object list and wrote lens and controller values as plain properties, and the list route's reply was fetched into state that nothing read.POST /live/camerahad no caller at all: of its three actions onlyswitchdid anything, the two streaming ones flipped a flag whose broadcast body had been commented out, and the UE port never implemented the route in the first place. Removing them takes the handler's whole heartbeat with it — the 100 ms poll for a changed live camera, thecamera_updatebroadcast it pushed and thecamera_imageevent — because the change feed reports the same thing generically:LiveCamera.priority's setter now records real transitions inLiveChangeLog, so a switch (which happens through direct C# writes the REST path never sees) marks exactly the two affected cameras for refetch, the same pattern the UE port already used. Without that, a client's LIVE badge froze after a switch. The preview picture, the one thing that genuinely cannot ride JSON, becomes an image member instead of a route:LiveCamera.previewis a[LiveProperty, ImagePreview]getter of RemoteControl's newLiveImageData, soGET /live/object/{cameraId}/previewanswers the PNG while JSON reads carry that address as the value (see the RemoteControl entry).CameraApiHandleris deleted outright, leaving the manipulator as this package's only bespoke REST. Two properties also join the generic surface to make it complete:LiveCamera.isLiveandaspectare now exposed (read-only, not persisted, as they are derived from the Cinemachine camera on every read), matching the UE proxy, which had both from the start — a client draws the LIVE badge and sizes the preview from them, and against Unity it previously could not. Breaking, with no compatibility shim. GET /live/input-actionsandPOST /live/input-actions/bindare gone; the input map is a live object like anything else. Neither route had a caller left — the remote app's input page was removed at some point and only its translation strings stayed behind — while the object side was already halfway there:AvatarInputhas been the live classInputActionsall along, exposingdeviceName,actionNamesand asettingssnapshot of every binding. What was missing was a per-action handle, so it gainsactions, a read-only array rebuilt from the live map whose elements carry the actionnameas their[LiveKey], the bound key as a human-readablebinding, andenabled. Rebinding is that element'sRebindlive function, which runs the sameRuntimeKeyBindingSystempath as expression and operation rebinding; it returns as soon as listening starts, and the captured key shows up inbinding. So an action is now addressed by name (actions[ToggleCamera].binding) rather than by a list index into a bespoke DTO, and a client renders the list and the rebind button from the generic object surface.InputActionsApiHandlerand its four DTOs are removed with it. Breaking, with no compatibility shim.POST /live/vrm/loadandPOST /live/vrm/resetare gone; loading and clearing an avatar happen through the generic live-object surface. Both had already lost every caller. Choosing an avatar file is registering it withExternalAssetManagerand enabling it (avatars are exclusive, so enabling one clears the rest), and a component that owns a model path —GltfModel.pathand the like — loads from its own setter when the property is written. The remote app's file selector had moved to that some time ago and only writes the property; the two functions still calling these routes were exported by a hook and invoked by nothing. What the routes did offer over a property write was progress, and that part stays:VrmLoadApiHandlerbecomesVrmLoadNotifier, a plain listener onVRMLoaderthat pushes the samevrm_load_start/vrm_load_progress/vrm_load_complete/vrm_load_errorevents into the inbox, with the same payloads, and still tells a client that connects mid-load. Progress now reports every load rather than only remote-initiated ones — it used to be gated on a client id that only the REST path ever set, so a load started from the app's own UI silently skipped its progress events. Breaking, with no compatibility shim.GETandPOST /live/expressionsare gone; expressions are reached through the generic live-object surface like anything else. Both halves of the route already had an exact equivalent there, and the remote app had moved to it: the expression list is the live functionPOST /live/function/{avatarId}/getavailableexpressions, and a weight is the ordinary live propertyGET/PUT /live/object/{avatarId}/expressions[<name>]/weight(AvatarController.ExpressionEntry, whosenameis the[LiveKey]). TheGETwas in fact the poorer of the two, reportingweightas a hard-coded0anddisplayNameas a copy ofname— live weights have never flowed from this route. What the route did offer was addressing the active avatar by expression name alone, with no object id to resolve; that is the one thing lost, and no client used it.ExpressionsApiHandlerand its four DTOs are removed with it. Breaking, with no compatibility shim.ExpressionServiceis untouched — it is whatAvatarController.expressionsreads through.POST /live/commandsis gone. It was already[Obsolete]with live functions (POST /live/function/{id}/{path}) named as its replacement, and it had stopped being reachable in practice: the only command it ever implemented wasinput_action, whose body never simulated a press at all — it just enabled or disabled the action and left a TODO — and nothing called it. On the remote app side the whole chain that fed it was dead too:sendCommand→sendMessage→ acommonProps.sendMessagethat no page was passed, plus auseHomePagehook with astartCalibrationthat no component imported (calibration is Fusion's endpoint, not this one). All of that is removed with the route.POST /live/commands/quitandPOST /live/commands/resetare unaffected — they are separate routes served by RemoteControl'sQuitApiHandler/ResetApiHandler, not by this handler, and both are still in use.FrameRecorderProjectis renamedLiveDataRecorderProject. This follows the rename ofFrameRecorderControllertoLiveDataRecorderinjp.lilium.remotecontrol. This is a breaking change for source.
Fixed
- A scene opened while the app was already running stood none of its external props up. A
*.prop.lsbprop instance is restored from a@prefabkey that only resolves once this machine's catalog knows the file, so the restore queues what it cannot make yet and something has to come back for it. The only thing that did was the project crawl — and no crawl follows a scene being opened, so the props appeared at startup and were missing from the same scene opened from the remote app, until a restart. They were never lost (an undrained entry is re-saved verbatim), but nothing said why the stage was bare.ExternalAssetManagernow also drains onLiveSceneSerializer.onLiveSceneRestored, which is raised whenever a scene is read rather than only when the catalog is rebuilt. The gap opened when the catalog stopped being saved: its deserialize callback was the other path into the drain, and it went with it. - An avatar item spawned from the scene page's "+" drove none of its animator parameters and none of its expressions.
AvatarItemlooked for the avatar by walking up its parent chain, which only finds one while the item is a child of the avatar — and an item added from the "+" stands in the base scene on its own. Its socket follow worked, so the item was in the right place and visibly dead. It now asks the avatar service, the same wayPropAttachment's socket lookup andAvatarChairalready did, so it works wherever it lives. A resolved reference is also re-resolved properly across an avatar swap: it is held as an interface, which does not see Unity's destroyed-object==, so a swapped-out avatar's components looked present for the rest of the session. - A built-in entry a saved scene no longer recognises is dropped instead of haunting the scene. A built-in asset has no file path to rebuild an id from, so an entry whose identity cannot be resolved — a built-in set the project stopped declaring, or one saved when the identity had another shape — came back with an empty id: it could never load (the diff skips empty ids), it dodged the de-dup that keys on id, and it was written straight back out on every save.
ExternalAssetManagernow drops such an entry as the restore lands, with a warning naming it. Nothing is lost — every still-declared built-in is re-injected immediately after. - A built-in set renamed or moved in the editor takes effect on a scene that was saved with it loaded. The de-dup keeps the restored entry (that is what carries its enabled state), so it also carried whatever name and path it was saved with: a renamed set kept the old name for the session, and one whose scene had moved kept a path that no longer loads. Its display fields are now re-read from the declaration as the catalog is injected.
- A prop instance spawned from a bundle came back on a replay; before, it silently did not. A take carries the key an instance was made from, and the maker that rebuilds it can only answer for a prefab already in hand — true of a built-in prop, whose catalogue reads from Resources synchronously, and not of an external
*.prop.lsbone, whose key is a project-relative path and whose prefab has to be read out of a bundle first. Nothing was doing that read, so the instance was counted as unresolved on every frame of the replay and reported nowhere.PropInstanceRecipeResolvernow stands as a second recipe resolver (see the resolver chain injp.lilium.remotecontrol) and does one thing: it starts the read and puts the result in thePrefabRegistry, which is what brings the key within reach of the maker that already knew how to rebuild these. It deliberately never creates the object — creating is the reconcile's, which is looking at the inventory of the frame it is on, so an instance scrubbed away mid-read is simply never made rather than appearing late over a world that has moved on. While the read runs the replay is held (FrameGate.HoldSupply, the same wait an asset load takes) rather than run on without the instance: how long a bundle takes is a property of this machine, not of the take. A read that comes back with nothing — a bundle file that is gone, which the loader reports as an error — is remembered and not retried until the catalog changes, or one broken prop would fill the console with the same line at every frame head. The "+" also registers a prop's prefab as it spawns it, so a replay in the same session resolves without opening the bundle again.ExternalAssetManagerkeeps the single answer to "what prefab is this key": the deferred live-scene restore and the replay both go through it. - The avatar the remote app showed as selected could briefly be the previous one after a switch:
AvatarSelection.GetSelectedNamereturned the first enabled avatar in list order, and reconciling briefly leaves both enabled. It now reads the manager's own pick (selectedExclusive), so it agrees with what is about to load. LiveCamera's preview capture no longer destroys its RenderTexture while it is still the camera's target texture (Unity logged a warning); the camera is detached first.- An avatar's meshes share one light/reflection probe anchor, so its parts are no longer lit differently. Each renderer used to sample probes at its own bounds center — nothing on the VRM 1.0 load path, in a
.lsavataror in a built-in prefab setRenderer.probeAnchor— so hair, body and clothing could pick up visibly different ambient light.AvatarControllernow points everySkinnedMeshRenderer/MeshRendererunder the avatar (inactive ones included) at the Chest bone, falling back to Spine and then Hips when the rig lacks it. A renderer whose anchor was already set in the prefab is left as the author set it.
[0.25.3] - 2026-07-22
Added
- Opening a project asks before discarding unsaved live-scene edits (Save / Don't Save / Cancel), instead of dropping them silently. The prompt is raised through
RemoteConfirmSystem, so it shows both on the machine and in the remote app that asked for the project — a cancelled "Save As" abandons the open rather than discarding the very edits the user just chose to keep.
Fixed
- Opening a project now leaves the app in the state a launch into that project would produce, instead of carrying the previously open project over.
OpenProjectreused the ordinary live-scene load, which only reloads the base Unity scene when the incoming scene targets a different one — so two projects sharing a base scene skipped the reload entirely, leaving the old project's loaded props/avatars in place and keeping every value the incoming (delta) scene file omits. Project-scoped settings were worse off: they are not in the scene file at all and are applied additively, so any setting the new project does not record kept the old project's value.LiveSceneManager.OpenProjectSceneresets every exposed object to its captured defaults and forces the base-scene reload before restoring. The project folder crawl also moved after the reset, so the previous project's enabled/loaded catalog entries — which the crawl deliberately never prunes — no longer survive the switch.
Changed
- The built-in asset catalog is no longer
AnimationClip-specific. Which asset types are baked out of theResourcesfolders now comes fromBuiltinAssetTypeRegistry, the built-in counterpart ofAssetTypeRegistry: a kind declares the Unity asset type it owns and theAssetBaseentry it lists as, so a package can add one without touching the baker or the runtime registry.BuiltinAssetCatalogstores a single entry list whose entries name their owning kind instead of one array per type;BuiltinAssetCatalogBuilderscans once per registered kind, classifies imports by asset type rather than by file extension (so a kind never enumerates the extensions its type can come from), and rebuilds once per editor load, which is what picks up a newly registered kind — no asset import announces one. The reference-resource behavior ofBuiltinAnimationAsset(no load/unload toggle, never pruned, never persisted) moved to a sharedBuiltinAssetBase, leaving each kind as its own exposed@type. Existing baked catalogs are rewritten in the new shape on the next editor load.
[0.25.2] - 2026-07-21
Fixed
- A child process started through
ChildProcessHostis now tied to the host's lifetime by a kill-on-close job object. Every graceful stop path ran from managed cleanup, so a host that died without unwinding (a crash, End task, the quit watchdog) orphaned Fusion and the Remote app, which went on holding their ports and locking build output.StartChildApplicationalso returns early when the process fails to start instead of falling through to use it. ExpressionEntry.weightno longer throws on the empty-named template instance that the array diff builds:FacialKeyrejects an empty custom name, and there is no expression slot to drive, so the property now reads0and ignores writes. The exception escapedwantsToQuit, and Unity then let the quit through without the unsaved-changes check ever running — which looks like a successful quit and is not.
Changed
- The render quality setting moved off the static
LiveSceneManagerclass intoRenderQuality, a serializableIExposedObject(likeStageManager). Whether the remote app's settings page shows the quality section is now decided by registration: the settingsNavigatePagelists theRenderQualityid, andNavigateObjectSelectorskips ids that resolve to nothing, so an app exposes quality control by putting aRenderQualityin aRemoteControlContainer(or the hostRemoteControlBehaviour) object list and hides it by leaving the instance out. TheLive Studio Systemprefab registers one by default.LiveSceneManager.quality/.qualityNames/.currentQualityIndex/.SetQualityare removed, and the persisted level moves fromLiveSceneManager.settings.jsontoRenderQuality.settings.json(the level resets to the project default once).
[0.25.0] - 2026-07-17
Added
AddSwitchAvatarOperationcreates an operation set that switches the active avatar, with the deck tile's full-frame background set to that avatar's thumbnail.DeckControlgains abackgroundAssetIdstored as a portable project-relative reference (resolved back byAvatarImageHandler), so a saved deck survives moving the project or opening it on another machine.InvokeFunctionOperationcan invoke exposed functions that take arguments and reach nested functions: it stores the positional call arguments as a JSON array (argsJson) plus an optionalpropertyPath(e.g. aStageManagerset element'sWarpTo), not just a no-argument function on the target.AddFunctionOperationgains trailing optionalargsJson/propertyPathparameters, keeping the existing no-argument overload source-compatible.VRCAvatarnow drives body animation, tracking gating, mesh visibility and the lower-body lock throughAvatarBodyDriver(PlayableGraph) likeVRCFTAvatar, so VRChat avatars gain untracked-part body-override clips and the lower-body pose lock. Viseme / voice and expression parameters are read and written through the wrapped controller playable.- Built-in animation asset catalog:
AnimationClips shipped inResourcesare baked into aBuiltinAssetCatalog(EditorBuiltinAssetCatalogBuilder) and surfaced asBuiltinAnimationAssetresources.BuiltinAssetRegistryregisters each clip inAssetRegistryby GUID eagerly and idempotently (play start, editor load, on demand), so persisted body-override references resolve synchronously on scene restore. Catalog entries are injected into the project asset list and markedisBuiltin, soExternalAssetManagernever prunes or persists them and removal is rejected. AvatarController's body-override slot now sources its candidates fromGET /api/assets?type=AnimationClip(built-inResourcesclips plus external*.anim.lsbbundles) instead of the inspector_bodyOverrideClipPresetsarray, which is removed.VRCFTAvatarexposes_bodyOverrideClipas a serialized field and forwards it onInitialize/OnValidate.AvatarExpressionConfig.syncBlinklevels both eye-blink weights to the smaller of the two. Some avatars have asymmetric blink-weight accuracy between the left and right eyes and blink lopsidedly; enabling this makes the eyes blink symmetrically.LiveStudioOrbitalFollow: a Body-stage Cinemachine component that orbits a single target on a sphere (yaw/pitch/distance, with position damping and a local-frame target offset), resolved in the camera's own parent frame so rotating the camera's parent rotates the whole orbit rig.OrbitalFollowCameraControllernow positions through it and drives a single tracking target for both position and aim.- The Studio Template scene ships with an
OrbitalFollowcamera out of the box.
Changed
- Cut per-frame GC and hitches along the Operation-driven property path:
SetPropertyOperationcaches its resolvedExposedProperty(self-healing on a failed typed read) and uses box-free typed accessors for its per-framebool/floatwrites instead of re-walkingFindPropertyand allocating element paths and component arrays every frame.AvatarControllercaches its expressions array (invalidated on avatar /AvatarItemchange) to avoid O(N^2) GC when resolving keyed-array paths, bumps the registry's keyed-collection generation fromInvalidateExpressionsso cached resolutions re-resolve after expression elements are rebuilt (preventing writes to stale keys), and narrowsOnPropertyChangedto reapply only the changed property instead of the full_PostSetupAvatar— which re-ran the T-pose / socket rebuild every frame and caused multi-millisecond hitches. CustomFacialKeyhashing is now allocation-free. - The Fusion UI Definition orders the capture page ahead of the license settings.
- Avatar attachment sockets created by
AvatarControllerare now prefixed withS_(S_Hips/S_Spine/S_Chest/S_Head/S_Neck/S_WristLeft/S_WristRight) so a bone of the same name can no longer shadow them; the name-based references were updated (PropAttachmentdefault socket, the bone-follower / look-at / orbital-cameraS_Headtarget). Props and cameras that referenced the old socket names need re-selecting.
Fixed
- The lower-body lock now honors a body-override clip's "Root Transform Position (Y)" offset (the humanoid muscle-clip
levelsetting).AvatarBodyDriverbaked the locked hip position and foot-IK goals withAnimationClip.SampleAnimationwhileapplyRootMotionwas disabled, which silently drops that offset, so a sitting clip authored with a Y offset (to sink the hips) locked at the un-offset height and floated — while a clip with a zero offset looked fine. The lock offset and foot goals are now re-sampled with root motion enabled so the clip's Y offset is applied; the base pose used off the lock path is unchanged. - A momentary deck button no longer intermittently misses its action on fast taps: press and release arrive as two REST calls, and when both collapsed within one
Updateframe the manager never observedheld=true, silently losing the release trigger.SetHeldnow latches a release pulse on the rising edge so it fires exactly once. - The lower-body lock no longer shakes the head from side to side on rigs whose hips parent carries a rotation (e.g. Blender Z-up armatures imported with X=-90). The root-yaw compensation was prepended to the hips local rotation, which turned the world yaw delta into roll; it is now applied as a world-space delta on the hips world rotation, independent of the rig's parent chain.
- The lower-body lock no longer clamps the legs fully straight with a degenerate knee axis on avatars whose
humanScaleis not 1. Foot IK goals were captured as raw root-local positions while the locked hip is humanScale-normalized, so the hip-to-foot distance changed and the clip's leg pose became unreachable. Goals are now captured relative to the sampled hips and rebased onto the normalized lock hip offset. - The lower-body lock height no longer shifts with avatar height:
AvatarBodyDriverlocks the hips to the override clip's hip position through a humanScale-normalized, root-relative offset applied in world space. QuitTerminationGuardnow arms a detached watchdog process on quitting instead of a managed watchdog thread, which dies with the Mono runtime before a late native teardown (WGI) wedge — this left the Player-built Fusion lingering after quit. The fire-time log, which can itself deadlock during teardown, is dropped.LookAtCameraControlleragain drives the LookAt target on its own under Cinemachine 3: it setsCustomLookAtTargetso the aim uses the assigned look-at target instead of silently falling back to the tracking target (Cinemachine 2 parity), and clears the flag on teardown.
[0.24.3] - 2026-07-09
Added
AvatarControllercan drive body parts the capture does not track with an overrideAnimationClip. The clip is chosen from Inspector presets or from an external animation bundle (*.anim.lsb, which packs several clips) via a unified asset key — a bare GUID for baked assets,file:<path>#<clip>for external ones — resolved lazily and re-applied after scene restore and REST writes so the selection survives the bundle's asynchronous load. VRM1 / VRCFT playback is supported.- A "lock lower body pose" toggle pins the hips and root to the capture anchor with foot IK, holding the lower body in place while the upper body keeps following the capture.
- Per-project session log: the Unity log is mirrored into
{project}/.livestudio/logas one file per session, alongside the native Player.log.ProjectPathsgainsGetLogDir/EnsureLogDirhelpers.
Changed
- Operation set manual hold and manual value now persist across scene reloads (previously both reset). On restore the edge baseline is seeded to the restored hold, so reloading a scene with a set left on no longer manufactures a rising edge that would re-fire the operation.
Fixed
- Avatar meshes no longer blink while the subject leaves the capture frustum. The avatar root's valid byte is now a body/face tracking bitmask (body = MediaPipe, face = ARKit) and visibility is asymmetric — hidden only when both signals drop, shown on the face rising edge — so the frame-to-frame body-validity beat can no longer flicker the meshes.
[0.24.2] - 2026-07-05
Added
- The editor gains a toolbar button to launch and close the Remote app, matching the runtime RemoteAppHost (same configured path / args and child-process handling).
LookAtCameraController(ICameraController) assigns a bone — resolved from aTransformRef(target owner name + bone name / path) — to a camera'sLookAttarget, re-resolving only when the reference changes or the avatar is swapped, so a look-at bone survives avatar reloads. OnlyLookAtis touched, leaving theTrackingtarget and any Aim component intact.BoneFollowercomponent drives its own transform to follow a bone resolved from aTransformRef, re-resolving on avatar swap. It bridges a swappable avatar bone to a Cinemachine camera authored in a separate stage / set bundle (outside the LiveStudio camera pipeline), where a direct cross-scene reference to the bone cannot survive the swap.AvatarControllergains an avatar animation layer override, letting a loaded avatar's layer be overridden per scene.ProjectManagerexposes selected / unselected camera-preview polling interval settings (per-project persist scope), controlling how often camera previews are refreshed.
Changed
- Value-mode Deck sliders now map their drag onto the source property's min/max range instead of always writing the raw normalized 0..1 value, so a slider bound to a property spanning e.g. 0..100 can drive the full range. The normalized value is retained internally so gauges and edge detection are unaffected.
- Loaded prop / avatar objects are now persisted through the live scene's top-level diff as the single source of truth. State whose owning asset has not finished loading is queued and bound once the asset arrives, and any entry that never binds during a session is preserved verbatim across a load→save cycle instead of being warned about and dropped.
Fixed
- Operation sets authored directly in a scene or prop bundle now have a stable id backfilled on init and restore. Previously they kept the default empty id, so every id-addressed function was a silent no-op and the set was skipped when rebuilding the input map.
- Multiple single-prefab props that shared the same internal AssetBundle CAB could collide when loaded concurrently ("same files already loaded"). Bundle opens are now serialized, and each built bundle is given a unique internal CAB seeded from its source asset GUID so props no longer share one.
- Loaded prop / avatar overrides (e.g. a prop moved from its authored position) are no longer lost when the live scene reloads. The load-complete re-baseline now preserves restore-applied overrides instead of folding the applied value back into the dirty-detection baseline.
[0.24.1] - 2026-06-30
Added
- Action sets can now be arranged on a Deck: a grid of push-button / toggle / slider controls, freely placed and editable from the remote app. The control type is chosen when a control is placed and is independent of the action set's input mode.
- Value-mode action sets gain a manual value override, so a slider control can drive a 0..1 value directly (throttled to the studio).
Changed
- The action "panel" concept was renamed to Deck throughout, and an action set's behaviour is now driven by a single control axis. Deck tile width is declared per control kind, missing deck panels are recreated automatically, and tile-layout fields are hidden from the generic remote editor.
Fixed
- Expression weights can now be read while the avatar is inactive.
KeyInputSource's control path is stored as a shadow field of its binding, so key bindings persist and resolve correctly.- The active live-scene path is re-resolved from startup state when entering play mode, so the last-opened scene restores reliably.
[0.24.0] - 2026-06-26
Added
- Action set / input binding system (
ActionManager/ActionSet/InputSource/ActionBase), editable from the remote app: bind a keyboard or gamepad input to an ordered list of actions (set an expression weight, switch avatar / stage, toggle a GameObject). Polymorphic input/actions use[SerializeReference]+[TypeSelector]; input modes are Button / Toggle / Value. Wired into theLive Studio Systemprefab. InvokeFunctionActionandSetPropertyActionback a generic "bind to key" affordance next to any[ExposedFunction]or bool/float control (addressed by stable id), replacing feature-specific bindings.- Mutually exclusive toggle groups for action sets: sets sharing a non-empty
groupwhose input is in Toggle mode behave as a radio group (turning one on clears the others; all-off is still allowed). SwitchStageActionperforms a complete stage switch (SwitchToSetByNameunloads the other loaded sets and loads the target on demand); the Stage page'sSetActiveSetkeeps its original non-exclusive selection behavior.AvatarChaircomponent for a chair prop the avatar sits on. It re-parents under theAvatarControllerso avatar root motion does not drag it, then makes its parts track the avatar's pelvis (Hips socket) relative to a recorded rest pose:swivel/reclinerotate a target transform about an authored local axis (hips yaw / pitch), andlateral/height/depthtranslate one along an axis (hips X / Y / Z). The rest pose is recorded via anActivateaction. Each axis (aChairAxis) has an operating range (min/max,0/0locks it), a deadzone that is the play/backlash between the source and the follow target (absorbs hips tremor), and damping (SmoothDampAnglefor rotation).- Per-project state and project-scoped settings: exposed members gain a persist scope (Scene / Project); project-scoped values are stored in per-class
{Class}.settings.json(screen output size/fullscreen/Spout and live-scene quality are now Project scope).StartupStateStoreremembers the last opened live scene per project (Settings/startup.json), andStartupSceneSwitcherredirects to it before the first scene load. - Two-tier (memory + file) thumbnail cache stored under a hidden
.livestudioproject folder (ProjectPaths).
Changed
- Breaking: the avatar-prop
AvatarPropcomponent was split into composable siblings: a new sharedProp(keeps the exposed name"Prop"; owns the socket follow + position/rotation/scale offsets) plus a behavior component.AvatarPropwas renamed toAvatarItem(the avatar→prop parameter bridge + expression driving); a[MovedFrom]keeps old type references resolving. Existing*.prop.lsbbundles carry only the old single component and therefore must be re-exported (a re-exported bundle root carries bothPropandAvatarItem); the live-scene"Prop"state key is unchanged. - Expression key bindings are now driven generically through the action system: a
SetPropertyActionwritesexpressions[name].weight(the data-driven expression slot), so the bespokeSetExpressionAction/ExpressionBindingSystemand the old/api/expressionsbind routes were removed. Existing expression key bindings must be re-created. - Shared
FormatHeaderunifies versioning across live-scene / project-settings / preset files (raw-JSON serialization, orphan prune). - Object presets (
.preset.json) now capture prop and avatar state as a delta with a lenient reader. - Button-mode one-shot actions (switch avatar / stage) now commit on key release instead of press (
ActionContext.triggered). - The
ActionManager,ExternalAssetManager, andStageManagersingletons are now hidden from the remote app's generic scene object list (HideInScene); they remain reachable through their dedicated pages.
[0.23.6] - 2026-06-23
Added
AvatarPropnow exposes ascaleOffset(Vector3, default1,1,1) that multiplies the prop's authored local scale, so an avatar-attached prop's size can be tuned from the remote app / inspector. It is applied every frame and persists across an unload/reload like the position/rotation offsets.
Changed
- Stage marks now warp the avatar through
StageManagerinstead of the standaloneAvatarStageController. EachSetBundleEntryexposes theStageMarklabels in its own set scene (the newmarksfield, scoped per set), andStageManager.WarpTo(setId, markLabel)moves the avatar's anchor to the named mark within that set's scene (an empty label warps to the origin).
Removed
- Breaking:
AvatarStageController(the "Avatar Placement" exposed object with theplace/availablePlacesdropdown of all loaded marks) was removed; warping is now per-set onStageManager.WarpTo(see above). It is also dropped from theLive Studio Systemprefab.
[0.23.5] - 2026-06-22
Changed
- Object preset files (
*.preset.json) now use thejp.lilium.remotecontrol.presetformat identifier with aformatVersionfield, aligned with the live-scene file format. The redundantsourceKindhint was dropped (it is derived from the source path).
Removed
- Backward-compatible reading of the legacy
prop.presetpreset format; presets saved before this change must be re-saved.
[0.23.1] - 2026-06-21
Fixed
- The Studio no longer reports unsaved changes on every launch (which blocked quit with an unsaved-changes dialog when a project folder contained files). Persisting the
ExternalAssetManagerasset array directly let the deferred project-folder crawl populate it after the live-scene save baseline was captured; the in-use-only persistence shadow is restored so the crawl-built catalog stays out of the saved/dirty state.
[0.23.0] - 2026-06-21
Added
ExternalAssetManager: a single runtime catalog that loads and unloads props, avatars and set bundles uniformly (replacing the oldPropManager). Avatar selection, prop loading and set-bundle management all flow through it.- Runtime avatar props: a
.prop.lsbbundle can be attached to an avatar at runtime, following a bone/socket throughAvatarProp. Socket attachment is now robust to the avatar's rig and scale, and a prop's exposed-object parameters and id persist across unload/reload. - Prop presets: a loaded prop's tweaked parameters can be saved as a
.preset.json(SaveAsPreset) and re-loaded as a new prop, storing only the delta from the prop's defaults. - Project asset catalog with live-scene assets and bundle thumbnails; exported
.prop.lsb/.avatar.lsbbundles embed a thumbnail used by the remote app's asset cards. - Avatar VRM thumbnail served over
/api/avatar/image?id=, extracted from the VRM's embedded thumbnail without a UniVRM dependency. - Cross-scene remote control: additively loaded set bundles expose their objects to the single persistent host server. New stage marks (
StageMark/StageMarkRegistry/AvatarStageController) warp the avatar to named marks (plus a built-in Zero origin) from the remote app, and the saved active scene is restored on load. - Adding an asset now imports it into the project:
AddAssetcopies the picked file into a kind-specific subfolder of the open project folder (Avatars/Props/Sets, resolved per asset kind, colliding names get a(n)suffix) and registers the in-project copy. A picked file already inside the project folder is registered in place without copying. Only the single picked file is copied for now. - A project folder is the home for all saved data: on first launch
Documents/<brand>/<project>is created and opened, it is used as the default Save As location for live scenes, and "Open Save Folder" opens it.
Changed
- The remote-control host and its managers now stay alive across base-scene reloads instead of being torn down and recreated, so remote connections and manager state survive scene switches; the Studio prefabs and template were reworked around this persistent host.
VRCFTAvatarexpression now flows throughExpressionResolver, and converted avatars auto-gain hand-gesture expressions.- Received poses are now time-interpolated between the two nearest received 60fps frames during the variable-fps render update (
AvatarAnimationSystem.Lerp), so the avatar advances smoothly and SpringBones no longer shiver on held frames. - Breaking: Renamed the scene-bundle concept to "set" and the world concept to "stage", aligning the vocabulary with virtual-production terms (a stage holds sets, props and marks). Set bundles are now exported as
.set.lsb;SceneBundleAsset→SetBundleAsset,SceneBundleLoader/LoadedSceneBundle→SetBundleLoader/LoadedSetBundle,SceneBundleExporter→SetBundleExporter(menu "Export Set Bundle (.set.lsb)"),LiveStudioBundle.SceneExtension/IsSceneBundle→SetExtension/IsSetBundle, andWorldManager→StageManager(exposed functionsAddSet/RemoveSet/SetActiveSet, exposed propertysets). The legacy.scene.lsbextension is still accepted on input (loaded as aSetBundleAsset), no longer produced on export. Saved live scenes referencing the old@typenames (e.g.SceneBundleAsset/WorldManager) are not migrated and must be re-saved; such unresolved entries now deserialize away instead of leaving a null hole.StageManagercarries[MovedFrom("WorldManager")]so in-scene[SerializeReference]wiring migrates automatically. ExternalAssetManagernow persists its asset array directly to the live scene; the project-folder crawl rebuilds the disabled catalog on load, so the separate persistence shadow was removed.SavedPathsderives its base directory from the configured brand name and organizes saved data into per-project folders, replacing the singleScenesubfolder and the standaloneLiveStudioPathsInitializer(folder setup now lives inProjectManager).
Fixed
ResetCameranow derives its yaw/position offset from the capture-camera channel instead of the pose-driven avatar root, so repeated resets are stable; a configurable camera height (default 1.5 m) keeps the virtual camera at eye level rather than dropping to the floor.- Arms Post Rig is now added only for
VRM1Avatar. - Stage/scene visibility and active state are restored correctly on startup, and the persistent active entry settles at restore so the save baseline matches the post-restore state (no false unsaved-changes prompt at quit).
- A runtime-loaded avatar re-applies its animation parameter overrides one frame after its
PlayableGraphbuilds, instead of reverting to the controller defaults;VRMAvatarSetupSystemskips adding a duplicate facial controller when anIAvataralready exists.
[0.22.0] - 2026-06-14
Added
- Scene bundle import: a
.scene.lsbAssetBundle can now be loaded additively at runtime. NewRuntimeSceneManagerandSceneBundleLoadercomponents plus a sharedBundleBuildUtility, withLiveStudioBundledetecting the compound extension so.scene.lsb(scene) and.avatar.lsb(avatar) bundles share one pipeline. CaptureCameraControllerexposes achannelIndexproperty (withCAMERA_CHANNELINDEXlocalization), so aCaptureCameraTrackercan choose which capture-camera channel (0 or 1) it follows.HumanoidPoseDatanow carries a per-bone tracking presence (bonePresences, 0..1).AvatarBodyDriver's pose job blends the mocap pose over the avatar's animation per bone by this weight: a bone with presence 1 takes the mocap rotation fully, presence 0 leaves the animation flowing through, and values in between slerp between the two. This lets untracked body parts keep playing theirAnimatorControlleranimation while tracked parts follow the mocap.
Changed
ExternalAvatarSource's file selector now filters the compound.avatar.lsbextension (was.lsb) to distinguish avatar bundles from.scene.lsb; the legacy.lsavatarextension is still accepted.- Avatar frames now carry multiple capture-camera channels per frame (one
CameraDataper channel), so the worldA / worldB cameras are conveyed independently. VRM1AvatarandVRCFTAvatarnow drive body animation through aPlayableGraph(new sharedAvatarBodyDriver) instead of writing the humanoid bone transforms directly every frame. An optionalAnimatorControlleron the avatar is wrapped into the graph: its animation (e.g. an idle or range-of-motion clip) plays through while tracking is lost and is overwritten by the mocap pose while tracking. Shared tracking-state handling, mesh visibility, and the avatar-build boilerplate were extracted intoAvatarBodyDriverandAvatarBuildNotifier.BuildAndNotify.
Fixed
VRM1Avatar/VRCFTAvatarnow resolve theirAnimatorlazily inBuildAvatar, which can run beforeStartduring a synchronous.lsavatarload and previously left the animator null and logged an error.AvatarBodyDrivernow disablesapplyRootMotionon the wrappedAnimator, so a controller's root motion no longer sinks the whole avatar to the floor (VRCFT avatars).
[0.21.3] - 2026-06-12
Changed
- The exposed
Light's shadow toggle now enables soft shadows (LightShadows.Soft) instead of hard shadows, both in the property setter and on deserialize, for smoother shadow edges.
[0.21.2] - 2026-06-11
Added
ChildProcessHost.RequestCloseAndReleaseposts WM_CLOSE to a windowed child without a quit signal and releases it immediately, without waiting for exit.
Changed
- Quitting Studio no longer blocks waiting for child processes to exit. The Fusion process is signaled to quit and released immediately (
RequestStopAndRelease), and the Remote app is asked to close (WM_CLOSE) and released immediately (RequestCloseAndRelease), instead of each blocking up to 5 seconds onWaitForExit. - The Studio build output folder and exe name are now derived from the project folder name, dropping the dedicated VRC build entry point and the App VRC build profile path.
Fixed
SkyboxBackgroundnow logs an error instead of propagating a null shader when the Skybox/Cubemap shader is stripped from a player build (it runs duringExposedObjectContainer.Initializeand must not throw).
[0.21.1] - 2026-06-09
Added
ExternalAvatarSourcecan now import avatars from AssetBundle files (.lsavatar) in addition to VRM, chosen automatically by file extension.
Changed
- Renamed
VRMAvatarSourcetoExternalAvatarSource(the former exposed name is preserved for existing scenes). The avatar load APIRequestLoadVRMwas renamed toRequestLoadacrossIAvatarService,AvatarController, andAvatarService. - Generalized the model file field label and scoped its help text to VRM-only guidance.
[0.21.0] - 2026-06-08
Fixed
- Windows builds could leave a lingering background process after quitting, caused by a native shutdown hang related to Windows.Gaming.Input. A watchdog (
QuitTerminationGuard) now force-terminates the process if graceful shutdown stalls.
[0.20.12] - 2026-06-04
Added
- Eye gaze is now delegated to the VRM10
LookAtcomponent, so the avatar's eyes follow the configured look-at target instead of being driven directly.
[0.20.11] - 2026-06-03
Added
ChildProcessQuitSignal: signals a specific child process (keyed by its PID) to quit gracefully via a Windows named event, so a windowless child can run its own save-on-quit instead of being hard-killed.- "Assets/Lilium Live Studio/Remove Missing Scripts" context-menu command that removes missing scripts from the selected prefab assets.
Changed
- Stopping a windowless child process now signals it to quit and returns immediately without waiting for it to exit (
ChildProcessHost.RequestStopAndRelease).
Fixed
- The package Readme inspector buttons (language toggle, section actions, Install) are now clickable even when the package is consumed as an immutable git/registry dependency.
[0.20.10] - 2026-06-02
Added
WindowsFirewallhelper that registers inbound UDP allow rules vianetsh. Rules are keyed by the listening port (not the program path), so a single rule survives the tool being relocated, reinstalled, or shipped from a different package-cache path. Elevation is requested only when the rule is missing.
[0.20.9] - 2026-06-02
Changed
- RemoteControl handlers are now registered per-instance with their routes declared in the constructor.
[0.19.1] - 2026-04-29
Added
- Initial release. Split out from
jp.lilium.virgo.studioto separate VTuber-app generic infrastructure (Camera / Lighting / Scene / Build / RemoteControl base / shared Localization) from VirgoMotion-specific motion reception and avatar control.