OS Name: Microsoft Windows 11 Pro
OS Version: 10.0.22621 N/A Build 22621
Unity: 6000.0.48f1
Cubism SDK: 5-r.3
I dynamically instantiate GameObject with CubismModel at runtime and the .moc3 file is outside of Unity Editor. I have managed to make it play motions with a simple MotionController.PlayAnimation() and it works on the Unity Editor. When I tried the build version, it somehow won't play the motions.
I've tried to reimport entire project, create a clean project but still didn't work. What I know is, If i use the generated prefab, instantiate it on runtime then play motions, it's work on the build.
Here's my main code's:
CharacterViewer.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Cysharp.Threading.Tasks;
using Kiraio.Azure.Utils;
using Live2D.Cubism.Framework;
using Live2D.Cubism.Framework.Expression;
using Live2D.Cubism.Framework.Json;
using Live2D.Cubism.Framework.Motion;
using Live2D.Cubism.Framework.MotionFade;
using Live2D.Cubism.Framework.Pose;
using Live2D.Cubism.Framework.Raycasting;
using Live2D.Cubism.Rendering.Masking;
using UnityEngine;
namespace Kiraio.Azure.Components
{
[AddComponentMenu("Azure/Components/Character Viewer")]
public class CharacterViewer : CubismViewerBase
{
// TouchBody = Upper Body, TouchHead = Head, TouchSpecial = Bust
private readonly string[] _touchAreas = { "TouchBody", "TouchHead", "TouchSpecial" };
private void OnDestroy()
{
foreach (var clip in Animations.Select(animationClip => animationClip.Value))
clip.events = Array.Empty<AnimationEvent>();
}
public async UniTask Initialize()
{
// Load the *.model3.json file
var baseModelName = Path.GetFileName(ModelJsonFile).Replace(".model3.json", "");
ModelJson = CubismModel3Json.LoadAtPath(
StorageHelper.NormalizePath(ModelJsonFile),
LoadAssetAtPath
);
ModelJson.FileReferences.DisplayInfo = $"{baseModelName}.cdi3.json";
ModelJson.FileReferences.Moc = $"{baseModelName}.moc3";
// ModelJson.FileReferences.Expressions = $"{baseModelName}.exp3.json";
// ModelJson.FileReferences.Motions = $"{baseModelName}.motion3.json";
ModelJson.FileReferences.Physics = $"{baseModelName}.physics3.json";
// PhysicsJson =
// CubismPhysics3Json.LoadFrom(await WebRequestHelper.GetTextDataAsync(ModelJson.FileReferences.Physics));
PoseJson = CubismPose3Json.LoadFrom(await WebRequestHelper.GetTextDataAsync(ModelJson.FileReferences.Pose));
ModelJson.FileReferences.Pose = $"{baseModelName}.pose3.json";
ModelJson.HitAreas = _touchAreas
.Select(area => new CubismModel3Json.SerializableHitArea { Name = area, Id = area }).ToArray();
Model = ModelJson.ToModel();
Model.transform.parent = transform;
Model.gameObject.SetActive(false); // Disable the model gameObject to stop the components being initialized
gameObject.name = Model.name;
//! Physics Rig is somehow became null after the initialization, disable the Physics for now
// Model.gameObject.AddComponent<CubismPhysicsController>();
Raycaster = Model.gameObject.AddComponent<CubismRaycaster>();
VoiceSource = Model.gameObject.AddComponent<AudioSource>();
Animator = Model.gameObject.GetComponent<Animator>();
// Animator.runtimeAnimatorController = AnimatorController;
// Add CubismMotionController after assigning CubismFadeMotionList
MotionController = Model.gameObject.AddComponent<CubismMotionController>();
FadeController = Model.gameObject.GetComponent<CubismFadeController>();
// MotionController.enabled = false;
MotionController.LayerCount = 3;
// Workaround for the "InstanceId" AnimationEvent error
Model.gameObject
.AddComponent<
FixAnimationEvent>(); // Fix AnimationEvent errors by bypassing the "InstanceId" AnimationEvent with empty callback
// Create Mask Texture
MaskController = Model.gameObject.GetComponent<CubismMaskController>();
var modelMaskTexture =
ScriptableObject.CreateInstance<CubismMaskTexture>();
modelMaskTexture.name = $"{Model.name}MaskTexture";
MaskController.MaskTexture = modelMaskTexture;
ExpressionController = Model.gameObject.AddComponent<CubismExpressionController>();
var expressionList = ScriptableObject.CreateInstance<CubismExpressionList>();
expressionList.name =
$"{Path.GetFileNameWithoutExtension(ModelJsonFile).Split(".")[0]}";
var expressionDataList = new List<CubismExpressionData>();
foreach (var expression in ModelJson
.FileReferences
.Expressions)
{
var expressionName = expression.File.Split('/', '.')[1];
var expressionJsonPath = Path.Combine(
Path.GetDirectoryName(ModelJsonFile) ?? string.Empty,
expression.File
);
var expressionJson = CubismExp3Json.LoadFrom(
await WebRequestHelper.GetTextDataAsync(expressionJsonPath)
);
// Create ExpressionData
var expressionData = CubismExpressionData.CreateInstance(expressionJson);
expressionData.name = $"{expressionName}.exp";
expressionDataList.Add(expressionData);
}
expressionList.CubismExpressionObjects = expressionDataList.ToArray();
ExpressionController.ExpressionsList = expressionList;
// Create Fade Motion List
// Ref: https://docs.live2d.com/en/cubism-sdk-manual/motionfade/
var fadeMotionList = ScriptableObject.CreateInstance<CubismFadeMotionList>();
fadeMotionList.name =
$"{Path.GetFileNameWithoutExtension(ModelJsonFile).Split(".")[0]}";
var motionsData = new Dictionary<int, CubismFadeMotionData>();
foreach (
var motion in ModelJson
.FileReferences
.Motions
.Motions
)
{
var motionName = motion[0].File.Split('/', '.')[1];
var motionJsonPath = Path.Combine(
Path.GetDirectoryName(ModelJsonFile) ?? string.Empty,
motion[0].File
);
var motionJson = CubismMotion3Json.LoadFrom(
await WebRequestHelper.GetTextDataAsync(motionJsonPath)
);
motionJson.Meta.FadeInTime = 1f;
motionJson.Meta.FadeOutTime = 1f;
// Create FadeMotionData
var fadeMotion = CubismFadeMotionData.CreateInstance(
motionJson,
Path.GetFileName(motionJsonPath),
motionJson.Meta.Duration,
false,
false,
ModelJson
);
fadeMotion.name = $"{motionName}.fade";
fadeMotion.FadeInTime = 1f;
fadeMotion.FadeOutTime = 1f;
for (var i = 0; i < fadeMotion.ParameterFadeInTimes.Length; i++)
{
fadeMotion.ParameterFadeInTimes[i] = 1f;
fadeMotion.ParameterFadeOutTimes[i] = 1f;
}
// Create AnimationClip
var animationClip = motionJson.ToAnimationClip(false, false, false, PoseJson);
animationClip.name = motionName;
animationClip.legacy = false;
// NOTE: Not clearing animation events first causing fade motion list errors.
// Create "InstanceId" AnimationEvent at the start
// var animationClipEvents = animationClip.events;
animationClip.events = Array.Empty<AnimationEvent>(); // Clear any events
// for (var i = 0; i < animationClipEvents.Length; i++)
// {
var newEvent = new AnimationEvent
{
functionName = "InstanceId",
time = 0,
intParameter = animationClip.GetInstanceID()
};
animationClip.AddEvent(newEvent);
// }
motionsData.Add(animationClip.GetInstanceID(), fadeMotion);
Animations.Add(motionName, animationClip);
}
// Assign the CubismFadeMotionList to the CubismFadeController
fadeMotionList.MotionInstanceIds = motionsData.Keys.ToArray();
fadeMotionList.CubismFadeMotionObjects = motionsData.Values.ToArray();
FadeController.CubismFadeMotionList = fadeMotionList;
// Add touch area
_touchAreas
.Select(area => transform.Find($"{transform.name}/Drawables/{area}").gameObject)
.Where(target => target != null)
.ToList()
.ForEach(target =>
{
var touchArea = target.AddComponent<Touch>();
touchArea.CharacterViewer = this;
switch (touchArea.name)
{
case "TouchBody":
touchArea.PlayData.Add("touch_body", "touch");
break;
case "TouchHead":
touchArea.PlayData.Add("touch_head", "headtouch");
break;
case "TouchSpecial":
touchArea.PlayData.Add("touch_special", "touch2");
break;
default:
Debug.LogError("No touch data.");
break;
}
});
// Fetch voices audio
if (Directory.Exists(VoicesDirectory))
{
var directoryInfo = new DirectoryInfo(VoicesDirectory);
var files = directoryInfo.GetFiles();
foreach (var file in files)
{
try
{
var clip = await WebRequestHelper.GetAudioClip(file.FullName);
if (clip != null)
Voices.Add(Path.GetFileNameWithoutExtension(file.Name), clip);
}
catch (Exception ex)
{
Debug.LogWarning($"Can't load audio: {ex.Message}");
}
}
}
// Enable the model gameObject to initialize the components
Model.gameObject.SetActive(true);
PlayInitialMotions();
}
private void PlayInitialMotions()
{
// Play the effect motion on layer 2
PlayMotion("effect", true, 2, 1);
// Play the login motion on layer 0
PlayMotion("login", onComplete: OnLoginComplete);
// PlayVoice("login");
}
private void OnLoginComplete(int instanceId)
{
// Play the idle motion on layer 0 after login completes
PlayMotion("idle", true, priority: 1);
// Unregister the completion handler
MotionController.AnimationEndHandler -= OnLoginComplete;
// Allow interactions
AllowInteraction = true;
}
}
}
CubismViewerBase.cs:
using System;
using System.Reflection;
using Gilzoide.SerializableCollections;
using Kiraio.Azure.Core;
using Kiraio.Azure.Utils;
using Live2D.Cubism.Core;
// using Live2D.Cubism.Framework;
using Live2D.Cubism.Framework.Expression;
using Live2D.Cubism.Framework.Json;
using Live2D.Cubism.Framework.Motion;
using Live2D.Cubism.Framework.MotionFade;
// using Live2D.Cubism.Framework.Pose;
using Live2D.Cubism.Framework.Raycasting;
// using Live2D.Cubism.Rendering;
using Live2D.Cubism.Rendering.Masking;
using UnityEngine;
namespace Kiraio.Azure.Components
{
public class CubismViewerBase : MonoBehaviour
{
public string ModelJsonFile { get; set; }
public string VoicesDirectory { get; set; }
protected CubismModel3Json ModelJson { get; set; }
// protected CubismPhysics3Json PhysicsJson { get; set; }
protected CubismPose3Json PoseJson { get; set; }
protected CubismModel Model { get; set; }
public CubismRaycaster Raycaster { get; set; }
public CubismMotionController MotionController { get; set; }
// protected CubismPoseController PoseController { get; set; }
protected CubismExpressionController ExpressionController { get; set; }
protected CubismMaskController MaskController { get; set; }
protected CubismFadeController FadeController { get; set; }
// protected CubismUpdateController UpdateController { get; set; }
// protected CubismRenderController RenderController { get; set; }
public Animator Animator { get; set; }
public RuntimeAnimatorController AnimatorController { get; set; }
public AudioSource VoiceSource { get; set; }
// public SerializableDictionary<string, AudioClip> AnimationsVoices { get; set; } = new();
public SerializableDictionary<string, AudioClip> Voices { get; set; } = new();
public SerializableDictionary<string, AnimationClip> Animations { get; set; } = new();
public MainControl MainControl { get; set; }
public InputManager InputManager { get; set; }
public bool AllowInteraction { get; set; }
protected virtual void Awake()
{
MainControl = FindObjectsByType<MainControl>(FindObjectsSortMode.None)[0];
InputManager = FindObjectsByType<InputManager>(FindObjectsSortMode.None)[0];
}
/// <summary>
/// Loads asset.
/// </summary>
/// <param name="absolutePath">Path to asset.</param>
/// <returns>The asset on success; <see langword="null" /> otherwise.</returns>
public static T LoadAsset<T>(string absolutePath) where T : class
{
return LoadAssetAtPath(typeof(T), absolutePath) as T;
}
/// <summary>
/// A helper method provided by the Live2D itself to load assets from the path.
/// </summary>
/// <param name="assetType"></param>
/// <param name="absolutePath"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
protected static object LoadAssetAtPath(Type assetType, string absolutePath)
{
if (assetType == typeof(byte[]))
return WebRequestHelper.GetBinaryData(absolutePath);
if (assetType == typeof(string))
return WebRequestHelper.GetTextData(absolutePath);
if (assetType != typeof(Texture2D)) throw new NotSupportedException();
var texture = new Texture2D(1, 1);
texture.LoadImage(WebRequestHelper.GetBinaryData(absolutePath));
return texture;
}
/// <summary>
/// A helper method to play the motion.
/// </summary>
/// <param name="motionName">The animation name.</param>
/// <param name="isLoop">Loop the animation?</param>
/// <param name="layerIndex">Which layer to play the animation?</param>
/// <param name="priority">How important is the animation? Scale from 0 ~ 3.</param>
/// <param name="onComplete">The callback to invoke when the animation completes.</param>
public void PlayMotion(
string motionName,
bool isLoop = false,
int layerIndex = 0,
int priority = CubismMotionPriority.PriorityNormal,
Action<int> onComplete = null
)
{
if (!Animations.TryGetValue(motionName, out var animationClip))
{
Debug.LogWarning($"Motion {motionName} not found.");
return;
}
// Stop the current motion on the target layer (if not looped)
// if (MotionController.IsPlayingAnimation(layerIndex) && !isLoop)
// {
// MotionController.StopAnimation(0, layerIndex);
// }
// Play the motion with the specified priority
MotionController.PlayAnimation(
animationClip,
layerIndex,
priority,
isLoop
);
// Register completion handler
if (onComplete != null) MotionController.AnimationEndHandler += onComplete;
}
public void PlayVoice(string voiceName)
{
if (!string.IsNullOrEmpty(voiceName))
{
VoiceSource.clip = Voices[voiceName];
VoiceSource.Play();
}
else
{
Debug.LogWarning("No voice clip with the name provided.");
}
}
/// <summary>
/// Resets the motion priorities for all layers.
/// </summary>
/// <param name="motionController">The CubismMotionController instance.</param>
public static void ResetMotionPriorities(CubismMotionController motionController)
{
if (motionController == null)
{
Debug.LogWarning("MotionController is null. Cannot reset priorities.");
return;
}
// Access the private _motionPriorities field using reflection
var motionPrioritiesField = typeof(CubismMotionController).GetField(
"_motionPriorities",
BindingFlags.NonPublic | BindingFlags.Instance
);
if (motionPrioritiesField == null)
{
Debug.LogError("Failed to access _motionPriorities field. Has the Cubism SDK changed?");
return;
}
// Get the current _motionPriorities array
var motionPriorities = (int[])motionPrioritiesField.GetValue(motionController);
if (motionPriorities == null || motionPriorities.Length == 0)
{
Debug.LogWarning("_motionPriorities array is null or empty. Cannot reset priorities.");
return;
}
// Reset all priorities to 0 (PriorityNone)
for (var i = 0; i < motionPriorities.Length; i++) motionPriorities[i] = CubismMotionPriority.PriorityNone;
// Update the _motionPriorities field
motionPrioritiesField.SetValue(motionController, motionPriorities);
}
}
}
Comments