diff --git a/TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs b/TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs new file mode 100644 index 0000000000..4b5acaebb1 --- /dev/null +++ b/TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs @@ -0,0 +1,65 @@ +using System.Numerics; +using TombLib; +using TombLib.LevelData.Compilers; + +namespace TombLib.Test; + +[TestClass] +public class PortalShadeMatchHelperTests +{ + [TestMethod] + public void IsCandidate_RejectsInteriorPortalVertex() + { + var legacyPortalVertices = new[] + { + new tr_vertex(0, 0, 0), + new tr_vertex(4, 2, 0), + new tr_vertex(4, 2, 4), + new tr_vertex(0, 0, 4) + }; + + var tombEnginePortalVertices = new[] + { + new VectorInt3(0, 0, 0), + new VectorInt3(4, 2, 0), + new VectorInt3(4, 2, 4), + new VectorInt3(0, 0, 4) + }; + + Assert.IsFalse(InvokeLegacyCandidate(legacyPortalVertices, new tr_vertex(2, 1, 2))); + Assert.IsFalse(InvokeTombEngineCandidate(tombEnginePortalVertices, new Vector3(2.0f, 1.0f, 2.0f))); + } + + [TestMethod] + public void IsCandidate_AcceptsPortalEdgeVertex() + { + var legacyPortalVertices = new[] + { + new tr_vertex(0, 0, 0), + new tr_vertex(4, 2, 0), + new tr_vertex(4, 2, 4), + new tr_vertex(0, 0, 4) + }; + + var tombEnginePortalVertices = new[] + { + new VectorInt3(0, 0, 0), + new VectorInt3(4, 2, 0), + new VectorInt3(4, 2, 4), + new VectorInt3(0, 0, 4) + }; + + Assert.IsTrue(InvokeLegacyCandidate(legacyPortalVertices, new tr_vertex(2, 1, 0))); + Assert.IsTrue(InvokeTombEngineCandidate(tombEnginePortalVertices, new Vector3(2.0f, 1.0f, 0.0f))); + } + + private static bool InvokeLegacyCandidate(tr_vertex[] portalVertices, tr_vertex vertexPosition) + { + return PortalShadeMatchHelper.IsCandidate(portalVertices, vertexPosition); + } + + private static bool InvokeTombEngineCandidate(VectorInt3[] portalVertices, Vector3 vertexPosition) + { + return PortalShadeMatchHelper.IsCandidate(portalVertices, vertexPosition); + } +} diff --git a/TombLib/TombLib/LevelData/Compilers/Rooms.cs b/TombLib/TombLib/LevelData/Compilers/Rooms.cs index 1224b8a2dc..f80e013b6f 100644 --- a/TombLib/TombLib/LevelData/Compilers/Rooms.cs +++ b/TombLib/TombLib/LevelData/Compilers/Rooms.cs @@ -1761,35 +1761,12 @@ private void MatchDoorShades(List roomList, tr_room room, bool grayscal (room.OriginalRoom.Properties.LightInterpolationMode == RoomLightInterpolationMode.Interpolate || otherRoom.OriginalRoom.Properties.LightInterpolationMode == RoomLightInterpolationMode.Interpolate))) { - int x1 = p.Vertices[0].X; - int y1 = p.Vertices[0].Y; - int z1 = p.Vertices[0].Z; - - int x2 = x1 + 1; - int y2 = y1 + 1; - int z2 = z1 + 1; - - for (int i = 1; i < 4; i++) - { - if (p.Vertices[i].X < x1) - x1 = p.Vertices[i].X; - else if (p.Vertices[i].X > x2) - x2 = p.Vertices[i].X + 1; - - if (p.Vertices[i].Y < y1) - y1 = p.Vertices[i].Y; - else if (p.Vertices[i].Y > y2) - y2 = p.Vertices[i].Y + 1; - - if (p.Vertices[i].Z < z1) - z1 = p.Vertices[i].Z; - else if (p.Vertices[i].Z > z2) - z2 = p.Vertices[i].Z + 1; - } - for (int i = 0; i < room.Vertices.Count; i++) { var v1 = room.Vertices[i]; + if (!PortalShadeMatchHelper.IsCandidate(p.Vertices, v1.Position)) + continue; + var sig = new ShadeMatchSignature() { // NOTE: We keep alternate group and water flag in dictionary as well, this way we only apply vertex colour to @@ -1799,94 +1776,85 @@ private void MatchDoorShades(List roomList, tr_room room, bool grayscal Position = new VectorInt3(v1.Position.X + room.Info.X, v1.Position.Y, v1.Position.Z + room.Info.Z) }; - if (v1.Position.X >= x1 && v1.Position.X <= x2) - if (v1.Position.Y >= y1 && v1.Position.Y <= y2) - if (v1.Position.Z >= z1 && v1.Position.Z <= z2) + v1.IsOnPortal = true; + room.Vertices[i] = v1; + + for (int j = 0; j < otherRoom.Vertices.Count; j++) + { + uint refColor = 0; + var v2 = otherRoom.Vertices[j]; + var isPresentInLookup = _vertexColors.TryGetValue(sig, out refColor); + + if (!isPresentInLookup) + { + if (_level.Settings.GameVersion != TRVersion.Game.TR5) { - v1.IsOnPortal = true; - room.Vertices[i] = v1; + if (_level.Settings.GameVersion == TRVersion.Game.TRNG && _level.Settings.Room32BitLighting) + refColor = UnpackFrom24BitPair(v1.Lighting1, v1.Lighting2); + else + refColor = v1.Lighting2; + } + else + refColor = v1.Color; + } - int otherX = v1.Position.X + room.Info.X - otherRoom.Info.X; - int otherY = v1.Position.Y; - int otherZ = v1.Position.Z + room.Info.Z - otherRoom.Info.Z; + if (room.Info.X + v1.Position.X == otherRoom.Info.X + v2.Position.X && + v1.Position.Y == v2.Position.Y && + room.Info.Z + v1.Position.Z == otherRoom.Info.Z + v2.Position.Z) + { + uint newColor = 0; - for (int j = 0; j < otherRoom.Vertices.Count; j++) - { - uint refColor = 0; - var v2 = otherRoom.Vertices[j]; - var isPresentInLookup = _vertexColors.TryGetValue(sig, out refColor); + // NOTE: We DON'T INTERPOLATE colours of both rooms in case we're dealing with alternate room and matched room + // isn't alternate room itself. Instead, we simply copy vertex colour from matched base room. + // This way we don't get sharp-cut half-transitioned vertex colour. - if (!isPresentInLookup) + if (flipped && otherRoom.AlternateKind != AlternateKind.AlternateRoom) + { + var baseSig = new ShadeMatchSignature() { IsWater = sig.IsWater, AlternateGroup = -1, Position = sig.Position }; + + if (!_vertexColors.TryGetValue(baseSig, out newColor)) + { + if (_level.Settings.GameVersion != TRVersion.Game.TR5) { - if (_level.Settings.GameVersion != TRVersion.Game.TR5) - { - if (_level.Settings.GameVersion == TRVersion.Game.TRNG && _level.Settings.Room32BitLighting) - refColor = UnpackFrom24BitPair(v1.Lighting1, v1.Lighting2); - else - refColor = v1.Lighting2; - } + if (_level.Settings.GameVersion == TRVersion.Game.TRNG && _level.Settings.Room32BitLighting) + newColor = UnpackFrom24BitPair(v2.Lighting1, v2.Lighting2); else - refColor = v1.Color; + newColor = v2.Lighting2; } - - if (room.Info.X + v1.Position.X == otherRoom.Info.X + v2.Position.X && - v1.Position.Y == v2.Position.Y && - room.Info.Z + v1.Position.Z == otherRoom.Info.Z + v2.Position.Z) + else + newColor = v2.Color; + } + } + else + { + if (grayscale) + newColor = (ushort)(8160 - (((8160 - v2.Lighting2) / 2) + ((8160 - refColor) / 2))); + else if (_level.Settings.GameVersion != TRVersion.Game.TR5) + { + if (_level.Settings.GameVersion == TRVersion.Game.TRNG && _level.Settings.Room32BitLighting) { - uint newColor = 0; - - // NOTE: We DON'T INTERPOLATE colours of both rooms in case we're dealing with alternate room and matched room - // isn't alternate room itself. Instead, we simply copy vertex colour from matched base room. - // This way we don't get sharp-cut half-transitioned vertex colour. - - if (flipped && otherRoom.AlternateKind != AlternateKind.AlternateRoom) - { - var baseSig = new ShadeMatchSignature() { IsWater = sig.IsWater, AlternateGroup = -1, Position = sig.Position }; - - if (!_vertexColors.TryGetValue(baseSig, out newColor)) - { - if (_level.Settings.GameVersion != TRVersion.Game.TR5) - { - if (_level.Settings.GameVersion == TRVersion.Game.TRNG && _level.Settings.Room32BitLighting) - newColor = UnpackFrom24BitPair(v2.Lighting1, v2.Lighting2); - else - newColor = v2.Lighting2; - } - else - newColor = v2.Color; - } - } - else - { - if (grayscale) - newColor = (ushort)(8160 - (((8160 - v2.Lighting2) / 2) + ((8160 - refColor) / 2))); - else if (_level.Settings.GameVersion != TRVersion.Game.TR5) - { - if (_level.Settings.GameVersion == TRVersion.Game.TRNG && _level.Settings.Room32BitLighting) - { - var color = UnpackFrom24BitPair(v2.Lighting1, v2.Lighting2); - newColor = (uint)(0xff000000 | (((((color & 0xff) + (refColor & 0xff)) >> 1) | - 256 * (((((color >> 8) & 0xff) + ((refColor >> 8) & 0xff)) >> 1) | - 256 * ((((color >> 16) & 0xff) + ((refColor >> 16) & 0xff)) >> 1))))); - } - else - newColor = (ushort)((((v2.Lighting2 & 0x1f) + (refColor & 0x1f)) >> 1) | - 32 * (((((v2.Lighting2 >> 5) & 0x1f) + ((refColor >> 5) & 0x1f)) >> 1) | - 32 * ((((v2.Lighting2 >> 10) & 0x1f) + ((refColor >> 10) & 0x1f)) >> 1))); - } - else - newColor = (uint)(0xff000000 | (((((v2.Color & 0xff) + (refColor & 0xff)) >> 1) | - 256 * (((((v2.Color >> 8) & 0xff) + ((refColor >> 8) & 0xff)) >> 1) | - 256 * ((((v2.Color >> 16) & 0xff) + ((refColor >> 16) & 0xff)) >> 1))))); - } - - if (!isPresentInLookup) - _vertexColors.TryAdd(sig, newColor); - else - _vertexColors[sig] = newColor; + var color = UnpackFrom24BitPair(v2.Lighting1, v2.Lighting2); + newColor = (uint)(0xff000000 | (((((color & 0xff) + (refColor & 0xff)) >> 1) | + 256 * (((((color >> 8) & 0xff) + ((refColor >> 8) & 0xff)) >> 1) | + 256 * ((((color >> 16) & 0xff) + ((refColor >> 16) & 0xff)) >> 1))))); } + else + newColor = (ushort)((((v2.Lighting2 & 0x1f) + (refColor & 0x1f)) >> 1) | + 32 * (((((v2.Lighting2 >> 5) & 0x1f) + ((refColor >> 5) & 0x1f)) >> 1) | + 32 * ((((v2.Lighting2 >> 10) & 0x1f) + ((refColor >> 10) & 0x1f)) >> 1))); } + else + newColor = (uint)(0xff000000 | (((((v2.Color & 0xff) + (refColor & 0xff)) >> 1) | + 256 * (((((v2.Color >> 8) & 0xff) + ((refColor >> 8) & 0xff)) >> 1) | + 256 * ((((v2.Color >> 16) & 0xff) + ((refColor >> 16) & 0xff)) >> 1))))); } + + if (!isPresentInLookup) + _vertexColors.TryAdd(sig, newColor); + else + _vertexColors[sig] = newColor; + } + } } } } diff --git a/TombLib/TombLib/LevelData/Compilers/Structs.cs b/TombLib/TombLib/LevelData/Compilers/Structs.cs index 6412d85081..22f4a0a546 100644 --- a/TombLib/TombLib/LevelData/Compilers/Structs.cs +++ b/TombLib/TombLib/LevelData/Compilers/Structs.cs @@ -32,6 +32,56 @@ public override int GetHashCode() public override bool Equals(object obj) => GetHashCode() == obj.GetHashCode(); } + internal static class PortalShadeMatchHelper + { + // Tolerance for point-on-segment checks to absorb floating-point error. + private const float PortalEdgeEpsilon = 0.001f; + + public static bool IsCandidate(tr_vertex[] portalVertices, tr_vertex vertexPosition) + { + return IsCandidate( + new Vector3(vertexPosition.X, vertexPosition.Y, vertexPosition.Z), + new Vector3(portalVertices[0].X, portalVertices[0].Y, portalVertices[0].Z), + new Vector3(portalVertices[1].X, portalVertices[1].Y, portalVertices[1].Z), + new Vector3(portalVertices[2].X, portalVertices[2].Y, portalVertices[2].Z), + new Vector3(portalVertices[3].X, portalVertices[3].Y, portalVertices[3].Z)); + } + + public static bool IsCandidate(global::TombLib.VectorInt3[] portalVertices, Vector3 vertexPosition) + { + return IsCandidate( + vertexPosition, + new Vector3(portalVertices[0].X, portalVertices[0].Y, portalVertices[0].Z), + new Vector3(portalVertices[1].X, portalVertices[1].Y, portalVertices[1].Z), + new Vector3(portalVertices[2].X, portalVertices[2].Y, portalVertices[2].Z), + new Vector3(portalVertices[3].X, portalVertices[3].Y, portalVertices[3].Z)); + } + + private static bool IsCandidate(Vector3 vertexPosition, Vector3 vertex0, Vector3 vertex1, Vector3 vertex2, Vector3 vertex3) + { + return IsPointOnSegment(vertexPosition, vertex0, vertex1) || + IsPointOnSegment(vertexPosition, vertex1, vertex2) || + IsPointOnSegment(vertexPosition, vertex2, vertex3) || + IsPointOnSegment(vertexPosition, vertex3, vertex0); + } + + private static bool IsPointOnSegment(Vector3 vertexPosition, Vector3 segmentStart, Vector3 segmentEnd) + { + var segment = segmentEnd - segmentStart; + var offset = vertexPosition - segmentStart; + var segmentLengthSquared = segment.LengthSquared(); + + if (segmentLengthSquared <= float.Epsilon) + return false; + + var projection = Vector3.Dot(offset, segment); + if (projection < -PortalEdgeEpsilon || projection > segmentLengthSquared + PortalEdgeEpsilon) + return false; + + return Vector3.Cross(offset, segment).LengthSquared() <= PortalEdgeEpsilon * PortalEdgeEpsilon * segmentLengthSquared; + } + } + [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct tr_color { diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs index 86ce048b32..a291da76bd 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/Rooms.cs @@ -1648,35 +1648,13 @@ private void MatchDoorShades(List roomList, TombEngineRoom room, (room.OriginalRoom.Properties.LightInterpolationMode == RoomLightInterpolationMode.Interpolate || otherRoom.OriginalRoom.Properties.LightInterpolationMode == RoomLightInterpolationMode.Interpolate))) { - int x1 = p.Vertices[0].X; - int y1 = p.Vertices[0].Y; - int z1 = p.Vertices[0].Z; - - int x2 = x1 + 1; - int y2 = y1 + 1; - int z2 = z1 + 1; - - for (int i = 1; i < 4; i++) - { - if (p.Vertices[i].X < x1) - x1 = p.Vertices[i].X; - else if (p.Vertices[i].X > x2) - x2 = p.Vertices[i].X + 1; - - if (p.Vertices[i].Y < y1) - y1 = p.Vertices[i].Y; - else if (p.Vertices[i].Y > y2) - y2 = p.Vertices[i].Y + 1; - - if (p.Vertices[i].Z < z1) - z1 = p.Vertices[i].Z; - else if (p.Vertices[i].Z > z2) - z2 = p.Vertices[i].Z + 1; - } - for (int i = 0; i < room.Vertices.Count; i++) { var v1 = room.Vertices[i]; + // Only match shades for vertices that actually lie on the portal edge. + if (!PortalShadeMatchHelper.IsCandidate(p.Vertices, v1.Position)) + continue; + var sig = new ShadeMatchSignature() { // NOTE: We keep alternate group and water flag in dictionary as well, this way we only apply vertex colour to @@ -1686,51 +1664,44 @@ private void MatchDoorShades(List roomList, TombEngineRoom room, Position = new VectorInt3((int)v1.Position.X + room.Info.X, (int)v1.Position.Y, (int)v1.Position.Z + room.Info.Z) }; - if (v1.Position.X >= x1 && v1.Position.X <= x2) - if (v1.Position.Y >= y1 && v1.Position.Y <= y2) - if (v1.Position.Z >= z1 && v1.Position.Z <= z2) - { - v1.IsOnPortal = true; - room.Vertices[i] = v1; - - int otherX = (int)v1.Position.X + room.Info.X - otherRoom.Info.X; - int otherY = (int)v1.Position.Y; - int otherZ = (int)v1.Position.Z + room.Info.Z - otherRoom.Info.Z; - - for (int j = 0; j < otherRoom.Vertices.Count; j++) - { - var v2 = otherRoom.Vertices[j]; - Vector3 refColor; - var isPresentInLookup = _vertexColors.TryGetValue(sig, out refColor); - if (!isPresentInLookup) refColor = v1.Color; - - if (room.Info.X + v1.Position.X == otherRoom.Info.X + v2.Position.X && - v1.Position.Y == v2.Position.Y && - room.Info.Z + v1.Position.Z == otherRoom.Info.Z + v2.Position.Z) - { - Vector3 newColor; + v1.IsOnPortal = true; + room.Vertices[i] = v1; - // NOTE: We DON'T INTERPOLATE colours of both rooms in case we're dealing with alternate room and matched room - // isn't alternate room itself. Instead, we simply copy vertex colour from matched base room. - // This way we don't get sharp-cut half-transitioned vertex colour. + for (int j = 0; j < otherRoom.Vertices.Count; j++) + { + var v2 = otherRoom.Vertices[j]; + Vector3 refColor; + var isPresentInLookup = _vertexColors.TryGetValue(sig, out refColor); + if (!isPresentInLookup) + refColor = v1.Color; + + if (room.Info.X + v1.Position.X == otherRoom.Info.X + v2.Position.X && + v1.Position.Y == v2.Position.Y && + room.Info.Z + v1.Position.Z == otherRoom.Info.Z + v2.Position.Z) + { + Vector3 newColor; - if (flipped && otherRoom.AlternateKind != AlternateKind.AlternateRoom) - { - var baseSig = new ShadeMatchSignature() { IsWater = sig.IsWater, AlternateGroup = -1, Position = sig.Position }; - if (!_vertexColors.TryGetValue(baseSig, out newColor)) newColor = v2.Color; - } - else - { - newColor = (v2.Color + refColor) / 2.0f; - } + // NOTE: We DON'T INTERPOLATE colours of both rooms in case we're dealing with alternate room and matched room + // isn't alternate room itself. Instead, we simply copy vertex colour from matched base room. + // This way we don't get sharp-cut half-transitioned vertex colour. - if (!isPresentInLookup) - _vertexColors.TryAdd(sig, newColor); - else - _vertexColors[sig] = newColor; - } - } + if (flipped && otherRoom.AlternateKind != AlternateKind.AlternateRoom) + { + var baseSig = new ShadeMatchSignature() { IsWater = sig.IsWater, AlternateGroup = -1, Position = sig.Position }; + if (!_vertexColors.TryGetValue(baseSig, out newColor)) + newColor = v2.Color; + } + else + { + newColor = (v2.Color + refColor) / 2.0f; } + + if (!isPresentInLookup) + _vertexColors.TryAdd(sig, newColor); + else + _vertexColors[sig] = newColor; + } + } } } } diff --git a/TombLib/TombLib/Properties/AssemblyInfo.cs b/TombLib/TombLib/Properties/AssemblyInfo.cs index 66c604f97b..276fea7b34 100644 --- a/TombLib/TombLib/Properties/AssemblyInfo.cs +++ b/TombLib/TombLib/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -14,6 +15,7 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: SupportedOSPlatform("windows")] +[assembly: InternalsVisibleTo("TombLib.Test")] // Se si imposta ComVisible su false, i tipi in questo assembly non saranno visibili // ai componenti COM. Se è necessario accedere a un tipo in questo assembly da