360 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			C#
		
	
	
	
			
		
		
	
	
			360 lines
		
	
	
		
			9.8 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;
 | 
						|
 | 
						|
[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;
 | 
						|
	[Export]
 | 
						|
	public bool Dump = false;
 | 
						|
 | 
						|
	DbFile _file;
 | 
						|
	List<Image> _textures; // TODO: Make these textures :)
 | 
						|
 | 
						|
	public override void _Ready()
 | 
						|
	{
 | 
						|
		_textures = new List<Image>();
 | 
						|
 | 
						|
		var missionSelector = GetNode<Control>("%MissionSelector") as MissionSelector;
 | 
						|
		missionSelector.LoadMission += (string path) =>
 | 
						|
		{
 | 
						|
			FileName = path;
 | 
						|
			Build = true;
 | 
						|
		};
 | 
						|
	}
 | 
						|
 | 
						|
	public override void _Process(double delta)
 | 
						|
	{
 | 
						|
		if (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();
 | 
						|
 | 
						|
		_file = new(FileName);
 | 
						|
		var textureList = (TxList)_file.Chunks["TXLIST"];
 | 
						|
		// LoadTextures(textureList);
 | 
						|
		if (Dump) DumpTextureList(textureList);
 | 
						|
 | 
						|
		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;
 | 
						|
 | 
						|
		// There's nothing to render
 | 
						|
		if (numRenderPolys == 0 || numPortalPolys >= numPolys)
 | 
						|
		{
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		var vertices = new List<Vector3>();
 | 
						|
		var normals = new List<Vector3>();
 | 
						|
		var indices = new List<int>();
 | 
						|
		var lightmapUvs = new List<Vector2>();
 | 
						|
		var cellIdxOffset = 0;
 | 
						|
 | 
						|
		// 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 packingRects = new PackingRectangle[maxPolyIdx];
 | 
						|
		var rectIdToUvIdxMap = new List<int[]>();
 | 
						|
		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;
 | 
						|
		}
 | 
						|
 | 
						|
		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
 | 
						|
			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++)
 | 
						|
			{
 | 
						|
				for (uint x = 0; x < width; x++)
 | 
						|
				{
 | 
						|
					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));
 | 
						|
 | 
						|
					// !HACK: lol just overwriting the lightmap :xdd:
 | 
						|
					// var texIdx = cell.RenderPolys[rect.Id].TextureId;
 | 
						|
					// colour = _textures[texIdx].GetPixel((int)x, (int)y);
 | 
						|
					image.SetPixel((int)(rect.X + x), (int)(rect.Y + y), colour);
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			// 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 cellNode = new Node3D();
 | 
						|
 | 
						|
		var material = new StandardMaterial3D
 | 
						|
		{
 | 
						|
			AlbedoTexture = ImageTexture.CreateFromImage(image),
 | 
						|
			TextureFilter = BaseMaterial3D.TextureFilterEnum.Nearest,
 | 
						|
		};
 | 
						|
 | 
						|
		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();
 | 
						|
		arrays[(int)Mesh.ArrayType.Index] = indices.ToArray();
 | 
						|
		arrays[(int)Mesh.ArrayType.TexUV] = lightmapUvs.ToArray();
 | 
						|
 | 
						|
		arrMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, arrays);
 | 
						|
		arrMesh.SurfaceSetMaterial(0, material);
 | 
						|
 | 
						|
		var meshInstance = new MeshInstance3D
 | 
						|
		{
 | 
						|
			Mesh = arrMesh,
 | 
						|
			CastShadow = GeometryInstance3D.ShadowCastingSetting.On
 | 
						|
		};
 | 
						|
		cellNode.AddChild(meshInstance);
 | 
						|
 | 
						|
		var occluder = new ArrayOccluder3D();
 | 
						|
		occluder.SetArrays(vertices.ToArray(), indices.ToArray());
 | 
						|
		var occluderInstance = new OccluderInstance3D
 | 
						|
		{
 | 
						|
			Occluder = occluder,
 | 
						|
			BakeSimplificationDistance = 0.0f
 | 
						|
		};
 | 
						|
		cellNode.AddChild(occluderInstance);
 | 
						|
 | 
						|
		var r = new Random();
 | 
						|
		if (r.NextSingle() > 0.9 && cell.SphereRadius > 5.0)
 | 
						|
		{
 | 
						|
			var light = new OmniLight3D
 | 
						|
			{
 | 
						|
				Position = cell.SphereCenter.ToGodotVec3(),
 | 
						|
				OmniRange = cell.SphereRadius * (r.NextSingle() + 1.0f) * 0.5f,
 | 
						|
			};
 | 
						|
			// cellNode.AddChild(light);
 | 
						|
		}
 | 
						|
 | 
						|
		AddChild(cellNode);
 | 
						|
	}
 | 
						|
 | 
						|
	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();
 | 
						|
 | 
						|
		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
 | 
						|
 | 
						|
		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();
 | 
						|
				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();
 | 
						|
				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;
 | 
						|
	}
 | 
						|
 | 
						|
	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}");
 | 
						|
		}
 | 
						|
	}
 | 
						|
} |