using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using LibMMD.Model; using LibMMD.Motion; using LibMMD.Reader; using LibMMD.Unity3D.Animation; using LibMMD.Util; using UnityEngine; using LibMMD.Unity3D.Physics; using Debug = UnityEngine.Debug; namespace LibMMD.Unity3D { internal sealed class MmdGameObject : MonoBehaviour, IMmdModelInstance { [SerializeField] private MmdModelOptions _loadOptions = new MmdModelOptions(); public MmdModelOptions Options => _loadOptions ??= new MmdModelOptions(); internal MmdModelOptions OptionsSnapshot => _loadOptions; public GameObject RootObject => gameObject; public bool EnableLogging { get { return _enableLog; } set { _enableLog = value; if (_mmdAnimator != null) { _mmdAnimator.EnableLog = value; } } } private bool _enableLog = true; public bool AutoPhysicsStepLength { get { return _mmdAnimator.AutoPhysicsStepLength; } set { _mmdAnimator.AutoPhysicsStepLength = value; } } public bool IsPlaying { get { return _mmdAnimator.Playing; } set { _mmdAnimator.Playing = value; } } public bool IsLooping { get { return _mmdAnimator.Loop; } set { _mmdAnimator.Loop = value; } } [Tooltip("Apply VMD IK after FK. VMD IK on/off keys are respected when enabled.")] public bool EnableVmdIk { get => _mmdAnimator != null ? _mmdAnimator.EnableVmdIk : _enableVmdIk; set { _enableVmdIk = value; if (_mmdAnimator != null) _mmdAnimator.EnableVmdIk = value; } } [SerializeField] private bool _enableVmdIk = true; public int PhysicsCacheFrameSize { get { return _mmdAnimator.PhysicsCacheFrameSize; } set { _mmdAnimator.PhysicsCacheFrameSize = value; } } public float PhysicsFps { get { return _mmdAnimator.PhysicsFps; } set { _mmdAnimator.PhysicsFps = value; } } public MmdAnimator.MmdEventDelegate OnMmdEvent { get { return _mmdAnimator.OnMmdEvent; } set { _mmdAnimator.OnMmdEvent = value; } } public string ModelName { get { return _model.Name; } } public string ModelPath { get; private set; } public string MotionPath => _mmdAnimator.MotionPath; public string BonePoseFilePath => _mmdAnimator.BonePoseFilePath; public double MotionLength => _mmdAnimator.MotionLength; public double MotionPos { get { return _mmdAnimator.MotionPos; } set { // MmdAnimator rebases its built-in physics while applying the seek pose. Reset // optional external backends afterwards so MagicaCloth starts from that same // discontinuously selected pose instead of retaining its pre-seek simulation. _mmdAnimator.MotionPos = value; } } public MmdRuntimeRig RuntimeRig { get; private set; } internal MmdGameObject() { ModelPath = null; } public Mesh Mesh { get; private set; } public List PartMeshes { get; private set; } private GameObject[] _bones; private const int DefaultMaxTextureSize = 1024; private MmdModel _model; private List> _partIndexes; private MaterialLoader _materialLoader; private readonly ModelReadConfig _modelReadConfig = new ModelReadConfig {GlobalToonPath = ""}; private UnityEngine.Material[] _materials; private readonly MmdRendererController _rendererController = new MmdRendererController(); // Non-null while _model/Mesh/PartMeshes/_partIndexes/_materialLoader/_materials are the // same instances held by MmdModelResourceCache's current entry (reused across a model // reload instead of freshly parsed/built by this component) - every method that would // otherwise Destroy()/Dispose() them (Release, ReleasePreviousMeshes, // ReleasePreviousMaterials) must skip doing so in that case, since the cache (or a // differently-timed reload) may still want them. See MmdModelResourceCache for the // cache itself and why only one entry is kept. private MmdModelResourceCache.Entry _cacheEntry; private MmdUnityConfig _config = new MmdUnityConfig(); private GameObject _boneRootGameObject; private MmdAnimator _mmdAnimator; private MmdModelHierarchyBuilder _hierarchyBuilder; private readonly MmdMaterialController _materialController = new MmdMaterialController(); private void Awake() { _mmdAnimator = GetComponent(); if (_mmdAnimator == null) { _mmdAnimator = gameObject.AddComponent(); } _mmdAnimator.EnableLog = _enableLog; _mmdAnimator.EnableVmdIk = _enableVmdIk; _mmdAnimator.PhysicsMode = _loadOptions.Physics; } internal static GameObject Create( MmdModelOptions options, string name = "MMDGameObject") { if (options == null) throw new ArgumentNullException(nameof(options)); var obj = new GameObject(name); obj.AddComponent(); var instance = obj.AddComponent(); instance._loadOptions = options.CopyAndValidate(); instance.ApplyLoadOptions(); var skinnedMeshRenderer = obj.AddComponent(); skinnedMeshRenderer.quality = SkinQuality.Bone4; return obj; } public MmdUnityConfig MaterialSettings => _config; public void UpdateMaterialSettings(MmdUnityConfig config) { if (config == null) throw new ArgumentNullException(nameof(config)); _config = config; RefreshByConfig(); } /// /// Switches every loaded model material immediately. Register a custom /// adapter for shader packages such as NiroToon. /// public void SetMaterialAdapter(string adapterId, string customShaderName = null) { _config.ShaderAdapterId = adapterId; _config.CustomShaderName = customShaderName; RefreshByConfig(); } public int SubMeshCount => _rendererController.SubMeshCount; public string[] GetSubMeshNames() => _rendererController.GetSubMeshNames(); public bool IsSubMeshVisible(int subMeshIndex) => _rendererController.IsSubMeshVisible(subMeshIndex); /// Returns whether at least one material-backed submesh is visible. public bool HasVisibleSubMesh => _rendererController.HasVisibleSubMesh; public void SetSubMeshVisible(int subMeshIndex, bool visible) => _rendererController.SetSubMeshVisible(subMeshIndex, visible); public int SetSubMeshVisible(string materialName, bool visible) => _rendererController.SetSubMeshVisible(materialName, visible); /// Returns the renderers created for the currently loaded model. public SkinnedMeshRenderer[] GetRenderers(bool visibleOnly = false) => _rendererController.GetRenderers(visibleOnly); /// /// Calculates world bounds for the visible model renderers in their current animated pose. /// Current bone positions are included to avoid a one-frame stale skinned-renderer bound. /// public bool TryGetVisibleWorldBounds(out Bounds bounds) => _rendererController.TryGetVisibleWorldBounds(out bounds); private void ResetSubMeshVisibility() { _rendererController.Configure( _model, _materials, _rendererController.GetRenderers(), Mesh != null); } private void RefreshByConfig() { _materialController.Refresh(_model, _materialLoader, _materials, _config); RefreshRendererShadowConfig(); } // Renderer-level casting/receiving is shared. The actual ShadowCaster // pass remains the responsibility of each selected shader. private void RefreshRendererShadowConfig() { _rendererController.ApplyShadowConfig(_config, GetComponentsInChildren(true)); } private void ApplyLoadOptions() { _loadOptions ??= new MmdModelOptions(); _loadOptions.Cloth ??= new MmdClothOptions(); // In EditMode, AddComponent does not guarantee that Awake has initialized the // companion animator before the factory applies its options. Keep this method // valid for both editor construction and the normal runtime Awake path. if (_mmdAnimator == null) { _mmdAnimator = GetComponent(); if (_mmdAnimator == null) _mmdAnimator = gameObject.AddComponent(); _mmdAnimator.EnableLog = _enableLog; _mmdAnimator.EnableVmdIk = _enableVmdIk; } _mmdAnimator.PhysicsMode = _loadOptions.Physics; } // Background-thread equivalent of LoadModel: the parse + mesh-array-build stage // (ModelLoadPreparer.Prepare, see DoLoadModelAsync) runs via Task.Run instead of // blocking the main thread, and material/texture loading (LoadMaterialsAsync) yields // once per material - Texture2D.LoadImage must run on the main thread and can't be // backgrounded, so spreading it across frames is the responsiveness knob for // texture-heavy models instead of one large stall. Mesh upload, bone hierarchy, // part renderers, and blend shapes are also time-sliced on the main thread. Physics, // cloth, and rig construction remain main-thread-only atomic stages. // Drive with `yield return`. private IEnumerator LoadModelCoroutine(string path) { var diagnostics = new MmdLoadDiagnostics(() => EnableLogging, path); ApplyLoadOptions(); ModelPath = path; var loadCoroutine = diagnostics.Measure("model preparation", DoLoadModelAsync(path)); while (loadCoroutine.MoveNext()) { yield return loadCoroutine.Current; } // File I/O for PNG/JPEG textures is safe off the Unity thread. Keep mobile peak // memory bounded to two in-flight reads; LoadImage itself remains main-thread-only. Task texturePrefetchTask = _cacheEntry != null && _cacheEntry.Materials != null ? Task.FromResult(0) : _materialLoader.PrefetchTexturesAsync(_model, 2); Utils.ClearAllTransformChild(transform); var boneLoader = diagnostics.Measure("bone hierarchy", CreateBonesIncremental(gameObject)); while (boneLoader.MoveNext()) yield return boneLoader.Current; SkinnedMeshRenderer skinnedMeshRenderer = null; SkinnedMeshRenderer[] partSkinnedMeshRenderers = null; int[] morphBlendShapeIndex = null; int[][] partMorphBlendShapeIndex = null; var textureWait = diagnostics.Measure("texture file I/O", MmdLoadDiagnostics.WaitForTask(texturePrefetchTask)); while (textureWait.MoveNext()) yield return textureWait.Current; // Observe and propagate I/O failures before creating materials. texturePrefetchTask.GetAwaiter().GetResult(); if (Mesh != null) { var materialLoader = diagnostics.Measure("materials and texture decode", LoadMaterialsAsync()); while (materialLoader.MoveNext()) yield return materialLoader.Current; GetComponent().mesh = Mesh; skinnedMeshRenderer = GetComponent(); BuildBindpose(Mesh, skinnedMeshRenderer, true); _rendererController.SetActiveRenderers(new[] { skinnedMeshRenderer }); var morphLoader = diagnostics.Measure("blend shapes", GetOrBuildMorphBlendShapeIndexIncremental(value => morphBlendShapeIndex = value)); while (morphLoader.MoveNext()) yield return morphLoader.Current; } else { var materialLoader = diagnostics.Measure("materials and texture decode", LoadMaterialsAsync()); while (materialLoader.MoveNext()) yield return materialLoader.Current; GetComponent().mesh = null; ClearMainSkinnedMeshRenderer(); var partLoader = diagnostics.Measure("part renderers", CreatePartObjectsIncremental(value => partSkinnedMeshRenderers = value)); while (partLoader.MoveNext()) yield return partLoader.Current; _rendererController.SetActiveRenderers(partSkinnedMeshRenderers); var morphLoader = diagnostics.Measure("blend shapes", GetOrBuildPartMorphBlendShapeIndexIncremental(value => partMorphBlendShapeIndex = value)); while (morphLoader.MoveNext()) yield return morphLoader.Current; } RefreshRendererShadowConfig(); ResetSubMeshVisibility(); // Every physics mode enters through the same backend contract. Backends may yield // between construction phases without leaking mode-specific behavior here. yield return null; var poser = new Poser(_model) { EnableIk = _mmdAnimator.EnableVmdIk }; if (!MmdPhysicsBackendRegistry.TryCreate(_loadOptions.Physics, out var physicsBackend)) { Debug.LogWarning($"No MMD physics backend is registered for {_loadOptions.Physics}; continuing without physics."); physicsBackend = new NoneMmdPhysicsBackend(); } var physicsContext = new MmdPhysicsBuildContext(this, _model, poser, _boneRootGameObject, _bones, skinnedMeshRenderer, partSkinnedMeshRenderers, _partIndexes); var physicsLoader = diagnostics.Measure($"physics ({_loadOptions.Physics})", physicsBackend.Build(physicsContext)); while (physicsLoader.MoveNext()) yield return physicsLoader.Current; yield return null; diagnostics.MeasureAtomic("animation rig", () => RuntimeRig = _loadOptions.AnimationRigging ? MmdRuntimeRigBuilder.Build(_boneRootGameObject, _model, _bones) : null); yield return null; diagnostics.MeasureAtomic("animator finalization", () => _mmdAnimator.LoadModel(_model, _boneRootGameObject, _bones, skinnedMeshRenderer, partSkinnedMeshRenderers, morphBlendShapeIndex, partMorphBlendShapeIndex, poser, physicsBackend, path)); diagnostics.Complete(); if (EnableLogging) Debug.LogFormat("load model finished {0}", path); } /// /// Loads a model and optional VMD through one pipeline. PMX/PMD preparation and VMD /// parsing start together on worker threads; motion binding/baking begins once the model /// hierarchy is ready. Existing individual load APIs remain available. /// private IEnumerator LoadModelAndMotionCoroutine(string modelPath, string motionPath) { var hasMotion = !string.IsNullOrWhiteSpace(motionPath); var motionTask = hasMotion ? MmdMotionLoader.LoadAsync(motionPath) : null; var modelLoader = LoadModel(modelPath); while (modelLoader.MoveNext()) yield return modelLoader.Current; if (!hasMotion) yield break; while (!motionTask.IsCompleted) yield return null; // Result propagates parse errors on the Unity thread, matching LoadMotionAsync. var motionLoader = LoadMotionCoroutine(motionTask.Result); while (motionLoader.MoveNext()) yield return motionLoader.Current; } /// Loads with background parsing and frame-budgeted Unity object creation. public IEnumerator LoadModel(string path) => LoadModelCoroutine(path); public IEnumerator LoadModelAndMotion(string modelPath, string motionPath) => LoadModelAndMotionCoroutine(modelPath, motionPath); public IEnumerator LoadMotion(string path) => LoadMotionCoroutine(path); public IEnumerator LoadMotion(MmdMotionAsset motionAsset) => LoadMotionCoroutine(motionAsset); private void LoadMotionImmediate(string path) { _mmdAnimator.LoadMotion(path); } /// Binds previously parsed, model-independent VMD data to this model. private void LoadMotionImmediate(MmdMotionAsset motionAsset) { _mmdAnimator.LoadMotion(motionAsset); } // See MmdAnimator.LoadMotionAsync - bakes the motion's kinematic FK on a background // thread instead of stalling the main thread. Drive with `yield return`. private IEnumerator LoadMotionCoroutine(string path) { return _mmdAnimator.LoadMotionAsync(path); } /// Binds and bakes previously parsed VMD data without reading the file again. private IEnumerator LoadMotionCoroutine(MmdMotionAsset motionAsset) { return _mmdAnimator.LoadMotionAsync(motionAsset); } // Lets callers hide the model while it's still in bind pose (e.g. while a motion // is being baked asynchronously via LoadMotionAsync) and reveal it only once ready. // Keep renderers enabled and mask rendering with forceRenderingOff: mesh-driven physics // integrations such as MagicaCloth must update an enabled source renderer while building // their runtime proxy mesh. Disabling it can make Unity calculate invalid skinned bounds. // Remembered by the renderer controller so any renderer created *after* this call (eg. a // >65535-vertex model's per-part renderers, built later during LoadModel/ // LoadModelAsync - see newMeshPart) starts in the same state instead of defaulting to // Unity's enabled=true and flashing visible mid-load regardless of this call. public void SetRenderersVisible(bool visible) { _rendererController.SetVisible(visible, GetComponentsInChildren(true)); } public void LoadPose(string path) { _mmdAnimator.LoadPose(path); } public void LoadBonePose(string path) { _mmdAnimator.LoadBonePoseFile(path); } public void ResetMotion() { _mmdAnimator.ResetMotion(); } public void ResetPhysics() { _mmdAnimator.ResetPhysics(); } private IEnumerator CreateBonesIncremental(GameObject rootGameObject) { _hierarchyBuilder = new MmdModelHierarchyBuilder(_model, gameObject); var loader = _hierarchyBuilder.CreateBonesIncremental(rootGameObject, Options, bones => _bones = bones); while (loader.MoveNext()) yield return loader.Current; _boneRootGameObject = _hierarchyBuilder.BoneRootGameObject; } public static BoneWeight ConvertBoneWeight(SkinningOperator op) => MmdModelHierarchyBuilder.ConvertBoneWeight(op); private void BuildBindpose(Mesh mesh, SkinnedMeshRenderer renderer, bool fillMaterials) { _hierarchyBuilder.BuildBindpose(mesh, renderer, fillMaterials, _materials); } private void OnDestroy() { Release(); _rendererController.Dispose(); } private IEnumerator CreatePartObjectsIncremental(Action completed) { var partMeshes = PartMeshes; var result = new SkinnedMeshRenderer[partMeshes.Count]; var slice = new MmdLoadTimeSlicer(Options); for (var i = 0; i < partMeshes.Count; i++) { var part = newMeshPart("Part" + i); part.GetComponent().mesh = partMeshes[i]; var renderer = part.GetComponent(); renderer.rootBone = _boneRootGameObject.transform; renderer.updateWhenOffscreen = true; renderer.material = _materials[i]; part.transform.SetParent(transform, false); BuildBindpose(partMeshes[i], renderer, false); result[i] = renderer; if (slice.ShouldYield()) yield return null; } completed(result); } // A model that exceeds the single-mesh vertex limit is rendered by // child part renderers. Clear the root renderer explicitly; otherwise // its previous sharedMesh remains invisible in the hierarchy but can // continue contributing a stale shadow caster. private void ClearMainSkinnedMeshRenderer() { var renderer = GetComponent(); if (renderer == null) return; renderer.sharedMesh = null; renderer.sharedMaterials = Array.Empty(); } private GameObject newMeshPart(string partName) { var ret = new GameObject(partName); ret.AddComponent(); var renderer = ret.AddComponent(); // Match the render mask last set by SetRenderersVisible. This renderer is created // later than the main one during LoadModel/LoadModelAsync. renderer.enabled = true; _rendererController.ApplyInitialVisibility(renderer); return ret; } // Yields once per material on an actual cache miss - // MaterialLoader.LoadMaterial decodes textures via Texture2D.LoadImage, which must run // on the main thread and can't be moved to a background thread, so this spreads that // decode cost across a frame per material instead of one large stall for models with // many textures. No-op fast path (no yields at all) when materials are already cached, // same as LoadMaterials(). private IEnumerator LoadMaterialsAsync() { var loader = _materialController.LoadAsync( _model, _materialLoader, _config, _cacheEntry, _materials, materials => _materials = materials); while (loader.MoveNext()) yield return loader.Current; } private void ReleasePreviousMaterials() { _materialController.Release(_materials, _cacheEntry); _materials = null; } private IEnumerator GetOrBuildMorphBlendShapeIndexIncremental(Action completed) { if (_cacheEntry != null && _cacheEntry.MorphBlendShapeIndex != null) { completed(_cacheEntry.MorphBlendShapeIndex); yield break; } int[] index = null; var loader = MorphBlendShapeBuilder.BuildIncremental(Mesh, _model, null, Options, value => index = value); while (loader.MoveNext()) yield return loader.Current; if (_cacheEntry != null) _cacheEntry.MorphBlendShapeIndex = index; completed(index); } private IEnumerator GetOrBuildPartMorphBlendShapeIndexIncremental(Action completed) { if (_cacheEntry != null && _cacheEntry.PartMorphBlendShapeIndex != null) { completed(_cacheEntry.PartMorphBlendShapeIndex); yield break; } var index = new int[PartMeshes.Count][]; for (var i = 0; i < PartMeshes.Count; i++) { var partIndex = i; var loader = MorphBlendShapeBuilder.BuildIncremental(PartMeshes[i], _model, _partIndexes[i], Options, value => index[partIndex] = value); while (loader.MoveNext()) yield return loader.Current; } if (_cacheEntry != null) _cacheEntry.PartMorphBlendShapeIndex = index; completed(index); } // Runs the pure-C# parse/mesh-array-build // stage (ModelLoadPreparer.Prepare) via Task.Run instead of blocking the main thread, // then applies the result (building the actual Mesh object(s), main-thread-only) once it // completes. No-op fast path (no yields at all) on a cache hit, same as DoLoadModel. // Drive with `yield return`. private IEnumerator DoLoadModelAsync(string filePath) { if (EnableLogging) Debug.LogFormat("start load model {0}", filePath); string key; if (TryReuseCachedModel(filePath, out key)) { if (EnableLogging) Debug.LogFormat("reused cached model resources {0}", filePath); yield break; } var task = Task.Run(() => ModelLoadPreparer.Prepare(filePath, _modelReadConfig)); while (!task.IsCompleted) { yield return null; } var prepared = task.Result; // rethrows on the calling thread if Prepare faulted var apply = ApplyPreparedModelIncremental(prepared, filePath, key); while (apply.MoveNext()) yield return apply.Current; if (EnableLogging) Debug.LogFormat("model resources prepared {0}", filePath); } // Shared cache-policy/lookup step for DoLoadModel/DoLoadModelAsync. Returns true after // filling _model/Mesh/PartMeshes from a cache hit. On a miss (or when caching is // disabled), returns false and supplies the optional key used when storing the result. private bool TryReuseCachedModel(string filePath, out string key) { // Guards against leaking a ref count if LoadModel is ever called a second time // on the same instance (normally each load gets its own fresh MmdGameObject). if (_cacheEntry != null) { MmdModelResourceCache.Release(_cacheEntry); _cacheEntry = null; } if (!Options.UseResourceCache) { key = null; return false; } key = MmdModelResourceCache.BuildKey(filePath); var cached = MmdModelResourceCache.TryGet(key); if (cached == null) { return false; } _model = cached.Model; Mesh = cached.Mesh; PartMeshes = cached.PartMeshes; _partIndexes = cached.PartIndexes; _materialLoader = cached.MaterialLoader; _cacheEntry = cached; key = null; return true; } private IEnumerator ApplyPreparedModelIncremental(ModelLoadPreparer.Result prepared, string filePath, string key) { _model = prepared.Model; Release(); var directoryInfo = new FileInfo(filePath).Directory; if (directoryInfo == null) throw new MmdFileParseException(filePath + " does not belong to any directory."); _materialLoader = new MaterialLoader(new TextureLoader(directoryInfo.FullName, DefaultMaxTextureSize, () => EnableLogging), () => EnableLogging); ReleasePreviousMeshes(); var meshBuilder = MmdMeshBuilder.BuildIncremental(prepared, Options, mesh => { // Track partial work immediately so cancellation/OnDestroy can release it. Mesh = mesh; _partIndexes = null; }, (meshes, indexes) => { PartMeshes = meshes; _partIndexes = indexes; }); while (meshBuilder.MoveNext()) yield return meshBuilder.Current; _cacheEntry = MmdModelResourceCache.Store(key, _model, Mesh, PartMeshes, _partIndexes, _materialLoader); } private void ReleasePreviousMeshes() { if (_cacheEntry == null) { if (PartMeshes != null) { foreach (var mesh in PartMeshes) { Destroy(mesh); } } if (Mesh != null) { Destroy(Mesh); } } PartMeshes = null; Mesh = null; } private void Release() { if (_mmdAnimator != null) { _mmdAnimator.Release(); } if (_materialLoader != null) { if (_cacheEntry == null) { _materialLoader.Dispose(); } _materialLoader = null; } if (_cacheEntry != null) { MmdModelResourceCache.Release(_cacheEntry); _cacheEntry = null; } } } }