thief-mission-viewer/project/code/Mission.cs

365 lines
10 KiB
C#
Raw Normal View History

2024-07-21 21:13:31 +00:00
using Godot;
using KeepersCompound.LGS;
using KeepersCompound.LGS.Database;
using KeepersCompound.LGS.Database.Chunks;
2024-08-03 09:24:45 +00:00
using KeepersCompound.TMV.UI;
2024-07-30 20:50:34 +00:00
using RectpackSharp;
2024-07-21 21:13:31 +00:00
using System;
using System.Collections.Generic;
2024-08-03 15:09:51 +00:00
using System.IO;
2024-07-21 21:13:31 +00:00
namespace KeepersCompound;
[Tool]
public partial class Mission : Node3D
{
[Export(PropertyHint.GlobalFile, "*.mis")]
public string FileName { get; set; }
[Export]
public bool Build = false;
[Export]
public bool Clear = false;
2024-08-03 15:09:51 +00:00
[Export]
public bool Dump = false;
2024-07-21 21:13:31 +00:00
DbFile _file;
2024-08-03 15:09:51 +00:00
List<Image> _textures; // TODO: Make these textures :)
2024-07-21 21:13:31 +00:00
public override void _Ready()
{
2024-08-03 15:09:51 +00:00
_textures = new List<Image>();
2024-08-03 09:24:45 +00:00
var missionSelector = GetNode<Control>("%MissionSelector") as MissionSelector;
missionSelector.LoadMission += (string path) =>
{
FileName = path;
Build = true;
};
2024-07-21 21:13:31 +00:00
}
public override void _Process(double delta)
{
if (Build)
{
RebuildMap();
Build = false;
}
if (Clear)
{
ClearMap();
Clear = false;
}
2024-07-21 21:13:31 +00:00
}
public override void _Input(InputEvent @event)
{
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
{
if (keyEvent.Keycode == Key.R)
{
Build = true;
}
}
}
public void ClearMap()
2024-07-21 21:13:31 +00:00
{
foreach (var node in GetChildren())
{
node.QueueFree();
}
}
public void RebuildMap()
{
ClearMap();
2024-07-21 21:13:31 +00:00
_file = new(FileName);
2024-08-03 15:09:51 +00:00
var textureList = (TxList)_file.Chunks["TXLIST"];
// LoadTextures(textureList);
if (Dump) DumpTextureList(textureList);
2024-07-21 21:13:31 +00:00
var wr = (WorldRep)_file.Chunks["WREXT"];
foreach (var cell in wr.Cells)
{
BuildCellMesh(cell);
}
}
private void BuildCellMesh(WorldRep.Cell cell)
{
var numPolys = cell.PolyCount;
var numRenderPolys = cell.RenderPolyCount;
var numPortalPolys = cell.PortalPolyCount;
2024-07-30 20:50:34 +00:00
// There's nothing to render
if (numRenderPolys == 0 || numPortalPolys >= numPolys)
{
return;
}
2024-07-30 20:50:34 +00:00
// 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
2024-07-22 19:34:12 +00:00
var maxPolyIdx = Math.Min(numRenderPolys, numPolys - numPortalPolys);
2024-08-03 16:22:38 +00:00
2024-07-30 20:50:34 +00:00
var packingRects = new PackingRectangle[maxPolyIdx];
var rectIdToUvIdxMap = new List<int[]>();
2024-07-21 21:13:31 +00:00
2024-08-03 16:22:38 +00:00
var vertices = new List<Vector3>();
var normals = new List<Vector3>();
var indices = new List<int>();
var lightmapUvs = new List<Vector2>();
GenerateMeshContent(cell, maxPolyIdx, packingRects, rectIdToUvIdxMap, vertices, normals, indices, lightmapUvs);
2024-07-21 21:13:31 +00:00
2024-07-30 20:50:34 +00:00
RectanglePacker.Pack(packingRects, out var bounds);
var image = Image.Create((int)bounds.Width, (int)bounds.Height, false, Image.Format.Rgba8);
foreach (var rect in packingRects)
{
// Build lightmap
var lightmap = cell.Lightmaps[rect.Id];
// TODO: Handle animlight layers
2024-08-01 17:23:17 +00:00
var layers = (uint)lightmap.Pixels.GetLength(0);
var height = (uint)lightmap.Pixels.GetLength(1);
var width = (uint)lightmap.Pixels.GetLength(2);
for (uint y = 0; y < height; y++)
2024-07-30 20:50:34 +00:00
{
2024-08-01 17:23:17 +00:00
for (uint x = 0; x < width; x++)
2024-07-30 20:50:34 +00:00
{
2024-08-01 17:23:17 +00:00
var rawColour = System.Numerics.Vector4.Zero;
for (uint l = 0; l < layers; l++)
{
rawColour += lightmap.GetPixel(l, x, y);
}
var colour = new Color(MathF.Min(rawColour.X, 1.0f), MathF.Min(rawColour.Y, 1.0f), MathF.Min(rawColour.Z, 1.0f), MathF.Min(rawColour.W, 1.0f));
2024-08-03 15:09:51 +00:00
// !HACK: lol just overwriting the lightmap :xdd:
// var texIdx = cell.RenderPolys[rect.Id].TextureId;
// colour = _textures[texIdx].GetPixel((int)x, (int)y);
2024-08-01 17:23:17 +00:00
image.SetPixel((int)(rect.X + x), (int)(rect.Y + y), colour);
2024-07-30 20:50:34 +00:00
}
}
// Transform UVs
var lmUvIdxs = rectIdToUvIdxMap[rect.Id];
foreach (var idx in lmUvIdxs)
{
var uv = lightmapUvs[idx];
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;
lightmapUvs[idx] = new Vector2(u, v);
}
}
var material = new StandardMaterial3D
{
AlbedoTexture = ImageTexture.CreateFromImage(image),
TextureFilter = BaseMaterial3D.TextureFilterEnum.Nearest,
};
MeshInstance3D mesh = GenerateMesh(vertices, normals, indices, lightmapUvs, material);
OccluderInstance3D occluderInstance = GenerateOccluder(vertices, indices);
var cellNode = new Node3D();
cellNode.AddChild(mesh);
cellNode.AddChild(occluderInstance);
AddChild(cellNode);
}
2024-08-03 16:22:38 +00:00
private static void GenerateMeshContent(WorldRep.Cell cell, int maxPolyIdx, PackingRectangle[] packingRects, List<int[]> rectIdToUvIdxMap, List<Vector3> vertices, List<Vector3> normals, List<int> indices, List<Vector2> lightmapUvs)
{
var cellIdxOffset = 0;
for (int i = 0; i < maxPolyIdx; i++)
{
var poly = cell.Polys[i];
var meshIdxOffset = vertices.Count;
var normal = cell.Planes[poly.PlaneId].Normal.ToGodotVec3();
var numPolyVertices = poly.VertexCount;
for (var j = 0; j < numPolyVertices; j++)
{
var vertex = cell.Vertices[cell.Indices[cellIdxOffset + j]];
vertices.Add(vertex.ToGodotVec3());
normals.Add(normal);
}
// Simple triangulation. Polys are always convex so we can just do a fan
for (int j = 1; j < numPolyVertices - 1; j++)
{
indices.Add(meshIdxOffset);
indices.Add(meshIdxOffset + j);
indices.Add(meshIdxOffset + j + 1);
}
// UVs
var renderPoly = cell.RenderPolys[i];
var light = cell.LightList[i];
packingRects[i] = new PackingRectangle(0, 0, light.Width, light.Height, i);
var uvIdxs = CalcBaseUV(cell, poly, renderPoly, light, lightmapUvs, cellIdxOffset);
rectIdToUvIdxMap.Add(uvIdxs);
cellIdxOffset += poly.VertexCount;
}
}
private static OccluderInstance3D GenerateOccluder(List<Vector3> vertices, List<int> indices)
{
var occluder = new ArrayOccluder3D();
occluder.SetArrays(vertices.ToArray(), indices.ToArray());
var occluderInstance = new OccluderInstance3D
{
Occluder = occluder,
BakeSimplificationDistance = 0.0f
};
return occluderInstance;
}
private static MeshInstance3D GenerateMesh(List<Vector3> vertices, List<Vector3> normals, List<int> indices, List<Vector2> lightmapUvs, StandardMaterial3D material)
{
2024-07-21 21:13:31 +00:00
var arrMesh = new ArrayMesh();
var arrays = new Godot.Collections.Array();
arrays.Resize((int)Mesh.ArrayType.Max);
arrays[(int)Mesh.ArrayType.Vertex] = vertices.ToArray();
arrays[(int)Mesh.ArrayType.Normal] = normals.ToArray();
2024-07-22 18:37:56 +00:00
arrays[(int)Mesh.ArrayType.Index] = indices.ToArray();
2024-07-30 20:50:34 +00:00
arrays[(int)Mesh.ArrayType.TexUV] = lightmapUvs.ToArray();
2024-07-21 21:13:31 +00:00
arrMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, arrays);
2024-07-30 20:50:34 +00:00
arrMesh.SurfaceSetMaterial(0, material);
2024-07-22 19:34:12 +00:00
2024-07-21 21:13:31 +00:00
var meshInstance = new MeshInstance3D
{
Mesh = arrMesh,
CastShadow = GeometryInstance3D.ShadowCastingSetting.On
};
return meshInstance;
2024-07-21 21:13:31 +00:00
}
2024-07-30 20:50:34 +00:00
private static int[] CalcBaseUV(
WorldRep.Cell cell,
WorldRep.Cell.Poly poly,
WorldRep.Cell.RenderPoly renderPoly,
WorldRep.Cell.LightmapInfo light,
List<Vector2> lightmapUvs,
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 texU = renderPoly.TextureVectors.Item1.ToGodotVec3();
var texV = renderPoly.TextureVectors.Item2.ToGodotVec3();
2024-07-30 20:50:34 +00:00
var uu = texU.Dot(texU);
var vv = texV.Dot(texV);
var uv = texU.Dot(texV);
var lmUScale = 4.0f / light.Width;
var lmVScale = 4.0f / light.Height;
var baseU = renderPoly.TextureBases.Item1;
var baseV = renderPoly.TextureBases.Item2;
var lmUBase = lmUScale * (baseU + (0.5f - light.Bases.Item1) / 4.0f);
var lmVBase = lmVScale * (baseV + (0.5f - light.Bases.Item2) / 4.0f);
var anchor = cell.Vertices[cell.Indices[cellIdxOffset + 0]].ToGodotVec3(); // TODO: This probably shouldn't be hardcoded idx 0
2024-07-30 20:50:34 +00:00
var uvIdxs = new int[poly.VertexCount];
if (uv == 0.0)
{
var lmUVec = texU * lmUScale / uu;
var lmVVec = texV * lmVScale / vv;
for (var i = 0; i < poly.VertexCount; i++)
{
uvIdxs[i] = lightmapUvs.Count;
var v = cell.Vertices[cell.Indices[cellIdxOffset + i]].ToGodotVec3();
2024-07-30 20:50:34 +00:00
var delta = new Vector3(v.X - anchor.X, v.Y - anchor.Y, v.Z - anchor.Z);
var lmUV = new Vector2(delta.Dot(lmUVec) + lmUBase, delta.Dot(lmVVec) + lmVBase);
lightmapUvs.Add(lmUV);
}
}
else
{
var denom = 1.0f / (uu * vv - uv * 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++)
{
uvIdxs[i] = lightmapUvs.Count;
var v = cell.Vertices[cell.Indices[cellIdxOffset + i]].ToGodotVec3();
2024-07-30 20:50:34 +00:00
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 lmUV = new Vector2(lmUBase + lmVv * du - lmUvu * dv, lmVBase + lmUu * dv - lmUvv * du);
lightmapUvs.Add(lmUV);
}
}
return uvIdxs;
}
2024-08-03 15:09:51 +00:00
private void LoadTextures(TxList textureList)
{
// TODO: This has hardcoded .png extension and relies on you placing extracted and converted images in godot user directory
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 + ".png"; // Hardcoded extension!
if (File.Exists(FileName + path))
{
path = FileName + path;
}
else if (File.Exists(ProjectSettings.GlobalizePath($"user://textures{path}")))
{
path = ProjectSettings.GlobalizePath($"user://textures{path}");
}
else
{
path = "user://textures/jorge.png";
}
if (Dump) GD.Print($"Loading texture: {path}");
// _textures.Add(ImageTexture.CreateFromImage(Image.LoadFromFile(path)));
_textures.Add(Image.LoadFromFile(path));
}
}
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}");
}
}
2024-07-21 21:13:31 +00:00
}