/** * 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 Live2D.Cubism.Core; using System; using System.IO; using Live2D.Cubism.Framework.MouthMovement; using Live2D.Cubism.Framework.Physics; using Live2D.Cubism.Framework.UserData; using Live2D.Cubism.Framework.Pose; using Live2D.Cubism.Framework.Expression; using Live2D.Cubism.Framework.MotionFade; using Live2D.Cubism.Framework.Raycasting; using Live2D.Cubism.Rendering; using Live2D.Cubism.Rendering.Masking; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; namespace Live2D.Cubism.Framework.Json { /// /// Exposes moc3.json asset data. /// [Serializable] // ReSharper disable once ClassCannotBeInstantiated public sealed class CubismModel3Json { #region Delegates /// /// Handles the loading of assets. /// /// The asset type to load. /// The path to the asset. /// public delegate object LoadAssetAtPathHandler(Type assetType, string assetPath); /// /// Picks a for a . /// /// Event source. /// Drawable to pick for. /// Picked material. public delegate Material MaterialPicker(CubismModel3Json sender, CubismDrawable drawable); /// /// Picks a for a . /// /// Event source. /// Drawable to pick for. /// Picked texture. public delegate Texture2D TexturePicker(CubismModel3Json sender, CubismDrawable drawable); #endregion #region Load Methods /// /// Loads a model.json asset. /// /// The path to the asset. /// The on success; otherwise. public static CubismModel3Json LoadAtPath(string assetPath) { // Use default asset load handler. return LoadAtPath(assetPath, BuiltinLoadAssetAtPath); } /// /// Loads a model.json asset. /// /// The path to the asset. /// Handler for loading assets. /// The on success; otherwise. public static CubismModel3Json LoadAtPath(string assetPath, LoadAssetAtPathHandler loadAssetAtPath) { // Load Json asset. var modelJsonAsset = loadAssetAtPath(typeof(string), assetPath) as string; // Return early in case Json asset wasn't loaded. if (modelJsonAsset == null) { return null; } // Deserialize Json. var modelJson = JsonUtility.FromJson(modelJsonAsset); // Finalize deserialization. modelJson.AssetPath = assetPath; modelJson.LoadAssetAtPath = loadAssetAtPath; // 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; } var motionGroupNames = value.Get("FileReferences").Get("Motions").KeySet().ToArray(); modelJson.FileReferences.Motions.GroupNames = motionGroupNames; var motionGroupNamesCount = motionGroupNames.Length; modelJson.FileReferences.Motions.Motions = new SerializableMotion[motionGroupNamesCount][]; for (var i = 0; i < motionGroupNamesCount; i++) { var motionGroup = value.Get("FileReferences").Get("Motions").Get(motionGroupNames[i]); var motionCount = motionGroup.GetVector(null).ToArray().Length; modelJson.FileReferences.Motions.Motions[i] = new SerializableMotion[motionCount]; for (var j = 0; j < motionCount; j++) { if (motionGroup.Get(j).GetMap(null).ContainsKey("File")) { modelJson.FileReferences.Motions.Motions[i][j].File = motionGroup.Get(j).Get("File").toString(); } if (motionGroup.Get(j).GetMap(null).ContainsKey("Sound")) { modelJson.FileReferences.Motions.Motions[i][j].Sound = motionGroup.Get(j).Get("Sound").toString(); } if (motionGroup.Get(j).GetMap(null).ContainsKey("FadeInTime")) { modelJson.FileReferences.Motions.Motions[i][j].FadeInTime = motionGroup.Get(j).Get("FadeInTime").ToFloat(); } if (motionGroup.Get(j).GetMap(null).ContainsKey("FadeOutTime")) { modelJson.FileReferences.Motions.Motions[i][j].FadeOutTime = motionGroup.Get(j).Get("FadeOutTime").ToFloat(); } } } return modelJson; } #endregion /// /// Path to . /// public string AssetPath { get; private set; } /// /// Method for loading assets. /// private LoadAssetAtPathHandler LoadAssetAtPath { get; set; } #region Json Data /// /// The motion3.json format version. /// [SerializeField] public int Version; /// /// The file references. /// [SerializeField] public SerializableFileReferences FileReferences; /// /// Groups. /// [SerializeField] public SerializableGroup[] Groups; /// /// Hit areas. /// [SerializeField] public SerializableHitArea[] HitAreas; #endregion /// /// The contents of the referenced moc3 asset. /// /// /// The contents isn't cached internally. /// public byte[] Moc3 { get { return LoadReferencedAsset(FileReferences.Moc); } } /// /// backing field. /// [NonSerialized] private CubismPose3Json _pose3Json; /// /// The contents of pose3.json asset. /// public CubismPose3Json Pose3Json { get { if(_pose3Json != null) { return _pose3Json; } var jsonString = string.IsNullOrEmpty(FileReferences.Pose) ? null : LoadReferencedAsset(FileReferences.Pose); _pose3Json = CubismPose3Json.LoadFrom(jsonString); return _pose3Json; } } /// /// backing field. /// [NonSerialized] private CubismExp3Json[] _expression3Jsons; /// /// The referenced expression assets. /// /// /// The references aren't cached internally. /// public CubismExp3Json[] Expression3Jsons { get { // Fail silently... if(FileReferences.Expressions == null) { return null; } // Load expression only if necessary. if (_expression3Jsons == null) { _expression3Jsons = new CubismExp3Json[FileReferences.Expressions.Length]; for (var i = 0; i < _expression3Jsons.Length; ++i) { var expressionJson = (string.IsNullOrEmpty(FileReferences.Expressions[i].File)) ? null : LoadReferencedAsset(FileReferences.Expressions[i].File); _expression3Jsons[i] = CubismExp3Json.LoadFrom(expressionJson); } } return _expression3Jsons; } } /// /// The contents of physics3.json asset. /// public string Physics3Json { get { return string.IsNullOrEmpty(FileReferences.Physics) ? null : LoadReferencedAsset(FileReferences.Physics); } } public string UserData3Json { get { return string.IsNullOrEmpty(FileReferences.UserData) ? null : LoadReferencedAsset(FileReferences.UserData); } } /// /// The contents of cdi3.json asset. /// public string DisplayInfo3Json { get { return string.IsNullOrEmpty(FileReferences.DisplayInfo) ? null : LoadReferencedAsset(FileReferences.DisplayInfo); } } /// /// backing field. /// [NonSerialized] private Texture2D[] _textures; /// /// The referenced texture assets. /// /// /// The references aren't cached internally. /// public Texture2D[] Textures { get { // Load textures only if necessary. if (_textures == null) { _textures = new Texture2D[FileReferences.Textures.Length]; for (var i = 0; i < _textures.Length; ++i) { _textures[i] = LoadReferencedAsset(FileReferences.Textures[i]); } } return _textures; } } #region Constructors /// /// Makes construction only possible through factories. /// private CubismModel3Json() { } #endregion /// /// Instantiates a model source and a model with the default texture set. /// /// Should import as original workflow. /// The instantiated model on success; otherwise. public CubismModel ToModel(bool shouldImportAsOriginalWorkflow = false) { return ToModel(CubismBuiltinPickers.MaterialPicker, CubismBuiltinPickers.TexturePicker, shouldImportAsOriginalWorkflow); } /// /// Instantiates a model source and a model. /// /// The material mapper to use. /// The texture mapper to use. /// Should import as original workflow. /// The instantiated model on success; otherwise. public CubismModel ToModel(MaterialPicker pickMaterial, TexturePicker pickTexture, bool shouldImportAsOriginalWorkflow = false) { // Initialize model source and instantiate it. var mocAsBytes = Moc3; if (mocAsBytes == null) { return null; } var moc = CubismMoc.CreateFrom(mocAsBytes); var model = CubismModel.InstantiateFrom(moc); model.name = Path.GetFileNameWithoutExtension(FileReferences.Moc); #if UNITY_EDITOR // Add parameters and parts inspectors. model.gameObject.AddComponent(); model.gameObject.AddComponent(); #endif // Create renderers. var rendererController = model.gameObject.AddComponent(); var renderers = rendererController.Renderers; var drawables = model.Drawables; // Initialize materials. for (var i = 0; i < renderers.Length; ++i) { renderers[i].Material = pickMaterial(this, drawables[i]); } // Initialize textures. for (var i = 0; i < renderers.Length; ++i) { renderers[i].MainTexture = pickTexture(this, drawables[i]); } // Initialize drawables. if(HitAreas != null) { for (var i = 0; i < HitAreas.Length; i++) { for (var j = 0; j < drawables.Length; j++) { if (drawables[j].Id == HitAreas[i].Id) { // Add components for hit judgement to HitArea target Drawables. var hitDrawable = drawables[j].gameObject.AddComponent(); hitDrawable.Name = HitAreas[i].Name; drawables[j].gameObject.AddComponent(); break; } } } } //Load from cdi3.json var DisplayInfo3JsonAsString = DisplayInfo3Json; var cdi3Json = CubismDisplayInfo3Json.LoadFrom(DisplayInfo3JsonAsString); // Initialize groups. var parameters = model.Parameters; for (var i = 0; i < parameters.Length; ++i) { if (IsParameterInGroup(parameters[i], "EyeBlink")) { if (model.gameObject.GetComponent() == null) { model.gameObject.AddComponent(); } parameters[i].gameObject.AddComponent(); } // Set up mouth parameters. if (IsParameterInGroup(parameters[i], "LipSync")) { if (model.gameObject.GetComponent() == null) { model.gameObject.AddComponent(); } parameters[i].gameObject.AddComponent(); } // Setting up the parameter name for display. if (cdi3Json != null) { var cubismDisplayInfoParameterName = parameters[i].gameObject.AddComponent(); cubismDisplayInfoParameterName.Name = cdi3Json.Parameters[i].Name; cubismDisplayInfoParameterName.DisplayName = string.Empty; } } // Setting up the part name for display. if (cdi3Json != null) { // Initialize groups. var parts = model.Parts; for (var i = 0; i < parts.Length; i++) { var cubismDisplayInfoPartNames = parts[i].gameObject.AddComponent(); cubismDisplayInfoPartNames.Name = cdi3Json.Parts[i].Name; cubismDisplayInfoPartNames.DisplayName = string.Empty; } } // Add mask controller if required. for (var i = 0; i < drawables.Length; ++i) { if (!drawables[i].IsMasked) { continue; } // Add controller exactly once... model.gameObject.AddComponent(); break; } // Add original workflow component if is original workflow. if(shouldImportAsOriginalWorkflow) { // Add cubism update manager. var updateManager = model.gameObject.GetComponent(); if(updateManager == null) { model.gameObject.AddComponent(); } // Add parameter store. var parameterStore = model.gameObject.GetComponent(); if(parameterStore == null) { parameterStore = model.gameObject.AddComponent(); } // Add pose controller. var poseController = model.gameObject.GetComponent(); if(poseController == null) { poseController = model.gameObject.AddComponent(); } // Add expression controller. var expressionController = model.gameObject.GetComponent(); if(expressionController == null) { expressionController = model.gameObject.AddComponent(); } // Add fade controller. var motionFadeController = model.gameObject.GetComponent(); if(motionFadeController == null) { motionFadeController = model.gameObject.AddComponent(); } } // Initialize physics if JSON exists. var physics3JsonAsString = Physics3Json; if (!string.IsNullOrEmpty(physics3JsonAsString)) { var physics3Json = CubismPhysics3Json.LoadFrom(physics3JsonAsString); var physicsController = model.gameObject.GetComponent(); if (physicsController == null) { physicsController = model.gameObject.AddComponent(); } physicsController.Initialize(physics3Json.ToRig()); } var userData3JsonAsString = UserData3Json; if (!string.IsNullOrEmpty(userData3JsonAsString)) { var userData3Json = CubismUserData3Json.LoadFrom(userData3JsonAsString); var drawableBodies = userData3Json.ToBodyArray(CubismUserDataTargetType.ArtMesh); for (var i = 0; i < drawables.Length; ++i) { var index = GetBodyIndexById(drawableBodies, drawables[i].Id); if (index >= 0) { var tag = drawables[i].gameObject.GetComponent(); if (tag == null) { tag = drawables[i].gameObject.AddComponent(); } tag.Initialize(drawableBodies[index]); } } } if (model.gameObject.GetComponent() == null) { model.gameObject.AddComponent(); } // Make sure model is 'fresh' model.ForceUpdateNow(); return model; } #region Helper Methods /// /// Type-safely loads an asset. /// /// Asset type. /// Path to asset. /// The asset on success; otherwise. private T LoadReferencedAsset(string referencedFile) where T : class { var assetPath = Path.GetDirectoryName(AssetPath) + "/" + referencedFile; return LoadAssetAtPath(typeof(T), assetPath) as T; } /// /// Builtin method for loading assets. /// /// Asset type. /// Path to asset. /// The asset on success; otherwise. private static object BuiltinLoadAssetAtPath(Type assetType, string assetPath) { // Explicitly deal with byte arrays. if (assetType == typeof(byte[])) { #if UNITY_EDITOR return File.ReadAllBytes(assetPath); #else var textAsset = Resources.Load(assetPath, typeof(TextAsset)) as TextAsset; return (textAsset != null) ? textAsset.bytes : null; #endif } else if (assetType == typeof(string)) { #if UNITY_EDITOR return File.ReadAllText(assetPath); #else var textAsset = Resources.Load(assetPath, typeof(TextAsset)) as TextAsset; return (textAsset != null) ? textAsset.text : null; #endif } #if UNITY_EDITOR return AssetDatabase.LoadAssetAtPath(assetPath, assetType); #else return Resources.Load(assetPath, assetType); #endif } /// /// Checks whether the parameter is an eye blink parameter. /// /// Parameter to check. /// Name of group to query for. /// if parameter is an eye blink parameter; otherwise. private bool IsParameterInGroup(CubismParameter parameter, string groupName) { // Return early if groups aren't available... if (Groups == null || Groups.Length == 0) { return false; } for (var i = 0; i < Groups.Length; ++i) { if (Groups[i].Name != groupName) { continue; } if(Groups[i].Ids != null) { for (var j = 0; j < Groups[i].Ids.Length; ++j) { if (Groups[i].Ids[j] == parameter.name) { return true; } } } } return false; } /// /// Get body index from body array by Id. /// /// Target body array. /// Id for find. /// Array index if Id found; -1 otherwise. private int GetBodyIndexById(CubismUserDataBody[] bodies, string id) { for (var i = 0; i < bodies.Length; ++i) { if (bodies[i].Id == id) { return i; } } return -1; } #endregion #region Json Helpers /// /// File references data. /// [Serializable] public struct SerializableFileReferences { /// /// Relative path to the moc3 asset. /// [SerializeField] public string Moc; /// /// Relative paths to texture assets. /// [SerializeField] public string[] Textures; /// /// Relative path to the pose3.json. /// [SerializeField] public string Pose; /// /// Relative path to the expression asset. /// [SerializeField] public SerializableExpression[] Expressions; /// /// Relative path to the pose motion3.json. /// [SerializeField] public SerializableMotions Motions; /// /// Relative path to the physics asset. /// [SerializeField] public string Physics; /// /// Relative path to the user data asset. /// [SerializeField] public string UserData; /// /// Relative path to the cdi3.json. /// [SerializeField] public string DisplayInfo; } /// /// Group data. /// [Serializable] public struct SerializableGroup { /// /// Target type. /// [SerializeField] public string Target; /// /// Group name. /// [SerializeField] public string Name; /// /// Referenced IDs. /// [SerializeField] public string[] Ids; } /// /// Expression data. /// [Serializable] public struct SerializableExpression { /// /// Expression Name. /// [SerializeField] public string Name; /// /// Expression File. /// [SerializeField] public string File; /// /// Expression FadeInTime. /// [SerializeField] public float FadeInTime; /// /// Expression FadeOutTime. /// [SerializeField] public float FadeOutTime; } /// /// Motion data. /// [Serializable] public struct SerializableMotions { /// /// Motion group names. /// [SerializeField] public string[] GroupNames; /// /// Motion groups. /// [SerializeField] public SerializableMotion[][] Motions; } /// /// Motion data. /// [Serializable] public struct SerializableMotion { /// /// File path. /// [SerializeField] public string File; /// /// Sound path. /// [SerializeField] public string Sound; /// /// Fade in time. /// [SerializeField] public float FadeInTime; /// /// Fade out time. /// [SerializeField] public float FadeOutTime; } /// /// Hit Area. /// [Serializable] public struct SerializableHitArea { /// /// Hit area name. /// [SerializeField] public string Name; /// /// Hit area id. /// [SerializeField] public string Id; } #endregion } }