2025-12-04 03:50:29 +03:00
|
|
|
using System.Buffers.Binary;
|
|
|
|
|
using System.Text;
|
2026-06-06 23:25:14 +03:00
|
|
|
using Common;
|
2025-12-04 03:50:29 +03:00
|
|
|
|
|
|
|
|
namespace MaterialLib;
|
|
|
|
|
|
|
|
|
|
public static class MaterialParser
|
|
|
|
|
{
|
2026-06-06 21:26:56 +03:00
|
|
|
public static MaterialFile ReadFromStream(Stream fs, string fileName, uint elementCount, int magic1)
|
2025-12-04 03:50:29 +03:00
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
var materialRenderingType = elementCount >> 2 & 0xf;
|
|
|
|
|
var supportsBumpMapping = (elementCount & 2) != 0;
|
|
|
|
|
var isParticleEffect = elementCount & 40;
|
2025-12-04 03:50:29 +03:00
|
|
|
|
|
|
|
|
// Reading content
|
|
|
|
|
var stageCount = fs.ReadUInt16LittleEndian();
|
|
|
|
|
var animCount = fs.ReadUInt16LittleEndian();
|
|
|
|
|
|
|
|
|
|
uint magic = (uint)magic1;
|
|
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
// Значения по умолчанию из 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
|
2025-12-04 03:50:29 +03:00
|
|
|
|
|
|
|
|
if (magic >= 2)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
sourceBlendMode = (BlendMode)fs.ReadByte();
|
|
|
|
|
destBlendMode = (BlendMode)fs.ReadByte();
|
2025-12-04 03:50:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (magic > 2)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
globalAlphaMultiplier = fs.ReadFloatLittleEndian();
|
2025-12-04 03:50:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (magic > 3)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
globalEmissiveIntensity = fs.ReadFloatLittleEndian();
|
2025-12-04 03:50:29 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
// Стадии материала.
|
2025-12-04 03:50:29 +03:00
|
|
|
const float Inv255 = 1.0f / 255.0f;
|
|
|
|
|
const float Field7Mult = 0.01f;
|
|
|
|
|
Span<byte> textureNameBuffer = stackalloc byte[16];
|
2026-05-09 20:36:43 +03:00
|
|
|
List<MaterialStage> stages = [];
|
2025-12-04 03:50:29 +03:00
|
|
|
|
|
|
|
|
for (int i = 0; i < stageCount; i++)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
// Порядок чтения соответствует файлу, а не C struct layout.
|
2025-12-04 03:50:29 +03:00
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
// Diffuse: 4 байта.
|
|
|
|
|
var diffuseR = fs.ReadByte() * Inv255;
|
|
|
|
|
var diffuseG = fs.ReadByte() * Inv255;
|
|
|
|
|
var diffuseB = fs.ReadByte() * Inv255;
|
|
|
|
|
var diffuseA = 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;
|
|
|
|
|
|
|
|
|
|
// Emissive: 4 байта.
|
|
|
|
|
var emissiveR = fs.ReadByte() * Inv255;
|
|
|
|
|
var emissiveG = fs.ReadByte() * Inv255;
|
|
|
|
|
var emissiveB = fs.ReadByte() * Inv255;
|
|
|
|
|
var emissiveA = fs.ReadByte() * Inv255;
|
|
|
|
|
|
|
|
|
|
// Power: 1 байт -> float.
|
|
|
|
|
var power = (float)fs.ReadByte();
|
2025-12-04 03:50:29 +03:00
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
// Texture stage index: 1 байт.
|
|
|
|
|
var textureStageIndex = fs.ReadByte();
|
2025-12-04 03:50:29 +03:00
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
// Texture name: 16 байт.
|
2025-12-04 03:50:29 +03:00
|
|
|
textureNameBuffer.Clear();
|
|
|
|
|
fs.ReadExactly(textureNameBuffer);
|
2026-06-07 15:55:56 +03:00
|
|
|
var terminator = textureNameBuffer.IndexOf((byte)0);
|
|
|
|
|
if (terminator == -1)
|
|
|
|
|
{
|
|
|
|
|
terminator = textureNameBuffer.Length - 1;
|
|
|
|
|
}
|
|
|
|
|
var textureName = Encoding.ASCII.GetString(textureNameBuffer[..terminator]);
|
2026-05-09 20:36:43 +03:00
|
|
|
|
|
|
|
|
stages.Add(new MaterialStage(
|
|
|
|
|
ambientR,
|
|
|
|
|
ambientG,
|
|
|
|
|
ambientB,
|
|
|
|
|
ambientA,
|
|
|
|
|
diffuseR,
|
|
|
|
|
diffuseG,
|
|
|
|
|
diffuseB,
|
|
|
|
|
diffuseA,
|
|
|
|
|
specularR,
|
|
|
|
|
specularG,
|
|
|
|
|
specularB,
|
|
|
|
|
specularA,
|
|
|
|
|
emissiveR,
|
|
|
|
|
emissiveG,
|
|
|
|
|
emissiveB,
|
|
|
|
|
emissiveA,
|
|
|
|
|
power,
|
|
|
|
|
textureStageIndex,
|
|
|
|
|
textureName));
|
2025-12-04 03:50:29 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
// Анимации.
|
|
|
|
|
List<MaterialAnimation> animations = [];
|
2025-12-04 03:50:29 +03:00
|
|
|
for (int i = 0; i < animCount; i++)
|
|
|
|
|
{
|
|
|
|
|
uint typeAndParams = fs.ReadUInt32LittleEndian();
|
2026-05-09 20:36:43 +03:00
|
|
|
var target = (AnimationTarget)(typeAndParams >> 3);
|
|
|
|
|
var loopMode = (AnimationLoopMode)(typeAndParams & 7);
|
2025-12-04 03:50:29 +03:00
|
|
|
|
|
|
|
|
ushort keyCount = fs.ReadUInt16LittleEndian();
|
2026-05-09 20:36:43 +03:00
|
|
|
List<AnimKey> keys = [];
|
2025-12-04 03:50:29 +03:00
|
|
|
|
|
|
|
|
for (int k = 0; k < keyCount; k++)
|
|
|
|
|
{
|
2026-05-09 20:36:43 +03:00
|
|
|
keys.Add(new AnimKey(
|
|
|
|
|
fs.ReadUInt16LittleEndian(),
|
|
|
|
|
fs.ReadUInt16LittleEndian(),
|
|
|
|
|
fs.ReadUInt16LittleEndian()));
|
2025-12-04 03:50:29 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
// Описание вычисляется один раз при парсинге, чтобы UI не выделял память каждый кадр.
|
|
|
|
|
var targetDescription = ComputeTargetDescription(target);
|
2025-12-04 03:50:29 +03:00
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
animations.Add(new MaterialAnimation(target, loopMode, keys, targetDescription));
|
2025-12-04 03:50:29 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
return new MaterialFile(
|
|
|
|
|
fileName,
|
|
|
|
|
elementCount,
|
|
|
|
|
magic1,
|
|
|
|
|
materialRenderingType,
|
|
|
|
|
supportsBumpMapping,
|
|
|
|
|
isParticleEffect,
|
|
|
|
|
sourceBlendMode,
|
|
|
|
|
destBlendMode,
|
|
|
|
|
globalAlphaMultiplier,
|
|
|
|
|
globalEmissiveIntensity,
|
|
|
|
|
stages,
|
|
|
|
|
animations);
|
2025-12-04 03:50:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ComputeTargetDescription(AnimationTarget target)
|
|
|
|
|
{
|
|
|
|
|
// Precompute the description once during parsing
|
|
|
|
|
if ((int)target == 0)
|
2026-05-09 20:36:43 +03:00
|
|
|
return "Без интерполяции - вся стадия копируется как есть (Flags: 0x0)";
|
2025-12-04 03:50:29 +03:00
|
|
|
|
|
|
|
|
var parts = new List<string>();
|
|
|
|
|
|
|
|
|
|
if ((target & AnimationTarget.Ambient) != 0)
|
|
|
|
|
parts.Add("Ambient RGB");
|
|
|
|
|
if ((target & AnimationTarget.Diffuse) != 0)
|
|
|
|
|
parts.Add("Diffuse RGB");
|
|
|
|
|
if ((target & AnimationTarget.Specular) != 0)
|
|
|
|
|
parts.Add("Specular RGB");
|
|
|
|
|
if ((target & AnimationTarget.Emissive) != 0)
|
|
|
|
|
parts.Add("Emissive RGB");
|
|
|
|
|
if ((target & AnimationTarget.Power) != 0)
|
|
|
|
|
parts.Add("Ambient.A + Power");
|
|
|
|
|
|
2026-05-09 20:36:43 +03:00
|
|
|
return $"Интерполируется: {string.Join(", ", parts)} | Остальные компоненты копируются (Flags: 0x{(int)target:X})";
|
2025-12-04 03:50:29 +03:00
|
|
|
}
|
|
|
|
|
}
|