Skip to content

Commit

Permalink
Merge pull request #8 from Live2D/develop
Browse files Browse the repository at this point in the history
Update to Cubism 5 SDK MotionSync Plugin for Unity R2
  • Loading branch information
itoh-at-live2d-com authored Nov 28, 2024
2 parents c3bbb0f + 3c5b2b4 commit e71561c
Show file tree
Hide file tree
Showing 39 changed files with 9,299 additions and 8,159 deletions.
21 changes: 21 additions & 0 deletions Assets/Live2D/CubismMotionSyncPlugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).


## [5-r.2] - 2024-11-28

### Added

* Add a feature to play the associated audio when playing back a motion.
* With this feature, `.motionSyncLink.asset` and `.motionSyncLinkList.asset` is generated based on the information specified in `.model3.json` during the import process.

### Changed

* Move `Microphone` sample to `Assets/Live2D/CubismMotionSyncPlugin/Samples/Microphone`.
* Move `Microphone` sample to a separate assembly.

### Fixed

* Fix an issue where a reference error could occur immediately after `CubismMotionSyncController` was enabled.
* Fix an issue where `CubismMotionSyncEngine_CRI.DisposeEngine()` could be called at an unintended time. by [@ppcuni](https://github.com/Live2D/CubismUnityMotionSyncComponents/pull/6)
* Fix an issue where a null reference occurs if there are parameters listed in `.motionsync3.json` that do not exist in the model.
* Fix an issue where where models names in the dropdown menus for `MotionSync` scene and `MotionSyncForWebGL` scene were incorrect.


## [5-r.1] - 2024-05-30

### Added
Expand Down Expand Up @@ -76,6 +96,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
* New released!


[5-r.2]: https://github.com/Live2D/CubismUnityMotionSyncComponents/compare/5-r.1...5-r.2
[5-r.1]: https://github.com/Live2D/CubismUnityMotionSyncComponents/compare/5-r.1-beta.3...5-r.1
[5-r.1-beta.3]: https://github.com/Live2D/CubismUnityMotionSyncComponents/compare/5-r.1-beta.2.1...5-r.1-beta.3
[5-r.1-beta.2.1]: https://github.com/Live2D/CubismUnityMotionSyncComponents/compare/5-r.1-beta.2...5-r.1-beta.2.1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private static void RegisterImporter()
#endregion

/// <summary>
/// Set expression list.
/// Get motionsync .
/// </summary>
/// <param name="importer">Event source.</param>
/// <param name="model">Imported model.</param>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
/**
* Copyright(c) Live2D Inc. All rights reserved.
*
* Use of this source code is governed by the Live2D Open Software license
* that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html.
*/


using System;
using System.IO;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Live2D.Cubism.Core;
using Live2D.Cubism.Editor;
using Live2D.Cubism.Editor.Importers;
using Live2D.Cubism.Framework.Json;
using Live2D.CubismMotionSyncPlugin.Framework.Motion;

namespace Live2D.CubismMotionSyncPlugin.Editor.Importers
{
/// <summary>
/// Retrieve information associated with motion from .model3.json.
/// </summary>
[Serializable]
internal static class CubismMotionSyncSettingIdForMotionImporter
{
/// <summary>
/// Motion data.
/// </summary>
[Serializable]
public struct SerializableMotions
{
/// <summary>
/// Motion groups.
/// </summary>
[SerializeField]
public SerializableMotion[][] Motions;
}

/// <summary>
/// Motions references data.
/// </summary>
[Serializable]
public struct SerializableMotion
{
/// <summary>
///The motion sync setting ID.
/// </summary>
[SerializeField]
public string MotionSync;

/// <summary>
/// Relative path to the sound file.
/// </summary>
[SerializeField]
public string Sound;
}

/// <summary>
/// FileReference keys from .model3.json.
/// </summary>
[Serializable]
private struct SerializableFileReferences
{
/// <summary>
/// Motions data.
/// </summary>
[SerializeField]
public SerializableMotions Motions;
}

/// <summary>
/// JSON key from .model.json.
/// </summary>
[Serializable]
private struct SerializableModel3Json
{
/// <summary>
/// File references data.
/// </summary>
[SerializeField]
public SerializableFileReferences FileReferences;
}

#region Unity Event Handling

/// <summary>
/// Registers importer.
/// </summary>
[InitializeOnLoadMethod]
private static void RegisterImporter()
{
CubismImporter.OnDidImportModel += OnModelImport;
}

#endregion

/// <summary>
/// After importing model.
/// </summary>
/// <param name="importer">Event source.</param>
/// <param name="model">Imported model.</param>
private static void OnModelImport(CubismModel3JsonImporter importer, CubismModel model)
{
// Load Json asset.
var modelJsonAsset = BuiltinLoadAssetAtPath(importer.AssetPath);

if (string.IsNullOrEmpty(modelJsonAsset))
{
// return silently...
return;
}

// Deserialize Json.
var modelJson = JsonUtility.FromJson<SerializableModel3Json>(modelJsonAsset);
var model3JsonDirectoryPath = Path.GetDirectoryName(importer.AssetPath)?.Replace("\\", "/");

// Get motion file settings from .model3.json data.
var motionGroupNames = importer.Model3Json.FileReferences.Motions.GroupNames;
var motionsFromImporter = importer.Model3Json.FileReferences.Motions.Motions;

if (string.IsNullOrEmpty(model3JsonDirectoryPath) || motionGroupNames == null || motionsFromImporter == null)
{
// return silently...
return;
}

// Create a temporary list to store motionSyncLinkData
var motionSyncLinkDataTempList = new List<CubismMotionSyncLinkData>();

#region Create .motionSyncLink.asset files
for (var motionGroupIndex = 0; motionGroupIndex < motionGroupNames.Length; motionGroupIndex++)
{
// Set motion references.
var value = CubismJsonParser.ParseFromString(modelJsonAsset);

// Return early if there is no references.
if (!value.Get("FileReferences").GetMap(null).ContainsKey("Motions"))
{
return;
}

modelJson.FileReferences.Motions.Motions = new SerializableMotion[motionGroupNames.Length][];
var motions = modelJson.FileReferences.Motions.Motions;
var motionGroupArrayCount = importer.Model3Json.FileReferences.Motions.Motions[motionGroupIndex].Length;

// Get motion data from Json.
var motionGroup = value.Get("FileReferences").Get("Motions").Get(motionGroupNames[motionGroupIndex]);
var motionCount = motionGroup.GetVector(null).ToArray().Length;
motions[motionGroupIndex] = new SerializableMotion[motionCount];

for (var motionsIndex = 0; motionsIndex < motionGroupArrayCount; motionsIndex++)
{
// Get sound file path and motion sync setting ID.
var soundRelativePath = string.IsNullOrEmpty(motionsFromImporter[motionGroupIndex][motionsIndex].Sound)
? string.Empty
: motionsFromImporter[motionGroupIndex][motionsIndex].Sound;

// Get motion sync setting ID from Json.
if (motionGroup.Get(motionsIndex).GetMap(null).ContainsKey("MotionSync"))
{
motions[motionGroupIndex][motionsIndex].MotionSync = motionGroup.Get(motionsIndex).Get("MotionSync").toString();
}

// Set motion sync setting ID.
var motionSyncSettingId = string.IsNullOrEmpty(motions[motionGroupIndex][motionsIndex].MotionSync)
? string.Empty
: motions[motionGroupIndex][motionsIndex].MotionSync;

// Skip create .motionSyncLink.asset file if sound file path or motion sync setting ID is empty.
if (string.IsNullOrEmpty(soundRelativePath) || string.IsNullOrEmpty(motionSyncSettingId))
{
motionSyncLinkDataTempList.Add(null);
continue;
}

// Get AudioClip asset from sound file path.
var soundFilePath = string.Format("{0}/{1}", model3JsonDirectoryPath, soundRelativePath);
var audioClip = AssetDatabase.LoadAssetAtPath<AudioClip>(soundFilePath);

if (audioClip == null)
{
Debug.LogWarning("AudioClip not found at path: " + soundFilePath);
}

var shouldImportAsOriginalWorkflow = CubismUnityEditorMenu.ShouldImportAsOriginalWorkflow;
var shouldClearAnimationCurves = CubismUnityEditorMenu.ShouldClearAnimationCurves;

var motionPath = string.Format("{0}/{1}", model3JsonDirectoryPath, motionsFromImporter[motionGroupIndex][motionsIndex].File);
var jsonString = string.IsNullOrEmpty(motionPath)
? null
: File.ReadAllText(motionPath);

if (jsonString == null)
{
continue;
}

var motion3Json = CubismMotion3Json.LoadFrom(jsonString);

var animationClipPath = string.Format("{0}/{1}", model3JsonDirectoryPath, motionsFromImporter[motionGroupIndex][motionsIndex].File.Replace(".motion3.json", ".anim"));
animationClipPath = animationClipPath.Replace("\\", "/");

var animationName = Path.GetFileNameWithoutExtension(motionsFromImporter[motionGroupIndex][motionsIndex].File.Replace(".motion3.json", ".anim"));
var assetList = CubismCreatedAssetList.GetInstance();
var assetListIndex = assetList.AssetPaths.Contains(animationClipPath)
? assetList.AssetPaths.IndexOf(animationClipPath)
: -1;

var oldAnimationClip = (shouldImportAsOriginalWorkflow)
? (assetListIndex >= 0)
? (AnimationClip)assetList.Assets[assetListIndex]
: AssetDatabase.LoadAssetAtPath<AnimationClip>(animationClipPath)
: null;

var newAnimationClip = (oldAnimationClip == null)
? motion3Json.ToAnimationClip(shouldImportAsOriginalWorkflow, shouldClearAnimationCurves)
: motion3Json.ToAnimationClip(oldAnimationClip, shouldImportAsOriginalWorkflow, shouldClearAnimationCurves);
newAnimationClip.name = animationName;

if (assetListIndex < 0)
{
// Create animation clip.
if (oldAnimationClip == null)
{
AssetDatabase.CreateAsset(newAnimationClip, animationClipPath);
}

assetList.Assets.Add(newAnimationClip);
assetList.AssetPaths.Add(animationClipPath);
assetList.IsImporterDirties.Add(false);
}
// Update animation clip.
else
{
EditorUtility.CopySerialized(newAnimationClip, oldAnimationClip);
EditorUtility.SetDirty(oldAnimationClip);
assetList.Assets[assetListIndex] = oldAnimationClip;
}

var motionInstanceId = -1;
// Add animation event
{
motionInstanceId = newAnimationClip.GetInstanceID();

var sourceAnimationEvents = AnimationUtility.GetAnimationEvents(newAnimationClip);
var index = -1;

for (var sourceAnimationEventIndex = 0; sourceAnimationEventIndex < sourceAnimationEvents.Length; ++sourceAnimationEventIndex)
{
if (sourceAnimationEvents[sourceAnimationEventIndex].functionName != "InstanceId")
{
continue;
}

index = sourceAnimationEventIndex;
break;
}

if (index == -1)
{
index = sourceAnimationEvents.Length;
Array.Resize(ref sourceAnimationEvents, sourceAnimationEvents.Length + 1);
sourceAnimationEvents[sourceAnimationEvents.Length - 1] = new AnimationEvent();
}

sourceAnimationEvents[index].time = 0;
sourceAnimationEvents[index].functionName = "InstanceId";
sourceAnimationEvents[index].intParameter = motionInstanceId;
sourceAnimationEvents[index].messageOptions = SendMessageOptions.DontRequireReceiver;

AnimationUtility.SetAnimationEvents(newAnimationClip, sourceAnimationEvents);
}

// Create .motionSyncLink.asset file.
var motionSyncLinkData = CubismMotionSyncLinkData.CreateInstance(motionSyncSettingId, audioClip, motionsFromImporter[motionGroupIndex][motionsIndex].File, motionInstanceId);

// Regist motion sync link data.
motionSyncLinkDataTempList.Add(motionSyncLinkData);

var motionSyncLinkDataPath = motionPath.Replace(".motion3.json", ".motionSyncLink.asset");
if (!motionSyncLinkData)
{
Debug.LogError($"Failed to create {motionSyncLinkDataPath}.");
continue;
}

AssetDatabase.CreateAsset(motionSyncLinkData, motionSyncLinkDataPath);

// Reflecting changes.
EditorUtility.SetDirty(motionSyncLinkData);
AssetDatabase.Refresh();
}
}
#endregion

// Create .motionSyncLinkList.asset.
var motionSyncLinkList = ScriptableObject.CreateInstance<CubismMotionSyncLinkList>();

if (!motionSyncLinkList)
{
Debug.LogError("Failed to create CubismMotionSyncLinkList.");
return;
}

motionSyncLinkList.CubismMotionSyncLinkObjects = new CubismMotionSyncLinkData[motionSyncLinkDataTempList.Count];

// Copy motion sync link data.
Array.Copy(motionSyncLinkDataTempList.ToArray(), motionSyncLinkList.CubismMotionSyncLinkObjects, motionSyncLinkDataTempList.Count);

AssetDatabase.CreateAsset(motionSyncLinkList, string.Format("{0}/{1}", model3JsonDirectoryPath, model.name + ".motionSyncLinkList.asset"));

// Reflecting changes.
EditorUtility.SetDirty(motionSyncLinkList);
AssetDatabase.Refresh();
}

/// <summary>
/// Builtin method for loading assets.
/// </summary>
/// <param name="assetPath">Path to asset.</param>
/// <returns>The asset on success; <see langword="null"/> otherwise.</returns>
private static string BuiltinLoadAssetAtPath(string assetPath)
{
#if UNITY_EDITOR
return AssetDatabase.LoadAssetAtPath<TextAsset>(assetPath).ToString();
#else
var textAsset = Resources.Load(assetPath, typeof(TextAsset)) as TextAsset;

return (textAsset != null)
? textAsset.text
: null;
#endif
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e71561c

Please sign in to comment.