2025-12-04 03:50:29 +03:00
using System.Numerics ;
using ImGuiNET ;
using MaterialLib ;
using NResUI.Abstractions ;
using NResUI.Models ;
namespace NResUI.ImGuiUI ;
public class MaterialExplorerPanel : IImGuiPanel
{
private readonly MaterialViewModel _viewModel ;
public MaterialExplorerPanel ( MaterialViewModel viewModel )
{
_viewModel = viewModel ;
}
public void OnImGuiRender ()
{
if ( ImGui . Begin ( "Material Explorer" ))
{
2025-12-04 03:55:41 +03:00
ImGui . Text ( "Материал это бинарный файл, который можно найти в materials.lib." );
ImGui . Text ( "Его можно открыть только из открытого NRes архива, т.к. чтение полагается на флаги из архива" );
2025-12-04 03:50:29 +03:00
if (! _viewModel . HasFile )
{
ImGui . Text ( "No Material file opened" );
}
else if ( _viewModel . Error != null )
{
ImGui . TextColored ( new Vector4 ( 1 , 0 , 0 , 1 ), $"Error: {_viewModel.Error}" );
}
else
{
var mat = _viewModel . MaterialFile !;
ImGui . Text ( $"File: {_viewModel.FilePath}" );
ImGui . Separator ();
// === FILE READ SEQUENCE ===
if ( ImGui . CollapsingHeader ( "1. Metadata & Derived Fields" , ImGuiTreeNodeFlags . DefaultOpen ))
{
ImGui . TextDisabled ( "(Not from file content)" );
ImGui . Text ( $"File Name: {mat.FileName}" );
ImGui . Text ( $"Version (ElementCount): {mat.Version}" );
ImGui . Text ( $"Magic1: {mat.Magic1}" );
ImGui . Separator ();
ImGui . TextDisabled ( "(Derived from Version)" );
ImGui . Text ( $"Material Rendering Type: {mat.MaterialRenderingType}" );
if ( ImGui . IsItemHovered ())
{
ImGui . SetTooltip ( "0=Standard, 1=Special, 2=Particle/Effect" );
}
ImGui . Text ( $"Supports Bump Mapping: {mat.SupportsBumpMapping}" );
ImGui . Text ( $"Is Particle Effect: {mat.IsParticleEffect}" );
if ( ImGui . IsItemHovered ())
{
ImGui . SetTooltip ( "0=Normal material, 8=Particle/Effect (e.g., jet engines)" );
}
}
if ( ImGui . CollapsingHeader ( "2. File Header (Read Order)" , ImGuiTreeNodeFlags . DefaultOpen ))
{
ImGui . Text ( $"Stage Count: {mat.Stages.Count}" );
ImGui . SameLine ();
ImGui . TextDisabled ( "(2 bytes, ushort)" );
ImGui . Text ( $"Animation Count: {mat.Animations.Count}" );
ImGui . SameLine ();
ImGui . TextDisabled ( "(2 bytes, ushort)" );
ImGui . Separator ();
ImGui . TextDisabled ( $"(Read if Magic1 >= 2)" );
ImGui . Text ( $"Source Blend Mode: {mat.SourceBlendMode}" );
if ( ImGui . IsItemHovered ())
{
ImGui . SetTooltip ( GetBlendModeDescription ( mat . SourceBlendMode ));
}
ImGui . SameLine ();
ImGui . TextDisabled ( "(1 byte)" );
ImGui . Text ( $"Dest Blend Mode: {mat.DestBlendMode}" );
if ( ImGui . IsItemHovered ())
{
ImGui . SetTooltip ( GetBlendModeDescription ( mat . DestBlendMode ));
}
ImGui . SameLine ();
ImGui . TextDisabled ( "(1 byte)" );
ImGui . Separator ();
2025-12-04 03:55:41 +03:00
ImGui . TextDisabled ( "(Read if Magic1 > 2)" );
2025-12-04 03:50:29 +03:00
ImGui . Text ( $"Global Alpha Multiplier: {mat.GlobalAlphaMultiplier:F3}" );
if ( ImGui . IsItemHovered ())
{
ImGui . SetTooltip ( "Always 1.0 in all 628 materials - global alpha multiplier" );
}
ImGui . SameLine ();
ImGui . TextDisabled ( "(4 bytes, float)" );
ImGui . Separator ();
2025-12-04 03:55:41 +03:00
ImGui . TextDisabled ( "(Read if Magic1 > 3)" );
2025-12-04 03:50:29 +03:00
ImGui . Text ( $"Global Emissive Intensity: {mat.GlobalEmissiveIntensity:F3}" );
if ( ImGui . IsItemHovered ())
{
ImGui . SetTooltip ( "0=no glow (99.4%), rare values: 1000, 10000" );
}
ImGui . SameLine ();
ImGui . TextDisabled ( "(4 bytes, float)" );
}
if ( ImGui . CollapsingHeader ( $"3. Stages ({mat.Stages.Count}) - 34 bytes each" , ImGuiTreeNodeFlags . DefaultOpen ))
{
for ( int i = 0 ; i < mat . Stages . Count ; i ++)
{
var stage = mat . Stages [ i ];
if ( ImGui . TreeNode ( $"Stage {i}: {stage.TextureName}" ))
{
ImGui . TextDisabled ( "=== File Read Order (34 bytes) ===" );
// 1. Ambient (4 bytes)
ImGui . Text ( "1. Ambient Color:" );
ImGui . SameLine ();
ImGui . TextDisabled ( "(4 bytes, A scaled by 0.01)" );
var ambient = new Vector4 ( stage . AmbientR , stage . AmbientG , stage . AmbientB , stage . AmbientA );
ImGui . ColorEdit4 ( "##Ambient" , ref ambient , ImGuiColorEditFlags . NoInputs );
ImGui . SameLine ();
ImGui . Text ( $"RGBA: ({stage.AmbientR:F3}, {stage.AmbientG:F3}, {stage.AmbientB:F3}, {stage.AmbientA:F3})" );
// 2. Diffuse (4 bytes)
ImGui . Text ( "2. Diffuse Color:" );
ImGui . SameLine ();
ImGui . TextDisabled ( "(4 bytes)" );
var diffuse = new Vector4 ( stage . DiffuseR , stage . DiffuseG , stage . DiffuseB , stage . DiffuseA );
ImGui . ColorEdit4 ( "##Diffuse" , ref diffuse , ImGuiColorEditFlags . NoInputs );
ImGui . SameLine ();
ImGui . Text ( $"RGBA: ({stage.DiffuseR:F3}, {stage.DiffuseG:F3}, {stage.DiffuseB:F3}, {stage.DiffuseA:F3})" );
// 3. Specular (4 bytes)
ImGui . Text ( "3. Specular Color:" );
ImGui . SameLine ();
ImGui . TextDisabled ( "(4 bytes)" );
var specular = new Vector4 ( stage . SpecularR , stage . SpecularG , stage . SpecularB , stage . SpecularA );
ImGui . ColorEdit4 ( "##Specular" , ref specular , ImGuiColorEditFlags . NoInputs );
ImGui . SameLine ();
ImGui . Text ( $"RGBA: ({stage.SpecularR:F3}, {stage.SpecularG:F3}, {stage.SpecularB:F3}, {stage.SpecularA:F3})" );
// 4. Emissive (4 bytes)
ImGui . Text ( "4. Emissive Color:" );
ImGui . SameLine ();
ImGui . TextDisabled ( "(4 bytes)" );
var emissive = new Vector4 ( stage . EmissiveR , stage . EmissiveG , stage . EmissiveB , stage . EmissiveA );
ImGui . ColorEdit4 ( "##Emissive" , ref emissive , ImGuiColorEditFlags . NoInputs );
ImGui . SameLine ();
ImGui . Text ( $"RGBA: ({stage.EmissiveR:F3}, {stage.EmissiveG:F3}, {stage.EmissiveB:F3}, {stage.EmissiveA:F3})" );
// 5. Power (1 byte)
ImGui . Text ( $"5. Power: {stage.Power:F3}" );
ImGui . SameLine ();
ImGui . TextDisabled ( "(1 byte -> float)" );
// 6. Texture Stage Index (1 byte)
ImGui . Text ( $"6. Texture Stage Index: {stage.TextureStageIndex}" );
if ( ImGui . IsItemHovered ())
{
ImGui . SetTooltip ( "255 = not set/default, 0-47 = reference to specific texture stage" );
}
ImGui . SameLine ();
ImGui . TextDisabled ( "(1 byte)" );
// 7. Texture Name (16 bytes)
ImGui . Text ( $"7. Texture Name: {stage.TextureName}" );
ImGui . SameLine ();
ImGui . TextDisabled ( "(16 bytes, ASCII)" );
ImGui . TreePop ();
}
}
}
if ( ImGui . CollapsingHeader ( $"4. Animations ({mat.Animations.Count})" ))
{
for ( int i = 0 ; i < mat . Animations . Count ; i ++)
{
var anim = mat . Animations [ i ];
if ( ImGui . TreeNode ( $"Anim {i}: {anim.Target} ({anim.LoopMode})" ))
{
ImGui . TextDisabled ( "=== File Read Order ===" );
// Combined field (4 bytes)
ImGui . Text ( "1. Target & Loop Mode:" );
ImGui . SameLine ();
ImGui . TextDisabled ( "(4 bytes combined)" );
ImGui . Indent ();
ImGui . Text ( $"Target (bits 3-31): {anim.Target}" );
if ( ImGui . IsItemHovered ())
{
ImGui . SetTooltip ( anim . TargetDescription );
}
ImGui . Text ( $"Loop Mode (bits 0-2): {anim.LoopMode}" );
if ( ImGui . IsItemHovered ())
{
ImGui . SetTooltip ( GetAnimationLoopModeDescription ( anim . LoopMode ));
}
ImGui . Unindent ();
// Key count (2 bytes)
ImGui . Text ( $"2. Key Count: {anim.Keys.Count}" );
ImGui . SameLine ();
ImGui . TextDisabled ( "(2 bytes, ushort)" );
// Keys (6 bytes each)
ImGui . Text ( $"3. Keys ({anim.Keys.Count} × 6 bytes):" );
if ( ImGui . BeginTable ( $"keys_{i}" , 4 , ImGuiTableFlags . Borders | ImGuiTableFlags . RowBg ))
{
ImGui . TableSetupColumn ( "#" );
ImGui . TableSetupColumn ( "Stage Index (2 bytes)" );
ImGui . TableSetupColumn ( "Duration ms (2 bytes)" );
ImGui . TableSetupColumn ( "Interpolation (2 bytes)" );
ImGui . TableHeadersRow ();
int keyIdx = 0 ;
foreach ( var key in anim . Keys )
{
ImGui . TableNextRow ();
ImGui . TableNextColumn ();
ImGui . Text ( keyIdx . ToString ());
ImGui . TableNextColumn ();
ImGui . Text ( key . StageIndex . ToString ());
ImGui . TableNextColumn ();
ImGui . Text ( key . DurationMs . ToString ());
ImGui . TableNextColumn ();
ImGui . Text ( key . InterpolationCurve . ToString ());
if ( ImGui . IsItemHovered ())
{
ImGui . SetTooltip ( "Always 0 = linear interpolation" );
}
keyIdx ++;
}
ImGui . EndTable ();
}
ImGui . TreePop ();
}
}
}
}
}
ImGui . End ();
}
private static string GetBlendModeDescription ( BlendMode mode )
{
return mode switch
{
2025-12-04 07:58:32 +03:00
BlendMode . Zero => "D3DBLEND_ZERO\nBlend factor is (0, 0, 0, 0)\nResults in black/transparent" ,
BlendMode . One => "D3DBLEND_ONE\nBlend factor is (1, 1, 1, 1)\nUses full color value (no blending)" ,
BlendMode . SrcColor => "D3DBLEND_SRCCOLOR\nBlend factor is (Rs, Gs, Bs, As)\nUses source color" ,
BlendMode . InvSrcColor => "D3DBLEND_INVSRCCOLOR\nBlend factor is (1-Rs, 1-Gs, 1-Bs, 1-As)\nUses inverted source color" ,
BlendMode . SrcAlpha => "D3DBLEND_SRCALPHA\nBlend factor is (As, As, As, As)\nUses source alpha for all channels\n Standard transparency (common with InvSrcAlpha for dest)" ,
BlendMode . InvSrcAlpha => "D3DBLEND_INVSRCALPHA\nBlend factor is (1-As, 1-As, 1-As, 1-As)\nUses inverted source alpha\n Standard transparency (common with SrcAlpha for source)" ,
BlendMode . DestAlpha => "D3DBLEND_DESTALPHA\nBlend factor is (Ad, Ad, Ad, Ad)\nUses destination alpha" ,
BlendMode . InvDestAlpha => "D3DBLEND_INVDESTALPHA\nBlend factor is (1-Ad, 1-Ad, 1-Ad, 1-Ad)\nUses inverted destination alpha" ,
BlendMode . DestColor => "D3DBLEND_DESTCOLOR\nBlend factor is (Rd, Gd, Bd, Ad)\nUses destination color" ,
BlendMode . InvDestColor => "D3DBLEND_INVDESTCOLOR\nBlend factor is (1-Rd, 1-Gd, 1-Bd, 1-Ad)\nUses inverted destination color" ,
BlendMode . SrcAlphaSat => "D3DBLEND_SRCALPHASAT\nBlend factor is (f, f, f, 1) where f = min(As, 1-Ad)\nSaturates source alpha" ,
BlendMode . BothSrcAlpha => "D3DBLEND_BOTHSRCALPHA (Obsolete in D3D9+)\nBlend factor is (As, As, As, As) for both source and dest\nSource: (As, As, As, As), Dest: (1-As, 1-As, 1-As, 1-As)" ,
BlendMode . BothInvSrcAlpha => "D3DBLEND_BOTHINVSRCALPHA (Obsolete in D3D9+)\nBlend factor is (1-As, 1-As, 1-As, 1-As) for both source and dest\nSource: (1-As, 1-As, 1-As, 1-As), Dest: (As, As, As, As)" ,
BlendMode . Unknown => "Unknown/Default (0xFF)\nUninitialized or opaque rendering" ,
2025-12-04 03:50:29 +03:00
_ => "Unknown blend mode"
};
}
private static string GetAnimationLoopModeDescription ( AnimationLoopMode mode )
{
return mode switch
{
AnimationLoopMode . Loop => "Loop: Animation repeats continuously from start to end" ,
AnimationLoopMode . PingPong => "PingPong: Animation plays forward then backward repeatedly" ,
AnimationLoopMode . Clamp => "Clamp: Animation plays once and holds at the final value" ,
AnimationLoopMode . Random => "Random: Animation selects random keyframes" ,
_ => "Unknown loop mode"
};
}
}