using System; using System.Collections.Generic; using UnityEditor; using UnityEngine; using NBShader; namespace NBShaderEditor { internal sealed class ParticleBaseHoudiniVATGUI { private readonly ShaderGUIHelper _helper; private static readonly string[] SubModeNames = { "SoftBody (Deformation)", "RigidBody (Pieces)", "Dynamic Remeshing (Lookup)", "Particle Sprites (Billboard)" }; private static readonly string[] SubModeKeywords = { "_HOUDINI_VAT_SOFTBODY", "_HOUDINI_VAT_RIGIDBODY", "_HOUDINI_VAT_DYNAMIC_REMESH", "_HOUDINI_VAT_PARTICLE_SPRITE" }; public ParticleBaseHoudiniVATGUI(ShaderGUIHelper helper) { _helper = helper; } public void Draw() { DrawSubModeSelector(); DrawPlaybackSection(); DrawBoundsSection(); DrawTextureSection(); DrawScaleSection(); DrawParticleSpriteSection(); DrawFlagsSection(); SyncKeywords(); } public static void AppendRequiredVertexStreams( Material material, List streams, List streamList) { if (material.GetFloat("_VAT_Toggle") <= 0.5f) { return; } int subMode = GetMaterialSubMode(material); bool isParticleMode = IsHoudiniParticleModeEnabled(material); AddStream(streams, streamList, ParticleSystemVertexStream.Position, ParticleBaseGUI.streamPositionText); AddStream(streams, streamList, ParticleSystemVertexStream.Normal, ParticleBaseGUI.streamNormalText); switch (subMode) { case 0: // SoftBody needs VAT UV1. AddVatUV1Stream(streams, streamList, isParticleMode); break; case 1: // RigidBody needs VAT UV1, UV2, UV3 (Custom2, vatTexcoord5) if (isParticleMode) { break; } AddVatUV1Stream(streams, streamList, isParticleMode); AddStream(streams, streamList, ParticleSystemVertexStream.Custom2XYZW, ParticleBaseGUI.streamCustom2Text); AddStream(streams, streamList, ParticleSystemVertexStream.UV, ParticleBaseGUI.streamUVText); break; case 2: // DynamicRemeshing needs UV0 (texcoords) AddStream(streams, streamList, ParticleSystemVertexStream.UV, ParticleBaseGUI.streamUVText); break; case 3: // ParticleSprite needs UV0 (corner) + VAT UV1 (particle U/V) AddStream(streams, streamList, ParticleSystemVertexStream.UV, ParticleBaseGUI.streamUVText); AddVatUV1Stream(streams, streamList, isParticleMode); break; } } private List mats => _helper.mats; private bool IsResetToolInitializing() { return _helper.ResetTool != null && _helper.ResetTool.IsInitResetData; } private bool TryGetCurrentSubMode(out int subMode) { MaterialProperty property = _helper.GetProperty("_HoudiniVATSubMode"); if (property == null || property.hasMixedValue) { subMode = -1; return false; } subMode = Mathf.Clamp(Mathf.RoundToInt(property.floatValue), 0, SubModeNames.Length - 1); return true; } private bool IsSubMode(params int[] subModes) { if (IsResetToolInitializing()) { return true; } if (!TryGetCurrentSubMode(out int currentSubMode)) { return false; } for (int i = 0; i < subModes.Length; i++) { if (currentSubMode == subModes[i]) { return true; } } return false; } private bool TryGetFloatProperty(string propertyName, out MaterialProperty property) { property = _helper.GetProperty(propertyName); return property != null && !property.hasMixedValue; } private bool IsFloatPropertyOn(string propertyName) { return TryGetFloatProperty(propertyName, out MaterialProperty property) && property.floatValue > 0.5f; } private bool IsFloatPropertyOff(string propertyName) { return TryGetFloatProperty(propertyName, out MaterialProperty property) && property.floatValue <= 0.5f; } private bool IsFloatPropertyMixed(string propertyName) { MaterialProperty property = _helper.GetProperty(propertyName); return property != null && property.hasMixedValue; } private bool ShouldDrawWhenFloatOn(string propertyName) { return IsResetToolInitializing() || IsFloatPropertyMixed(propertyName) || IsFloatPropertyOn(propertyName); } private bool ShouldDrawWhenFloatOff(string propertyName) { return IsResetToolInitializing() || IsFloatPropertyMixed(propertyName) || IsFloatPropertyOff(propertyName); } private static int GetMaterialSubMode(Material material) { if (!material.HasProperty("_HoudiniVATSubMode")) { return 0; } return Mathf.Clamp(Mathf.RoundToInt(material.GetFloat("_HoudiniVATSubMode")), 0, SubModeNames.Length - 1); } private static bool IsHoudiniParticleModeEnabled(Material material) { if (material == null || !material.HasProperty("_VAT_Toggle") || !material.HasProperty("_VATMode") || !material.HasProperty("_MeshSourceMode") || material.GetFloat("_VAT_Toggle") <= 0.5f || Mathf.RoundToInt(material.GetFloat("_VATMode")) != (int)ParticleBaseGUI.VATMode.Houdini) { return false; } ParticleBaseGUI.MeshSourceMode meshSourceMode = (ParticleBaseGUI.MeshSourceMode)Mathf.RoundToInt(material.GetFloat("_MeshSourceMode")); return meshSourceMode == ParticleBaseGUI.MeshSourceMode.Particle || meshSourceMode == ParticleBaseGUI.MeshSourceMode.UIParticle; } private void DrawSubModeSelector() { _helper.DrawPopUp("Houdini VAT Sub Mode", "_HoudiniVATSubMode", SubModeNames); if (HasUnsupportedParticleMode()) { EditorGUILayout.HelpBox("该 Houdini VAT 类型需要 Mesh 多 UV 数据,不支持 ParticleSystem VertexStream 模式。", MessageType.Warning); } } private void DrawPlaybackSection() { EditorGUILayout.Space(); EditorGUILayout.LabelField("Playback", EditorStyles.boldLabel); _helper.DrawToggle("Auto Playback", "_B_autoPlayback"); if (ShouldDrawWhenFloatOff("_B_autoPlayback")) { _helper.DrawFloat("Display Frame", "_displayFrame"); } DrawFrameCustomDataSelect(); _helper.DrawFloat("Game Time at First Frame", "_gameTimeAtFirstFrame"); _helper.DrawFloat("Playback Speed", "_playbackSpeed"); _helper.DrawFloat("Houdini FPS", "_houdiniFPS"); _helper.DrawToggle("Interframe Interpolation", "_B_interpolate"); if (IsSubMode(1)) // RigidBody { _helper.DrawToggle("Animate First Frame", "_animateFirstFrame"); } } private void DrawBoundsSection() { EditorGUILayout.Space(); EditorGUILayout.LabelField("Bounds Metadata", EditorStyles.boldLabel); _helper.DrawFloat("Bound Min X", "_boundMinX"); _helper.DrawFloat("Bound Min Y", "_boundMinY"); _helper.DrawFloat("Bound Min Z", "_boundMinZ"); _helper.DrawFloat("Bound Max X", "_boundMaxX"); _helper.DrawFloat("Bound Max Y", "_boundMaxY"); _helper.DrawFloat("Bound Max Z", "_boundMaxZ"); } private void DrawFrameCustomDataSelect() { const int dataBitPos = W9ParticleShaderFlags.FLAGBIT_POS_2_CUSTOMDATA_VAT_FRAME; const int dataIndex = 2; W9ParticleShaderFlags.CutomDataComponent component = GetCurrentCustomDataComponent(dataBitPos, dataIndex); if (!IsAnyHoudiniParticleModeEnabled() || _helper.shaderFlags == null || _helper.shaderFlags.Length == 0) { ClearFrameCustomData(dataBitPos, dataIndex); return; } (string, string) nameTuple = ("VATFrameCustomData", ""); EditorGUI.showMixedValue = CustomDataHasMixedValue(dataBitPos, dataIndex); EditorGUI.BeginChangeCheck(); EditorGUILayout.BeginHorizontal(); component = (W9ParticleShaderFlags.CutomDataComponent)EditorGUILayout.Popup(new GUIContent("VAT Frame CustomData"), (int)component, ParticleBaseGUI.CustomDataOptions); EditorGUI.showMixedValue = false; Action applySelection = () => { for (int i = 0; i < _helper.shaderFlags.Length; i++) { if (_helper.shaderFlags[i] is W9ParticleShaderFlags flags) { flags.SetCustomDataFlag(component, dataBitPos, dataIndex); } } _helper.ResetTool.CheckOnValueChange(nameTuple); }; if (EditorGUI.EndChangeCheck()) { applySelection(); } _helper.ResetTool.DrawResetModifyButton( new Rect(), nameTuple, resetCallBack: () => { component = W9ParticleShaderFlags.CutomDataComponent.Off; applySelection(); }, onValueChangedCallBack: applySelection, checkHasModifyOnValueChange: () => GetCurrentCustomDataComponent(dataBitPos, dataIndex) != W9ParticleShaderFlags.CutomDataComponent.Off, checkHasMixedValueOnValueChange: () => CustomDataHasMixedValue(dataBitPos, dataIndex)); EditorGUILayout.EndHorizontal(); _helper.ResetTool.EndResetModifyButtonScope(); } private void ClearFrameCustomData(int dataBitPos, int dataIndex) { if (_helper.shaderFlags == null) { return; } for (int i = 0; i < _helper.shaderFlags.Length; i++) { if (_helper.shaderFlags[i] is W9ParticleShaderFlags flags) { flags.SetCustomDataFlag(W9ParticleShaderFlags.CutomDataComponent.Off, dataBitPos, dataIndex); } } } private W9ParticleShaderFlags.CutomDataComponent GetCurrentCustomDataComponent(int dataBitPos, int dataIndex) { if (_helper.shaderFlags == null || _helper.shaderFlags.Length == 0) { return W9ParticleShaderFlags.CutomDataComponent.Off; } if (_helper.shaderFlags[0] is W9ParticleShaderFlags flags) { return flags.GetCustomDataFlag(dataBitPos, dataIndex); } return W9ParticleShaderFlags.CutomDataComponent.Off; } private bool CustomDataHasMixedValue(int dataBitPos, int dataIndex) { if (_helper.shaderFlags == null || _helper.shaderFlags.Length == 0) { return false; } W9ParticleShaderFlags.CutomDataComponent component = W9ParticleShaderFlags.CutomDataComponent.UnKnownOrMixed; for (int i = 0; i < _helper.shaderFlags.Length; i++) { if (!(_helper.shaderFlags[i] is W9ParticleShaderFlags flags)) { continue; } W9ParticleShaderFlags.CutomDataComponent current = flags.GetCustomDataFlag(dataBitPos, dataIndex); if (component == W9ParticleShaderFlags.CutomDataComponent.UnKnownOrMixed) { component = current; continue; } if (component != current) { return true; } } return false; } private bool IsAnyHoudiniParticleModeEnabled() { if (_helper.mats == null) { return false; } for (int i = 0; i < _helper.mats.Count; i++) { if (IsHoudiniParticleModeEnabled(_helper.mats[i])) { return true; } } return false; } private bool HasUnsupportedParticleMode() { if (_helper.mats == null) { return false; } for (int i = 0; i < _helper.mats.Count; i++) { if (IsUnsupportedParticleMode(_helper.mats[i])) { return true; } } return false; } private static bool IsUnsupportedParticleMode(Material material) { return IsHoudiniParticleModeEnabled(material) && GetMaterialSubMode(material) == 1; } private void DrawTextureSection() { EditorGUILayout.Space(); EditorGUILayout.LabelField("Textures", EditorStyles.boldLabel); _helper.DrawTexture("Position Texture", "_posTexture", drawScaleOffset: false, drawWrapMode: false); if (ShouldDrawWhenFloatOn("_B_LOAD_POS_TWO_TEX")) { _helper.DrawTexture("Position Texture 2", "_posTexture2", drawScaleOffset: false, drawWrapMode: false); } if (ShouldDrawRotationTexture()) { _helper.DrawTexture("Rotation Texture", "_rotTexture", drawScaleOffset: false, drawWrapMode: false); } if (ShouldDrawWhenFloatOn("_B_LOAD_COL_TEX")) { _helper.DrawTexture("Color Texture", "_colTexture", drawScaleOffset: false, drawWrapMode: false); } if (IsSubMode(2) || ShouldDrawWhenFloatOn("_B_LOAD_LOOKUP_TABLE")) { _helper.DrawTexture("Lookup Table", "_lookupTable", drawScaleOffset: false, drawWrapMode: false); } } private void DrawScaleSection() { if (IsSubMode(1, 3)) { EditorGUILayout.Space(); EditorGUILayout.LabelField("Scale", EditorStyles.boldLabel); _helper.DrawFloat("Global Piece Scale Multiplier", "_globalPscaleMul"); _helper.DrawToggle("Piece Scales in Position Alpha", "_B_pscaleAreInPosA"); } } private void DrawParticleSpriteSection() { if (!IsSubMode(3)) { return; } EditorGUILayout.Space(); EditorGUILayout.LabelField("Particle Sprite", EditorStyles.boldLabel); _helper.DrawFloat("Width Base Scale", "_widthBaseScale"); _helper.DrawFloat("Height Base Scale", "_heightBaseScale"); _helper.DrawToggle("Hide Overlapping Origin", "_B_hideOverlappingOrigin"); if (ShouldDrawWhenFloatOn("_B_hideOverlappingOrigin")) { _helper.DrawFloat("Origin Effective Radius", "_originRadius"); } _helper.DrawToggle("Particles Can Spin", "_B_CAN_SPIN"); if (ShouldDrawWhenFloatOn("_B_CAN_SPIN")) { _helper.DrawToggle("Compute Spin from Heading", "_B_spinFromHeading"); if (ShouldDrawWhenFloatOff("_B_spinFromHeading")) { _helper.DrawFloat("Particle Spin Phase", "_spinPhase"); } _helper.DrawFloat("Scale by Velocity Amount", "_scaleByVelAmount"); } _helper.DrawFloat("Particle Texture U Scale", "_particleTexUScale"); _helper.DrawFloat("Particle Texture V Scale", "_particleTexVScale"); } private void DrawFlagsSection() { EditorGUILayout.Space(); EditorGUILayout.LabelField("Flags", EditorStyles.boldLabel); _helper.DrawToggle("Positions Require Two Textures", "_B_LOAD_POS_TWO_TEX"); if (ShouldDrawCompressedNormalsToggle()) { _helper.DrawToggle("Use Compressed Normals (no rotTex)", "_B_UNLOAD_ROT_TEX"); } _helper.DrawToggle("Load Color Texture", "_B_LOAD_COL_TEX"); if (IsSubMode(2)) { _helper.DrawToggle("Load Lookup Table", "_B_LOAD_LOOKUP_TABLE"); } } private bool ShouldDrawRotationTexture() { if (IsResetToolInitializing()) { return true; } if (!TryGetCurrentSubMode(out int subMode) || subMode == 3) { return false; } return IsFloatPropertyMixed("_B_UNLOAD_ROT_TEX") || IsFloatPropertyOff("_B_UNLOAD_ROT_TEX"); } private bool ShouldDrawCompressedNormalsToggle() { if (IsResetToolInitializing()) { return true; } return TryGetCurrentSubMode(out int subMode) && subMode != 3; } private void SyncKeywords() { if (mats == null) { return; } foreach (Material mat in mats) { if (mat == null) { continue; } SyncKeywords(mat); } } internal static void SyncKeywords(Material mat) { if (mat == null) { return; } SetSubModeKeyword(mat, GetMaterialSubMode(mat)); } internal static void ClearKeywords(Material mat) { if (mat == null) { return; } SetSubModeKeyword(mat, -1); } private static void SetSubModeKeyword(Material mat, int enabledIndex) { for (int i = 0; i < SubModeKeywords.Length; i++) { mat.DisableKeyword(SubModeKeywords[i]); } if (enabledIndex >= 0 && enabledIndex < SubModeKeywords.Length) { mat.EnableKeyword(SubModeKeywords[enabledIndex]); } } private static void AddStream( List streams, List streamList, ParticleSystemVertexStream stream, string streamLabel) { if (streams.Contains(stream)) { return; } streams.Add(stream); streamList.Add(streamLabel); } private static void AddVatUV1Stream( List streams, List streamList, bool isParticleMode) { if (isParticleMode) { AddStream(streams, streamList, ParticleSystemVertexStream.UV2, ParticleBaseGUI.streamUV2Text); return; } AddStream(streams, streamList, ParticleSystemVertexStream.Custom1XYZW, ParticleBaseGUI.streamCustom1Text); } } }