fpopov thanks

This commit is contained in:
bird_egop
2026-05-09 20:36:43 +03:00
parent d73a6dde70
commit eb458ad9a8
43 changed files with 1562 additions and 1233 deletions
+107 -113
View File
@@ -1,152 +1,148 @@
namespace MaterialLib;
public class MaterialFile
{
// === Metadata (not from file content) ===
public string FileName { get; set; }
public int Version { get; set; } // From NRes ElementCount
public int Magic1 { get; set; } // From NRes Magic1
// === Derived from Version/ElementCount ===
public int MaterialRenderingType { get; set; } // (Version >> 2) & 0xF - 0=Standard, 1=Special, 2=Particle
public bool SupportsBumpMapping { get; set; } // (Version & 2) != 0
public int IsParticleEffect { get; set; } // Version & 40 - 0=Normal, 8=Particle/Effect
// === File Content (in read order) ===
// Read order: StageCount (ushort), AnimCount (ushort), then conditionally blend modes and params
// Global Blend Modes (read if Magic1 >= 2)
public BlendMode SourceBlendMode { get; set; } // Default: Unknown (0xFF)
public BlendMode DestBlendMode { get; set; } // Default: Unknown (0xFF)
// Global Parameters (read if Magic1 > 2 and > 3 respectively)
public float GlobalAlphaMultiplier { get; set; } // Default: 1.0 (always 1.0 in all 628 materials)
public float GlobalEmissiveIntensity { get; set; } // Default: 0.0 (0=no glow, rare values: 1000, 10000)
public List<MaterialStage> Stages { get; set; } = new();
public List<MaterialAnimation> Animations { get; set; } = new();
}
/// <summary>Файл материала MAT0.</summary>
/// <param name="FileName">Имя файла из NRes metadata, не из payload материала.</param>
/// <param name="Version">Значение attr1 / ElementCount из NRes metadata.</param>
/// <param name="Magic1">Значение attr2 / Magic1 из NRes metadata.</param>
/// <param name="MaterialRenderingType">Производное от Version: (Version >> 2) &amp; 0xF. 0 = standard, 1 = special, 2 = particle.</param>
/// <param name="SupportsBumpMapping">Производное от Version: (Version &amp; 2) != 0.</param>
/// <param name="IsParticleEffect">Производное от Version: Version &amp; 40. 0 = normal, 8 = particle/effect.</param>
/// <param name="SourceBlendMode">Глобальный source blend mode; читается, если Magic1 >= 2. Значение по умолчанию: Unknown (0xFF).</param>
/// <param name="DestBlendMode">Глобальный destination blend mode; читается, если Magic1 >= 2. Значение по умолчанию: Unknown (0xFF).</param>
/// <param name="GlobalAlphaMultiplier">Глобальный alpha multiplier; читается, если Magic1 > 2. Значение по умолчанию: 1.0.</param>
/// <param name="GlobalEmissiveIntensity">Глобальная emissive intensity; читается, если Magic1 > 3. Значение по умолчанию: 0.0.</param>
/// <param name="Stages">Стадии материала в порядке чтения из файла.</param>
/// <param name="Animations">Анимации материала в порядке чтения из файла.</param>
public record MaterialFile(
string FileName,
int Version,
int Magic1,
int MaterialRenderingType,
bool SupportsBumpMapping,
int IsParticleEffect,
BlendMode SourceBlendMode,
BlendMode DestBlendMode,
float GlobalAlphaMultiplier,
float GlobalEmissiveIntensity,
List<MaterialStage> Stages,
List<MaterialAnimation> Animations);
/// <summary>
/// Blend modes for material rendering. These control how source and destination colors are combined.
/// Formula: FinalColor = (SourceColor * SourceBlend) [operation] (DestColor * DestBlend)
/// Maps to Direct3D D3DBLEND values.
/// Режимы blend для материала. Управляют смешиванием source и destination цветов.
/// Формула: FinalColor = SourceColor * SourceBlend [operation] DestColor * DestBlend.
/// Соответствуют значениям Direct3D D3DBLEND.
/// </summary>
public enum BlendMode : byte
{
/// <summary>Blend factor is (0, 0, 0, 0) - results in black/transparent</summary>
/// <summary>Blend factor = (0, 0, 0, 0).</summary>
Zero = 1,
/// <summary>Blend factor is (1, 1, 1, 1) - uses full color value</summary>
/// <summary>Blend factor = (1, 1, 1, 1).</summary>
One = 2,
/// <summary>Blend factor is (Rs, Gs, Bs, As) - uses source color</summary>
/// <summary>Blend factor = (Rs, Gs, Bs, As).</summary>
SrcColor = 3,
/// <summary>Blend factor is (1-Rs, 1-Gs, 1-Bs, 1-As) - uses inverted source color</summary>
/// <summary>Blend factor = (1-Rs, 1-Gs, 1-Bs, 1-As).</summary>
InvSrcColor = 4,
/// <summary>Blend factor is (As, As, As, As) - uses source alpha for all channels (standard transparency)</summary>
/// <summary>Blend factor = (As, As, As, As).</summary>
SrcAlpha = 5,
/// <summary>Blend factor is (1-As, 1-As, 1-As, 1-As) - uses inverted source alpha</summary>
/// <summary>Blend factor = (1-As, 1-As, 1-As, 1-As).</summary>
InvSrcAlpha = 6,
/// <summary>Blend factor is (Ad, Ad, Ad, Ad) - uses destination alpha</summary>
/// <summary>Blend factor = (Ad, Ad, Ad, Ad).</summary>
DestAlpha = 7,
/// <summary>Blend factor is (1-Ad, 1-Ad, 1-Ad, 1-Ad) - uses inverted destination alpha</summary>
/// <summary>Blend factor = (1-Ad, 1-Ad, 1-Ad, 1-Ad).</summary>
InvDestAlpha = 8,
/// <summary>Blend factor is (Rd, Gd, Bd, Ad) - uses destination color</summary>
/// <summary>Blend factor = (Rd, Gd, Bd, Ad).</summary>
DestColor = 9,
/// <summary>Blend factor is (1-Rd, 1-Gd, 1-Bd, 1-Ad) - uses inverted destination color</summary>
/// <summary>Blend factor = (1-Rd, 1-Gd, 1-Bd, 1-Ad).</summary>
InvDestColor = 10,
/// <summary>Blend factor is (f, f, f, 1) where f = min(As, 1-Ad) - saturates source alpha</summary>
/// <summary>Blend factor = (f, f, f, 1), где f = min(As, 1-Ad).</summary>
SrcAlphaSat = 11,
/// <summary>Blend factor is (As, As, As, As) for both source and dest - obsolete in D3D9+</summary>
/// <summary>Blend factor = (As, As, As, As) для source и destination; устарело в D3D9+.</summary>
BothSrcAlpha = 12,
/// <summary>Blend factor is (1-As, 1-As, 1-As, 1-As) for both source and dest - obsolete in D3D9+</summary>
/// <summary>Blend factor = (1-As, 1-As, 1-As, 1-As) для source и destination; устарело в D3D9+.</summary>
BothInvSrcAlpha = 13,
/// <summary>Unknown or uninitialized blend mode (0xFF default value)</summary>
/// <summary>Неизвестный или неинициализированный режим blend; значение по умолчанию 0xFF.</summary>
Unknown = 0xFF
}
public class MaterialStage
{
// === FILE READ ORDER (34 bytes per stage) ===
// This matches the order bytes are read from the file (decompiled.c lines 159-217)
// NOT the C struct memory layout (which is Diffuse, Ambient, Specular, Emissive)
// 1. Ambient Color (4 bytes, read first from file)
public float AmbientR;
public float AmbientG;
public float AmbientB;
public float AmbientA; // Scaled by 0.01 when read from file
// 2. Diffuse Color (4 bytes, read second from file)
public float DiffuseR;
public float DiffuseG;
public float DiffuseB;
public float DiffuseA;
/// <summary>Стадия материала. Порядок полей соответствует порядку чтения из файла, а не C struct layout.</summary>
/// <param name="AmbientR">Ambient R, читается первым цветовым блоком.</param>
/// <param name="AmbientG">Ambient G, читается первым цветовым блоком.</param>
/// <param name="AmbientB">Ambient B, читается первым цветовым блоком.</param>
/// <param name="AmbientA">Ambient A, читается первым цветовым блоком и масштабируется на 0.01.</param>
/// <param name="DiffuseR">Diffuse R, читается вторым цветовым блоком.</param>
/// <param name="DiffuseG">Diffuse G, читается вторым цветовым блоком.</param>
/// <param name="DiffuseB">Diffuse B, читается вторым цветовым блоком.</param>
/// <param name="DiffuseA">Diffuse A, читается вторым цветовым блоком.</param>
/// <param name="SpecularR">Specular R, читается третьим цветовым блоком.</param>
/// <param name="SpecularG">Specular G, читается третьим цветовым блоком.</param>
/// <param name="SpecularB">Specular B, читается третьим цветовым блоком.</param>
/// <param name="SpecularA">Specular A, читается третьим цветовым блоком.</param>
/// <param name="EmissiveR">Emissive R, читается четвертым цветовым блоком.</param>
/// <param name="EmissiveG">Emissive G, читается четвертым цветовым блоком.</param>
/// <param name="EmissiveB">Emissive B, читается четвертым цветовым блоком.</param>
/// <param name="EmissiveA">Emissive A, читается четвертым цветовым блоком.</param>
/// <param name="Power">Power, один байт -> float.</param>
/// <param name="TextureStageIndex">Индекс texture stage. 255 = не задано/default, 0..47 = ссылка на texture stage.</param>
/// <param name="TextureName">Имя текстуры, строка из 16 байт.</param>
public record MaterialStage(
float AmbientR,
float AmbientG,
float AmbientB,
float AmbientA,
float DiffuseR,
float DiffuseG,
float DiffuseB,
float DiffuseA,
float SpecularR,
float SpecularG,
float SpecularB,
float SpecularA,
float EmissiveR,
float EmissiveG,
float EmissiveB,
float EmissiveA,
float Power,
int TextureStageIndex,
string TextureName);
// 3. Specular Color (4 bytes, read third from file)
public float SpecularR;
public float SpecularG;
public float SpecularB;
public float SpecularA;
// 4. Emissive Color (4 bytes, read fourth from file)
public float EmissiveR;
public float EmissiveG;
public float EmissiveB;
public float EmissiveA;
// 5. Power (1 byte → float, read fifth from file)
public float Power;
// 6. Texture Stage Index (1 byte, read sixth from file)
// 255 = not set/default, 0-47 = reference to specific texture stage
public int TextureStageIndex;
// 7. Texture Name (16 bytes, read seventh from file)
public string TextureName { get; set; }
}
public class MaterialAnimation
{
// === File Read Order ===
// Combined field (4 bytes): bits 3-31 = Target, bits 0-2 = LoopMode
public AnimationTarget Target;
public AnimationLoopMode LoopMode;
// Key count (2 bytes), then keys
public List<AnimKey> Keys { get; set; } = new();
// Cached description for UI (computed once during parsing)
public string TargetDescription { get; set; } = string.Empty;
}
/// <summary>Анимация материала.</summary>
/// <param name="Target">Целевые компоненты анимации: биты 3..31 combined field.</param>
/// <param name="LoopMode">Режим повтора: биты 0..2 combined field.</param>
/// <param name="Keys">Ключи анимации.</param>
/// <param name="TargetDescription">Кэшированное описание target для UI.</param>
public record MaterialAnimation(
AnimationTarget Target,
AnimationLoopMode LoopMode,
List<AnimKey> Keys,
string TargetDescription);
[Flags]
public enum AnimationTarget : int
{
// NOTE: This is a BITSET (flags enum). Multiple flags can be combined.
// When a flag is SET, that component is INTERPOLATED between stages.
// When a flag is NOT SET, that component is COPIED from the source stage (no interpolation).
// If ALL flags are 0, the ENTIRE stage is copied without any interpolation.
// Это bitset: несколько флагов могут комбинироваться.
// Установленный флаг означает интерполяцию компонента между стадиями.
// Неустановленный флаг означает копирование компонента из исходной стадии.
// Если все флаги равны 0, вся стадия копируется без интерполяции.
Ambient = 1, // 0x01 - Interpolates Ambient RGB (Interpolate.c lines 23-37)
Diffuse = 2, // 0x02 - Interpolates Diffuse RGB (Interpolate.c lines 7-21)
Specular = 4, // 0x04 - Interpolates Specular RGB (Interpolate.c lines 39-53)
Emissive = 8, // 0x08 - Interpolates Emissive RGB (Interpolate.c lines 55-69)
Power = 16 // 0x10 - Interpolates Ambient.A and sets Power (Interpolate.c lines 71-76)
Ambient = 1, // 0x01: интерполирует Ambient RGB.
Diffuse = 2, // 0x02: интерполирует Diffuse RGB.
Specular = 4, // 0x04: интерполирует Specular RGB.
Emissive = 8, // 0x08: интерполирует Emissive RGB.
Power = 16 // 0x10: интерполирует Ambient.A и задает Power.
}
@@ -158,10 +154,8 @@ public enum AnimationLoopMode : int
Random = 3
}
public struct AnimKey
{
// === File Read Order (6 bytes per key) ===
public ushort StageIndex; // Read first
public ushort DurationMs; // Read second
public ushort InterpolationCurve; // Read third - Always 0 (linear interpolation) in all 1848 keys
}
/// <summary>Ключ анимации материала (length = 6).</summary>
/// <param name="StageIndex">[0x00..0x02] Индекс стадии материала.</param>
/// <param name="DurationMs">[0x02..0x04] Длительность в миллисекундах.</param>
/// <param name="InterpolationCurve">[0x04..0x06] Кривая интерполяции. В исследованных данных всегда 0.</param>
public readonly record struct AnimKey(ushort StageIndex, ushort DurationMs, ushort InterpolationCurve);
+88 -69
View File
@@ -7,17 +7,9 @@ public static class MaterialParser
{
public static MaterialFile ReadFromStream(Stream fs, string fileName, int elementCount, int magic1)
{
var file = new MaterialFile
{
FileName = fileName,
Version = elementCount,
Magic1 = magic1
};
// Derived fields
file.MaterialRenderingType = elementCount >> 2 & 0xf;
file.SupportsBumpMapping = (elementCount & 2) != 0;
file.IsParticleEffect = elementCount & 40;
var materialRenderingType = elementCount >> 2 & 0xf;
var supportsBumpMapping = (elementCount & 2) != 0;
var isParticleEffect = elementCount & 40;
// Reading content
var stageCount = fs.ReadUInt16LittleEndian();
@@ -25,113 +17,140 @@ public static class MaterialParser
uint magic = (uint)magic1;
// Defaults found in C code
file.GlobalAlphaMultiplier = 1.0f; // field8_0x15c
file.GlobalEmissiveIntensity = 0.0f; // field9_0x160
file.SourceBlendMode = BlendMode.Unknown; // field6_0x154
file.DestBlendMode = BlendMode.Unknown; // field7_0x158
// Значения по умолчанию из C-кода.
var globalAlphaMultiplier = 1.0f; // field8_0x15c
var globalEmissiveIntensity = 0.0f; // field9_0x160
var sourceBlendMode = BlendMode.Unknown; // field6_0x154
var destBlendMode = BlendMode.Unknown; // field7_0x158
if (magic >= 2)
{
file.SourceBlendMode = (BlendMode)fs.ReadByte();
file.DestBlendMode = (BlendMode)fs.ReadByte();
sourceBlendMode = (BlendMode)fs.ReadByte();
destBlendMode = (BlendMode)fs.ReadByte();
}
if (magic > 2)
{
file.GlobalAlphaMultiplier = fs.ReadFloatLittleEndian();
globalAlphaMultiplier = fs.ReadFloatLittleEndian();
}
if (magic > 3)
{
file.GlobalEmissiveIntensity = fs.ReadFloatLittleEndian();
globalEmissiveIntensity = fs.ReadFloatLittleEndian();
}
// --- 2. Material Stages ---
// Стадии материала.
const float Inv255 = 1.0f / 255.0f;
const float Field7Mult = 0.01f;
Span<byte> textureNameBuffer = stackalloc byte[16];
List<MaterialStage> stages = [];
for (int i = 0; i < stageCount; i++)
{
var stage = new MaterialStage();
// === FILE READ ORDER (matches decompiled.c lines 159-217) ===
// Порядок чтения соответствует файлу, а не C struct layout.
// 1. Ambient (4 bytes, A scaled by 0.01) - Lines 159-168
stage.AmbientR = fs.ReadByte() * Inv255;
stage.AmbientG = fs.ReadByte() * Inv255;
stage.AmbientB = fs.ReadByte() * Inv255;
stage.AmbientA = fs.ReadByte() * Field7Mult; // 0.01 scaling
// Ambient: 4 байта, A масштабируется на 0.01.
var ambientR = fs.ReadByte() * Inv255;
var ambientG = fs.ReadByte() * Inv255;
var ambientB = fs.ReadByte() * Inv255;
var ambientA = fs.ReadByte() * Field7Mult; // 0.01 scaling
// 2. Diffuse (4 bytes) - Lines 171-180
stage.DiffuseR = fs.ReadByte() * Inv255;
stage.DiffuseG = fs.ReadByte() * Inv255;
stage.DiffuseB = fs.ReadByte() * Inv255;
stage.DiffuseA = fs.ReadByte() * Inv255;
// Diffuse: 4 байта.
var diffuseR = fs.ReadByte() * Inv255;
var diffuseG = fs.ReadByte() * Inv255;
var diffuseB = fs.ReadByte() * Inv255;
var diffuseA = fs.ReadByte() * Inv255;
// 3. Specular (4 bytes) - Lines 183-192
stage.SpecularR = fs.ReadByte() * Inv255;
stage.SpecularG = fs.ReadByte() * Inv255;
stage.SpecularB = fs.ReadByte() * Inv255;
stage.SpecularA = fs.ReadByte() * Inv255;
// Specular: 4 байта.
var specularR = fs.ReadByte() * Inv255;
var specularG = fs.ReadByte() * Inv255;
var specularB = fs.ReadByte() * Inv255;
var specularA = fs.ReadByte() * Inv255;
// 4. Emissive (4 bytes) - Lines 195-204
stage.EmissiveR = fs.ReadByte() * Inv255;
stage.EmissiveG = fs.ReadByte() * Inv255;
stage.EmissiveB = fs.ReadByte() * Inv255;
stage.EmissiveA = fs.ReadByte() * Inv255;
// Emissive: 4 байта.
var emissiveR = fs.ReadByte() * Inv255;
var emissiveG = fs.ReadByte() * Inv255;
var emissiveB = fs.ReadByte() * Inv255;
var emissiveA = fs.ReadByte() * Inv255;
// 5. Power (1 byte → float) - Line 207
stage.Power = (float)fs.ReadByte();
// Power: 1 байт -> float.
var power = (float)fs.ReadByte();
// 6. Texture Stage Index (1 byte) - Line 210
stage.TextureStageIndex = fs.ReadByte();
// Texture stage index: 1 байт.
var textureStageIndex = fs.ReadByte();
// 7. Texture Name (16 bytes) - Lines 212-217
// Texture name: 16 байт.
textureNameBuffer.Clear();
fs.ReadExactly(textureNameBuffer);
stage.TextureName = Encoding.ASCII.GetString(textureNameBuffer).TrimEnd('\0');
var textureName = Encoding.ASCII.GetString(textureNameBuffer).TrimEnd('\0');
file.Stages.Add(stage);
stages.Add(new MaterialStage(
ambientR,
ambientG,
ambientB,
ambientA,
diffuseR,
diffuseG,
diffuseB,
diffuseA,
specularR,
specularG,
specularB,
specularA,
emissiveR,
emissiveG,
emissiveB,
emissiveA,
power,
textureStageIndex,
textureName));
}
// --- 3. Animations ---
// Анимации.
List<MaterialAnimation> animations = [];
for (int i = 0; i < animCount; i++)
{
var anim = new MaterialAnimation();
uint typeAndParams = fs.ReadUInt32LittleEndian();
anim.Target = (AnimationTarget)(typeAndParams >> 3);
anim.LoopMode = (AnimationLoopMode)(typeAndParams & 7);
var target = (AnimationTarget)(typeAndParams >> 3);
var loopMode = (AnimationLoopMode)(typeAndParams & 7);
ushort keyCount = fs.ReadUInt16LittleEndian();
List<AnimKey> keys = [];
for (int k = 0; k < keyCount; k++)
{
var key = new AnimKey
{
StageIndex = fs.ReadUInt16LittleEndian(),
DurationMs = fs.ReadUInt16LittleEndian(),
InterpolationCurve = fs.ReadUInt16LittleEndian()
};
anim.Keys.Add(key);
keys.Add(new AnimKey(
fs.ReadUInt16LittleEndian(),
fs.ReadUInt16LittleEndian(),
fs.ReadUInt16LittleEndian()));
}
// Precompute description for UI to avoid per-frame allocations
anim.TargetDescription = ComputeTargetDescription(anim.Target);
// Описание вычисляется один раз при парсинге, чтобы UI не выделял память каждый кадр.
var targetDescription = ComputeTargetDescription(target);
file.Animations.Add(anim);
animations.Add(new MaterialAnimation(target, loopMode, keys, targetDescription));
}
return file;
return new MaterialFile(
fileName,
elementCount,
magic1,
materialRenderingType,
supportsBumpMapping,
isParticleEffect,
sourceBlendMode,
destBlendMode,
globalAlphaMultiplier,
globalEmissiveIntensity,
stages,
animations);
}
private static string ComputeTargetDescription(AnimationTarget target)
{
// Precompute the description once during parsing
if ((int)target == 0)
return "No interpolation - entire stage is copied as-is (Flags: 0x0)";
return "Без интерполяции - вся стадия копируется как есть (Flags: 0x0)";
var parts = new List<string>();
@@ -146,7 +165,7 @@ public static class MaterialParser
if ((target & AnimationTarget.Power) != 0)
parts.Add("Ambient.A + Power");
return $"Interpolates: {string.Join(", ", parts)} | Other components copied (Flags: 0x{(int)target:X})";
return $"Интерполируется: {string.Join(", ", parts)} | Остальные компоненты копируются (Flags: 0x{(int)target:X})";
}
}