459 lines
14 KiB
C#
459 lines
14 KiB
C#
using Godot;
|
|
using KeepersCompound.LGS;
|
|
using KeepersCompound.LGS.Database;
|
|
using KeepersCompound.LGS.Database.Chunks;
|
|
using KeepersCompound.TMV.UI;
|
|
using RectpackSharp;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
|
|
namespace KeepersCompound.TMV;
|
|
|
|
public partial class Mission : Node3D
|
|
{
|
|
private readonly struct LightmapRectData
|
|
{
|
|
public readonly int cellIndex;
|
|
public readonly int lightmapIndex;
|
|
public readonly int textureId;
|
|
public readonly int uvStart;
|
|
public readonly int uvEnd;
|
|
|
|
public LightmapRectData(int cellIndex, int lightmapIndex, int textureId, int uvStart, int uvEnd)
|
|
{
|
|
this.cellIndex = cellIndex;
|
|
this.lightmapIndex = lightmapIndex;
|
|
this.textureId = textureId;
|
|
this.uvStart = uvStart;
|
|
this.uvEnd = uvEnd;
|
|
}
|
|
}
|
|
|
|
[Export(PropertyHint.GlobalFile, "*.mis")]
|
|
public string FileName { get; set; }
|
|
[Export]
|
|
public bool Build = false;
|
|
[Export]
|
|
public bool Clear = false;
|
|
[Export]
|
|
public bool Dump = false;
|
|
|
|
string _campaignName;
|
|
string _missionName;
|
|
ResourcePathManager _installPaths;
|
|
DbFile _file;
|
|
TextureLoader _textureLoader;
|
|
ModelLoader _modelLoader;
|
|
|
|
public override void _Ready()
|
|
{
|
|
var extractPath = ProjectSettings.GlobalizePath($"user://extracted/tmp");
|
|
_installPaths = new ResourcePathManager(extractPath);
|
|
_modelLoader = new ModelLoader(_installPaths);
|
|
var missionSelector = GetNode<Control>("%MissionSelector") as MissionSelector;
|
|
missionSelector.pathManager = _installPaths;
|
|
missionSelector.MissionSelected += (string campaign, string mission) =>
|
|
{
|
|
_campaignName = campaign;
|
|
_missionName = mission;
|
|
FileName = _installPaths.GetMissionPath(campaign, mission);
|
|
Build = true;
|
|
};
|
|
}
|
|
|
|
public override void _Process(double delta)
|
|
{
|
|
if (Build)
|
|
{
|
|
Timing.TimeStage("Build", () => RebuildMap());
|
|
Build = false;
|
|
}
|
|
if (Clear)
|
|
{
|
|
ClearMap();
|
|
Clear = false;
|
|
}
|
|
}
|
|
|
|
public override void _Input(InputEvent @event)
|
|
{
|
|
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
|
|
{
|
|
if (keyEvent.Keycode == Key.R)
|
|
{
|
|
Build = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ClearMap()
|
|
{
|
|
foreach (var node in GetChildren())
|
|
{
|
|
node.QueueFree();
|
|
}
|
|
}
|
|
|
|
public void RebuildMap()
|
|
{
|
|
ClearMap();
|
|
|
|
_textureLoader = new TextureLoader(_campaignName);
|
|
Timing.TimeStage("DbFile Parse", () => _file = new(FileName));
|
|
Timing.TimeStage("Load FAM", () => UseChunk<TxList>("TXLIST", LoadTextures));
|
|
Timing.TimeStage("Build WR", () => UseChunk<WorldRep>("WREXT", BuildWrMeshes));
|
|
|
|
if (_file.Chunks.TryGetValue("BRLIST", out var brList))
|
|
{
|
|
var objHierarchy = Timing.TimeStage("Hierarchy", BuildHierarchy);
|
|
Timing.TimeStage("Object Placement", () => PlaceObjects((BrList)brList, objHierarchy));
|
|
}
|
|
}
|
|
|
|
// TODO: Make this less of a mess
|
|
private ObjectHierarchy BuildHierarchy()
|
|
{
|
|
ObjectHierarchy objHierarchy;
|
|
if (_file.Chunks.TryGetValue("GAM_FILE", out var gamFileChunk))
|
|
{
|
|
|
|
var options = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive };
|
|
var name = ((GamFile)gamFileChunk).fileName;
|
|
var paths = Directory.GetFiles(FileName.GetBaseDir(), name, options);
|
|
if (!paths.IsEmpty())
|
|
{
|
|
objHierarchy = new ObjectHierarchy(_file, new DbFile(paths[0]));
|
|
}
|
|
else
|
|
{
|
|
objHierarchy = new ObjectHierarchy(_file);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
objHierarchy = new ObjectHierarchy(_file);
|
|
}
|
|
return objHierarchy;
|
|
}
|
|
|
|
private void UseChunk<T>(string name, Action<T> action)
|
|
{
|
|
if (_file.Chunks.TryGetValue(name, out var value))
|
|
{
|
|
action((T)value);
|
|
}
|
|
else
|
|
{
|
|
GD.Print($"No chunk found/loaded: {name}");
|
|
}
|
|
}
|
|
|
|
private void PlaceObjects(BrList brList, ObjectHierarchy objHierarchy)
|
|
{
|
|
foreach (var brush in brList.Brushes)
|
|
{
|
|
if (brush.media != BrList.Brush.Media.Object)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var id = (int)brush.brushInfo;
|
|
var modelNameProp = objHierarchy.GetProperty<PropModelName>(id, "P$ModelName");
|
|
var scaleProp = objHierarchy.GetProperty<PropScale>(id, "P$Scale");
|
|
var renderTypeProp = objHierarchy.GetProperty<PropRenderType>(id, "P$RenderTyp");
|
|
var renderMode = renderTypeProp == null ? PropRenderType.Mode.Normal : renderTypeProp.mode;
|
|
|
|
if (modelNameProp == null || renderMode == PropRenderType.Mode.NotRendered)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Let's try and place an object :)
|
|
var modelName = modelNameProp.modelName + ".bin";
|
|
var pos = brush.position.ToGodotVec3();
|
|
var rawRot = brush.angle;
|
|
var rot = new Vector3(rawRot.Y, rawRot.Z, rawRot.X) * 360 / ushort.MaxValue;
|
|
var scale = scaleProp == null ? Vector3.One : scaleProp.scale.ToGodotVec3(false);
|
|
var model = _modelLoader.Load(_campaignName, modelName);
|
|
if (model != null)
|
|
{
|
|
model.Position = pos;
|
|
model.RotationDegrees = rot;
|
|
model.Scale = scale;
|
|
AddChild(model);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void BuildWrMeshes(WorldRep worldRep)
|
|
{
|
|
var cells = worldRep.Cells;
|
|
var lmHdr = worldRep.DataHeader.LightmapFormat == 2;
|
|
GD.Print($"HDR Lightmap: {lmHdr}");
|
|
|
|
var packingRects = new List<PackingRectangle>();
|
|
var surfaceDataMap = new Dictionary<int, MeshSurfaceData>();
|
|
var rectDataMap = new Dictionary<int, LightmapRectData>();
|
|
|
|
for (var cellIdx = 0; cellIdx < cells.Length; cellIdx++)
|
|
{
|
|
var cell = cells[cellIdx];
|
|
var numPolys = cell.PolyCount;
|
|
var numRenderPolys = cell.RenderPolyCount;
|
|
var numPortalPolys = cell.PortalPolyCount;
|
|
|
|
// There's nothing to render
|
|
if (numRenderPolys == 0 || numPortalPolys >= numPolys)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// You'd think these would be the same number, but apparently not
|
|
// I think it's because water counts as a render poly and a portal poly
|
|
var maxPolyIdx = Math.Min(numRenderPolys, numPolys - numPortalPolys);
|
|
var cellIdxOffset = 0;
|
|
for (int polyIdx = 0; polyIdx < maxPolyIdx; polyIdx++)
|
|
{
|
|
var lightmapRectData = ProcessCellSurfaceData(surfaceDataMap, worldRep, cellIdx, polyIdx, cellIdxOffset);
|
|
rectDataMap.Add(packingRects.Count, lightmapRectData);
|
|
|
|
var light = cell.LightList[polyIdx];
|
|
var rect = new PackingRectangle(0, 0, light.Width, light.Height, packingRects.Count);
|
|
packingRects.Add(rect);
|
|
|
|
cellIdxOffset += cell.Polys[polyIdx].VertexCount;
|
|
}
|
|
}
|
|
|
|
var lightmapTexture = Timing.TimeStage("Build Lightmap", () =>
|
|
{
|
|
return BuildLightmapTexture(cells, packingRects.ToArray(), rectDataMap, surfaceDataMap);
|
|
});
|
|
|
|
foreach (var (textureId, surface) in surfaceDataMap)
|
|
{
|
|
if (surface.Empty)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var albedoTexture = _textureLoader.Get(textureId);
|
|
|
|
var mesh = new ArrayMesh();
|
|
mesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, surface.BuildSurfaceArray());
|
|
mesh.SurfaceSetMaterial(0, BuildMaterial(albedoTexture, lightmapTexture, lmHdr));
|
|
|
|
var meshInstance = new MeshInstance3D { Mesh = mesh };
|
|
AddChild(meshInstance);
|
|
}
|
|
}
|
|
|
|
private LightmapRectData ProcessCellSurfaceData(Dictionary<int, MeshSurfaceData> surfaceDataMap, WorldRep worldRep, int cellIdx, int polyIdx, int indicesOffset)
|
|
{
|
|
var cell = worldRep.Cells[cellIdx];
|
|
var poly = cell.Polys[polyIdx];
|
|
var normal = cell.Planes[poly.PlaneId].Normal.ToGodotVec3();
|
|
var vertices = new List<Vector3>();
|
|
var textureUvs = new List<Vector2>();
|
|
var lightmapUvs = new List<Vector2>();
|
|
|
|
var numPolyVertices = poly.VertexCount;
|
|
for (var j = 0; j < numPolyVertices; j++)
|
|
{
|
|
var vertex = cell.Vertices[cell.Indices[indicesOffset + j]];
|
|
vertices.Add(vertex.ToGodotVec3());
|
|
}
|
|
|
|
var renderPoly = cell.RenderPolys[polyIdx];
|
|
var light = cell.LightList[polyIdx];
|
|
var lightmapScale = worldRep.DataHeader.LightmapScaleMultiplier();
|
|
var textureId = CalcBaseUV(cell, poly, renderPoly, light, textureUvs, lightmapUvs, lightmapScale, indicesOffset);
|
|
|
|
if (!surfaceDataMap.ContainsKey(textureId))
|
|
{
|
|
surfaceDataMap.Add(textureId, new MeshSurfaceData());
|
|
}
|
|
var surfaceData = surfaceDataMap[textureId];
|
|
var (start, end) = surfaceData.AddPolygon(vertices, normal, textureUvs, lightmapUvs);
|
|
|
|
if (polyIdx >= cell.Lightmaps.Length) GD.Print("HUH");
|
|
|
|
return new LightmapRectData(cellIdx, polyIdx, textureId, start, end);
|
|
}
|
|
|
|
private static Texture BuildLightmapTexture(WorldRep.Cell[] cells, PackingRectangle[] packingRects, Dictionary<int, LightmapRectData> rectDataMap, Dictionary<int, MeshSurfaceData> surfaceDataMap)
|
|
{
|
|
var bounds = Timing.TimeStage("RectPack", () =>
|
|
{
|
|
RectanglePacker.Pack(packingRects, out var bounds, PackingHints.FindBest, 0);
|
|
return bounds;
|
|
});
|
|
GD.Print($"Creating lightmap with bounds: ({bounds.Width}, {bounds.Height})");
|
|
var image = Image.CreateEmpty((int)bounds.Width, (int)bounds.Height, false, Image.Format.Rgba8);
|
|
foreach (var rect in packingRects)
|
|
{
|
|
if (!rectDataMap.ContainsKey(rect.Id)) GD.Print("Invalid rectDataMap key");
|
|
var info = rectDataMap[rect.Id];
|
|
|
|
if (info.cellIndex >= cells.Length) GD.Print($"CellIndex too big: {info.cellIndex}/{cells.Length}");
|
|
if (info.lightmapIndex >= cells[info.cellIndex].Lightmaps.Length) GD.Print($"LightmapIndex too big: {info.lightmapIndex}/{cells[info.cellIndex].Lightmaps.Length}");
|
|
var lightmap = cells[info.cellIndex].Lightmaps[info.lightmapIndex];
|
|
var cellLm = Image.CreateFromData(lightmap.Width, lightmap.Height, false, Image.Format.Rgba8, lightmap.AsBytesRgba());
|
|
image.BlitRect(cellLm, new Rect2I(0, 0, lightmap.Width, lightmap.Height), new Vector2I((int)rect.X, (int)rect.Y));
|
|
|
|
if (!surfaceDataMap.ContainsKey(info.textureId)) GD.Print("Invalid SurfaceDataMap key");
|
|
surfaceDataMap[info.textureId].TransformUv2s(info.uvStart, info.uvEnd, (uv) =>
|
|
{
|
|
var u = uv.X;
|
|
var v = uv.Y;
|
|
|
|
// Clamp uv range to [0..1]
|
|
u %= 1;
|
|
v %= 1;
|
|
if (u < 0) u = Math.Abs(u);
|
|
if (v < 0) v = Math.Abs(v);
|
|
|
|
// Transform!
|
|
u = (rect.X + rect.Width * u) / (int)bounds.Width;
|
|
v = (rect.Y + rect.Height * v) / (int)bounds.Height;
|
|
return new Vector2(u, v);
|
|
});
|
|
}
|
|
|
|
return ImageTexture.CreateFromImage(image);
|
|
}
|
|
|
|
private int CalcBaseUV(
|
|
WorldRep.Cell cell,
|
|
WorldRep.Cell.Poly poly,
|
|
WorldRep.Cell.RenderPoly renderPoly,
|
|
WorldRep.Cell.LightmapInfo light,
|
|
List<Vector2> textureUvs,
|
|
List<Vector2> lightmapUvs,
|
|
float lightmapScale,
|
|
int cellIdxOffset)
|
|
{
|
|
// TODO: This is slightly hardcoded for ND. Check other stuff at some point. Should be handled in LG side imo
|
|
// TODO: This is a mess lol
|
|
var textureId = renderPoly.TextureId;
|
|
|
|
var texture = _textureLoader.Get(textureId);
|
|
var texU = renderPoly.TextureVectors.Item1.ToGodotVec3();
|
|
var texV = renderPoly.TextureVectors.Item2.ToGodotVec3();
|
|
var baseU = renderPoly.TextureBases.Item1;
|
|
var baseV = renderPoly.TextureBases.Item2;
|
|
|
|
var txUScale = 64.0f / lightmapScale / texture.GetWidth();
|
|
var txVScale = 64.0f / lightmapScale / texture.GetHeight();
|
|
var lmUScale = 4.0f / light.Width;
|
|
var lmVScale = 4.0f / light.Height;
|
|
|
|
var txUBase = baseU * txUScale;
|
|
var txVBase = baseV * txVScale;
|
|
var lmUBase = lmUScale * (baseU + (0.5f - light.Bases.Item1) / 4.0f);
|
|
var lmVBase = lmVScale * (baseV + (0.5f - light.Bases.Item2) / 4.0f);
|
|
|
|
var uu = texU.Dot(texU);
|
|
var vv = texV.Dot(texV);
|
|
var uv = texU.Dot(texV);
|
|
var anchor = cell.Vertices[cell.Indices[cellIdxOffset + 0]].ToGodotVec3(); // TODO: This probably shouldn't be hardcoded idx 0
|
|
if (uv == 0.0)
|
|
{
|
|
var txUVec = texU * txUScale / uu;
|
|
var txVVec = texV * txVScale / vv;
|
|
var lmUVec = texU * lmUScale / uu;
|
|
var lmVVec = texV * lmVScale / vv;
|
|
for (var i = 0; i < poly.VertexCount; i++)
|
|
{
|
|
var v = cell.Vertices[cell.Indices[cellIdxOffset + i]].ToGodotVec3();
|
|
var delta = new Vector3(v.X - anchor.X, v.Y - anchor.Y, v.Z - anchor.Z);
|
|
var txUV = new Vector2(delta.Dot(txUVec) + txUBase, delta.Dot(txVVec) + txVBase);
|
|
var lmUV = new Vector2(delta.Dot(lmUVec) + lmUBase, delta.Dot(lmVVec) + lmVBase);
|
|
textureUvs.Add(txUV);
|
|
lightmapUvs.Add(lmUV);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var denom = 1.0f / (uu * vv - uv * uv);
|
|
|
|
var txUu = uu * txVScale * denom;
|
|
var txVv = vv * txUScale * denom;
|
|
var txUvu = txUScale * denom * uv;
|
|
var txUvv = txVScale * denom * uv;
|
|
var lmUu = uu * lmVScale * denom;
|
|
var lmVv = vv * lmUScale * denom;
|
|
var lmUvu = lmUScale * denom * uv;
|
|
var lmUvv = lmVScale * denom * uv;
|
|
for (var i = 0; i < poly.VertexCount; i++)
|
|
{
|
|
var v = cell.Vertices[cell.Indices[cellIdxOffset + i]].ToGodotVec3();
|
|
var delta = new Vector3(v.X - anchor.X, v.Y - anchor.Y, v.Z - anchor.Z);
|
|
var du = delta.Dot(texU);
|
|
var dv = delta.Dot(texV);
|
|
|
|
var txUV = new Vector2(txUBase + txVv * du - txUvu * dv, txVBase + txUu * dv - txUvv * du);
|
|
var lmUV = new Vector2(lmUBase + lmVv * du - lmUvu * dv, lmVBase + lmUu * dv - lmUvv * du);
|
|
textureUvs.Add(txUV);
|
|
lightmapUvs.Add(lmUV);
|
|
}
|
|
}
|
|
|
|
return textureId;
|
|
}
|
|
|
|
private void LoadTextures(TxList textureList)
|
|
{
|
|
// TODO: Use PathJoin
|
|
var count = textureList.ItemCount;
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var item = textureList.Items[i];
|
|
var path = "";
|
|
for (var j = 0; j < item.Tokens.Length; j++)
|
|
{
|
|
var token = item.Tokens[j];
|
|
if (token == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
path += $"{textureList.Tokens[token - 1]}/";
|
|
}
|
|
path += item.Name;
|
|
|
|
if (!_textureLoader.Load(_installPaths, i, path))
|
|
{
|
|
GD.Print($"Failed to load texture: {path}");
|
|
}
|
|
}
|
|
|
|
if (Dump) DumpTextureList(textureList);
|
|
}
|
|
|
|
private static void DumpTextureList(TxList textureList)
|
|
{
|
|
GD.Print($"TXLIST:\n BlockSize: {textureList.BlockSize}\n ItemCount: {textureList.ItemCount}\n TokenCount: {textureList.TokenCount}\n Tokens:");
|
|
for (var i = 0; i < textureList.TokenCount; i++)
|
|
{
|
|
GD.Print($" {i}: {textureList.Tokens[i]}");
|
|
}
|
|
GD.Print($" Items:");
|
|
for (var i = 0; i < textureList.ItemCount; i++)
|
|
{
|
|
var item = textureList.Items[i];
|
|
GD.Print($" {i}:\n Tokens: [{item.Tokens[0]}, {item.Tokens[1]}, {item.Tokens[2]}, {item.Tokens[3]}]\n Name: {item.Name}");
|
|
}
|
|
}
|
|
|
|
const string MATERIAL_PATH = "res://project/materials/base.tres";
|
|
private static Material BuildMaterial(Texture albedoTexture, Texture lightmapTexture, bool lmHdr)
|
|
{
|
|
var material = ResourceLoader.Load<ShaderMaterial>(MATERIAL_PATH).Duplicate() as ShaderMaterial;
|
|
material.SetShaderParameter("texture_albedo", albedoTexture);
|
|
material.SetShaderParameter("lightmap_albedo", lightmapTexture);
|
|
material.SetShaderParameter("lightmap_2x", lmHdr);
|
|
return material;
|
|
}
|
|
} |