Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions TombLib/TombLib.Test/PortalShadeMatchHelperTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
174 changes: 71 additions & 103 deletions TombLib/TombLib/LevelData/Compilers/Rooms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1761,35 +1761,12 @@ private void MatchDoorShades(List<tr_room> 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
Expand All @@ -1799,94 +1776,85 @@ private void MatchDoorShades(List<tr_room> 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;
}
}
}
}
}
Expand Down
50 changes: 50 additions & 0 deletions TombLib/TombLib/LevelData/Compilers/Structs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Loading