Editor/HumanoidRetargeter/Embedded/ValveResourceFormat/Resource/ResourceTypes/Mesh.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using HumanoidRetargeterVrf.Utils;
using System.IO;
using HumanoidRetargeterVrf.Blocks;
using HumanoidRetargeterVrf.IO;
using HumanoidRetargeterVrf.ResourceTypes.ModelData;
using HumanoidRetargeterVrf.ResourceTypes.ModelData.Attachments;
using HumanoidRetargeterVrf.Serialization.KeyValues;
#nullable disable
namespace HumanoidRetargeterVrf.ResourceTypes
{
/// <summary>
/// Represents a mesh resource containing geometry and vertex buffer data.
/// </summary>
public class Mesh : KeyValuesOrNTRO
{
/// <summary>
/// Gets or sets the mesh's vertex/index buffer block (VBIB).
/// </summary>
public VBIB VBIB
{
get
{
//new format has VBIB block, for old format we can get it from NTRO DATA block
cachedVBIB ??= (VBIB)Resource.GetBlockByType(BlockType.VBIB) ?? new VBIB(Resource, Data);
return cachedVBIB;
}
set
{
cachedVBIB = value;
}
}
/// <summary>
/// Gets or sets the mesh name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets the minimum bounds of the mesh.
/// </summary>
public global::System.Numerics.Vector3 MinBounds { get; private set; }
/// <summary>
/// Gets the maximum bounds of the mesh.
/// </summary>
public global::System.Numerics.Vector3 MaxBounds { get; private set; }
/// <summary>
/// Gets or sets the morph data for this mesh.
/// </summary>
public Morph MorphData { get; set; }
private VBIB cachedVBIB { get; set; }
/// <summary>
/// Gets the attachments associated with this mesh.
/// </summary>
public Dictionary<string, Attachment> Attachments { get; init; } = [];
/// <summary>
/// Gets the hitbox sets associated with this mesh.
/// </summary>
public Dictionary<string, Hitbox[]> HitboxSets { get; init; } = [];
/// <summary>
/// Initializes a new instance of the <see cref="Mesh"/> class.
/// </summary>
/// <param name="type">The block type.</param>
public Mesh(BlockType type) : base(type, "PermRenderMeshData_t")
{
}
/// <inheritdoc/>
public override void Read(BinaryReader reader)
{
base.Read(reader);
Name = Resource.FileName;
if (Data.ContainsKey("m_attachments"))
{
var attachmentsData = Data.GetArray("m_attachments");
for (var i = 0; i < attachmentsData.Length; i++)
{
var attachment = new Attachment(attachmentsData[i]);
Attachments.Add(attachment.Name, attachment);
}
}
if (Data.ContainsKey("m_hitboxsets"))
{
var hitboxSetsData = Data.GetArray("m_hitboxsets");
for (var i = 0; i < hitboxSetsData.Length; i++)
{
var hitboxSet = hitboxSetsData[i].GetSubCollection("value") ?? hitboxSetsData[i];
var hitboxSetName = hitboxSet.GetStringProperty("m_name");
var hitboxesKey = hitboxSet.ContainsKey("m_HitBoxes") ? "m_HitBoxes" : "m_hitboxes";
var hitboxes = hitboxSet.GetArray(hitboxesKey, d => new Hitbox(d));
HitboxSets.Add(hitboxSetName, hitboxes);
}
}
}
/// <summary>
/// Calculates and sets the bounding box from scene objects.
/// </summary>
public void GetBounds()
{
var sceneObjects = Data.GetArray("m_sceneObjects");
if (sceneObjects.Length == 0)
{
MinBounds = MaxBounds = new global::System.Numerics.Vector3(0, 0, 0);
return;
}
var minBounds = sceneObjects[0].GetSubCollection("m_vMinBounds").ToVector3();
var maxBounds = sceneObjects[0].GetSubCollection("m_vMaxBounds").ToVector3();
for (var i = 1; i < sceneObjects.Length; ++i)
{
var localMin = sceneObjects[i].GetSubCollection("m_vMinBounds").ToVector3();
var localMax = sceneObjects[i].GetSubCollection("m_vMaxBounds").ToVector3();
minBounds.X = Math.Min(minBounds.X, localMin.X);
minBounds.Y = Math.Min(minBounds.Y, localMin.Y);
minBounds.Z = Math.Min(minBounds.Z, localMin.Z);
maxBounds.X = Math.Max(maxBounds.X, localMax.X);
maxBounds.Y = Math.Max(maxBounds.Y, localMax.Y);
maxBounds.Z = Math.Max(maxBounds.Z, localMax.Z);
}
MinBounds = minBounds;
MaxBounds = maxBounds;
}
/// <summary>
/// Determines if compressed normal tangent is enabled for the draw call.
/// </summary>
/// <param name="drawCall">The draw call data.</param>
/// <returns>True if compressed normal tangent is used.</returns>
public static bool IsCompressedNormalTangent(KVObject drawCall)
{
if (drawCall.ContainsKey("m_bUseCompressedNormalTangent"))
{
return drawCall.GetProperty<bool>("m_bUseCompressedNormalTangent");
}
if (!drawCall.ContainsKey("m_nFlags"))
{
return false;
}
var flags = drawCall.GetProperty<object>("m_nFlags");
return flags switch
{
string flagsString => flagsString.Contains("MESH_DRAW_FLAGS_USE_COMPRESSED_NORMAL_TANGENT", StringComparison.InvariantCulture),
long flagsLong => ((RenderMeshDrawPrimitiveFlags)flagsLong & RenderMeshDrawPrimitiveFlags.UseCompressedNormalTangent) != 0,
byte flagsByte => ((RenderMeshDrawPrimitiveFlags)flagsByte & RenderMeshDrawPrimitiveFlags.UseCompressedNormalTangent) != 0,
_ => false
};
}
/// <summary>
/// Determines if the draw call has baked lighting from lightmap.
/// </summary>
/// <param name="drawCall">The draw call data.</param>
/// <returns>True if baked lighting from lightmap is present.</returns>
public static bool HasBakedLightingFromLightMap(KVObject drawCall)
=> drawCall.ContainsKey("m_bHasBakedLightingFromLightMap")
&& drawCall.GetProperty<bool>("m_bHasBakedLightingFromLightMap");
/// <summary>
/// Determines if the draw call has baked lighting from vertex stream.
/// </summary>
/// <param name="drawCall">The draw call data.</param>
/// <returns>True if baked lighting from vertex stream is present.</returns>
public static bool HasBakedLightingFromVertexStream(KVObject drawCall)
=> drawCall.ContainsKey("m_bHasBakedLightingFromVertexStream")
&& drawCall.GetProperty<bool>("m_bHasBakedLightingFromVertexStream");
/// <summary>
/// Determines if the draw call is an occluder.
/// </summary>
/// <param name="drawCall">The draw call data.</param>
/// <returns>True if the draw call is an occluder.</returns>
public static bool IsOccluder(KVObject drawCall)
=> drawCall.ContainsKey("m_bIsOccluder")
&& drawCall.GetProperty<bool>("m_bIsOccluder");
/// <summary>
/// Loads external morph data from the file loader.
/// </summary>
/// <param name="fileLoader">The file loader to use.</param>
}
}