using System; using System.Linq; using LibMMD.Model; using UnityEngine; namespace LibMMD.Unity3D { internal sealed class MmdSubMeshVisibilityController { private MmdModel _model; private UnityEngine.Material[] _materials; private SkinnedMeshRenderer[] _activeModelRenderers = Array.Empty(); private bool _usesMainMesh; private bool _renderersVisible = true; private bool[] _subMeshVisibility = Array.Empty(); private UnityEngine.Material[] _visibleSubMeshOverrides = Array.Empty(); private Mesh _sourceMainMesh; private Mesh _instanceMainMesh; private int[][] _subMeshTriangles = Array.Empty(); internal void Configure(MmdModel model, UnityEngine.Material[] materials, SkinnedMeshRenderer[] activeModelRenderers, bool usesMainMesh, bool renderersVisible) { ReleaseInstanceMainMesh(); _model = model; _materials = materials; _activeModelRenderers = activeModelRenderers ?? Array.Empty(); _usesMainMesh = usesMainMesh; _renderersVisible = renderersVisible; PrepareInstanceMainMesh(); } private void PrepareInstanceMainMesh() { _subMeshTriangles = Array.Empty(); if (!_usesMainMesh || _activeModelRenderers.Length == 0 || _activeModelRenderers[0] == null) return; _sourceMainMesh = _activeModelRenderers[0].sharedMesh; if (_sourceMainMesh == null) return; // Cached model meshes can be shared by multiple MmdGameObjects. Visibility is // per instance, so keep a private mesh whose submesh index buffers can be changed. _instanceMainMesh = UnityEngine.Object.Instantiate(_sourceMainMesh); _instanceMainMesh.name = _sourceMainMesh.name + " (Visibility Instance)"; _instanceMainMesh.hideFlags = HideFlags.DontSave; _subMeshTriangles = new int[_instanceMainMesh.subMeshCount][]; for (var i = 0; i < _subMeshTriangles.Length; i++) _subMeshTriangles[i] = _instanceMainMesh.GetTriangles(i); _activeModelRenderers[0].sharedMesh = _instanceMainMesh; } /// Number of material-backed submeshes in the loaded MMD model. internal int SubMeshCount => _model == null || _model.Parts == null ? 0 : _model.Parts.Length; /// Returns the PMX/PMD material names in submesh order. internal string[] GetSubMeshNames() { if (_model == null || _model.Parts == null) return Array.Empty(); return _model.Parts.Select(part => part.Material == null ? string.Empty : part.Material.Name).ToArray(); } internal bool IsSubMeshVisible(int subMeshIndex) { ValidateSubMeshIndex(subMeshIndex); return _subMeshVisibility.Length != SubMeshCount || _subMeshVisibility[subMeshIndex]; } internal bool HasVisibleSubMesh() { if (SubMeshCount == 0) return false; if (_subMeshVisibility.Length != SubMeshCount) return true; return _subMeshVisibility.Any(visible => visible); } /// Shows or hides one material-backed submesh for every render pass. internal void SetSubMeshVisible(int subMeshIndex, bool visible) { ValidateSubMeshIndex(subMeshIndex); EnsureSubMeshVisibilityState(); _subMeshVisibility[subMeshIndex] = visible; ApplySubMeshVisibility(subMeshIndex); } /// /// Shows or hides all submeshes whose material name exactly matches . /// Returns the number of matching submeshes. /// internal int SetSubMeshVisible(string materialName, bool visible) { if (materialName == null) throw new ArgumentNullException(nameof(materialName)); if (_model == null || _model.Parts == null) return 0; EnsureSubMeshVisibilityState(); var changed = 0; for (var i = 0; i < _model.Parts.Length; i++) { var material = _model.Parts[i].Material; if (material == null || !string.Equals(material.Name, materialName, StringComparison.OrdinalIgnoreCase)) continue; _subMeshVisibility[i] = visible; ApplySubMeshVisibility(i); changed++; } return changed; } internal void Reset() { ReleaseVisibleSubMeshOverrides(); _subMeshVisibility = new bool[SubMeshCount]; _visibleSubMeshOverrides = new UnityEngine.Material[SubMeshCount]; for (var i = 0; i < _subMeshVisibility.Length; i++) { // PMX models commonly keep alternate outfits at diffuse alpha zero and // reveal them through a material morph. Reflect that authored initial // state in the manual submesh UI instead of showing a misleading ON. _subMeshVisibility[i] = _model.Parts[i].Material.DiffuseColor.a > 1.0f / 255.0f; ApplySubMeshVisibility(i); } } private void ReleaseVisibleSubMeshOverrides() { foreach (var material in _visibleSubMeshOverrides) if (material != null) UnityEngine.Object.Destroy(material); _visibleSubMeshOverrides = Array.Empty(); } private void EnsureSubMeshVisibilityState() { if (_subMeshVisibility.Length == SubMeshCount) return; Reset(); } private void ValidateSubMeshIndex(int subMeshIndex) { if (_model == null) throw new InvalidOperationException("An MMD model has not been loaded."); if (subMeshIndex < 0 || subMeshIndex >= SubMeshCount) throw new ArgumentOutOfRangeException(nameof(subMeshIndex)); } private void ApplySubMeshVisibility(int subMeshIndex) { if (_materials == null || subMeshIndex >= _materials.Length) return; var visible = _subMeshVisibility[subMeshIndex]; if (_usesMainMesh) { if (_activeModelRenderers.Length == 0 || _activeModelRenderers[0] == null) return; // MagicaCloth replaces sharedMesh with a runtime custom mesh after setup. // Updating only _instanceMainMesh would therefore preserve the visibility // captured at build time but ignore every later UI toggle. Always modify the // mesh currently rendered; both the normal visibility instance and // MagicaCloth's custom mesh retain the same submesh ordering. var renderMesh = _activeModelRenderers[0].sharedMesh; if (renderMesh != null && subMeshIndex < _subMeshTriangles.Length && subMeshIndex < renderMesh.subMeshCount) renderMesh.SetTriangles(visible ? _subMeshTriangles[subMeshIndex] : Array.Empty(), subMeshIndex, false); var assigned = _activeModelRenderers[0].sharedMaterials; if (subMeshIndex >= assigned.Length) return; assigned[subMeshIndex] = GetVisibleSubMeshMaterial(subMeshIndex); _activeModelRenderers[0].sharedMaterials = assigned; return; } if (subMeshIndex < _activeModelRenderers.Length && _activeModelRenderers[subMeshIndex] != null) { var renderer = _activeModelRenderers[subMeshIndex]; renderer.sharedMaterial = GetVisibleSubMeshMaterial(subMeshIndex); // MagicaCloth owns the source renderer while its render mesh is active and // may restore Renderer.enabled during a simulation/render update. Keep the // renderer enabled for integrations that require it, and apply manual // submesh visibility as the final render-stage mask instead. MagicaCloth // does not overwrite forceRenderingOff. renderer.enabled = true; renderer.forceRenderingOff = !_renderersVisible || !visible; } } internal void SetRenderersVisible(bool visible) { _renderersVisible = visible; for (var i = 0; i < _activeModelRenderers.Length; i++) { var renderer = _activeModelRenderers[i]; if (renderer == null) continue; renderer.enabled = true; renderer.forceRenderingOff = !visible || (!_usesMainMesh && i < SubMeshCount && !IsSubMeshVisible(i)); } } private UnityEngine.Material GetVisibleSubMeshMaterial(int subMeshIndex) { var source = _model.Parts[subMeshIndex].Material; if (source.DiffuseColor.a > 1.0f / 255.0f) return _materials[subMeshIndex]; if (_visibleSubMeshOverrides[subMeshIndex] != null) return _visibleSubMeshOverrides[subMeshIndex]; // Do not mutate _materials: those instances may belong to the model resource // cache and be shared by another MmdGameObject. Only initially-hidden outfit // parts need a private alpha override when manually enabled. var visible = new UnityEngine.Material(_materials[subMeshIndex]) { name = _materials[subMeshIndex].name + " (Visible Override)" }; SetMaterialColorAlpha(visible, "_Color", 1.0f); SetMaterialColorAlpha(visible, "_BaseColor", 1.0f); SetMaterialColorAlpha(visible, "_OutlineColor", 1.0f); if (visible.HasProperty("_Opacity")) visible.SetFloat("_Opacity", 1.0f); _visibleSubMeshOverrides[subMeshIndex] = visible; return visible; } private static void SetMaterialColorAlpha(UnityEngine.Material material, string propertyName, float alpha) { if (!material.HasProperty(propertyName)) return; var color = material.GetColor(propertyName); color.a = alpha; material.SetColor(propertyName, color); } internal void Dispose() { ReleaseInstanceMainMesh(); ReleaseVisibleSubMeshOverrides(); } private void ReleaseInstanceMainMesh() { if (_instanceMainMesh == null) return; if (_activeModelRenderers.Length > 0 && _activeModelRenderers[0] != null && _activeModelRenderers[0].sharedMesh == _instanceMainMesh) _activeModelRenderers[0].sharedMesh = _sourceMainMesh; UnityEngine.Object.Destroy(_instanceMainMesh); _sourceMainMesh = null; _instanceMainMesh = null; _subMeshTriangles = Array.Empty(); } } }