From 2dd9e8d73774a48b768562525cb385c90caafb67 Mon Sep 17 00:00:00 2001 From: RDW Date: Wed, 13 Dec 2023 16:57:57 +0100 Subject: [PATCH 1/2] GND: Implement a terrain geometry generation algorithm Lightmaps and normals are still missing, but can easily be added later. After much trial and error, the current approach seems to be the best in terms of both extensibility and performance. It's probably not exactly optimal still, but I'll accept a worst-case generation time of 200ms for schg_dun01 as "good enough" for now. --- Core/FileFormats/RagnarokGND.lua | 304 ++++++++++++++++++++++++- Tests/FileFormats/RagnarokGND.spec.lua | 4 +- 2 files changed, 303 insertions(+), 5 deletions(-) diff --git a/Core/FileFormats/RagnarokGND.lua b/Core/FileFormats/RagnarokGND.lua index d39343ee..67a2dc3f 100644 --- a/Core/FileFormats/RagnarokGND.lua +++ b/Core/FileFormats/RagnarokGND.lua @@ -3,14 +3,26 @@ local BinaryReader = require("Core.FileFormats.BinaryReader") local ffi = require("ffi") local uv = require("uv") +local assert = assert local ffi_copy = ffi.copy local ffi_new = ffi.new local ffi_sizeof = ffi.sizeof +local format = string.format +local table_insert = table.insert local tonumber = tonumber local RagnarokGND = { + GAT_TILES_PER_GND_SURFACE = 2, DEFAULT_GEOMETRY_SCALE_FACTOR = 10, - NORMALIZING_SCALE_FACTOR = 1 / (10 / 2), -- 1/(geometryScale / numTilesPerSurface) + SURFACE_DIRECTION_UP = 0, + SURFACE_DIRECTION_EAST = 1, + SURFACE_DIRECTION_NORTH = 2, + FALLBACK_VERTEX_COLOR = { + red = 0, + green = 0, + blue = 0, + alpha = 255, + }, cdefs = [[ #pragma pack(1) typedef struct gnd_header { @@ -84,9 +96,14 @@ local RagnarokGND = { ]], } +local worldUnitsPerSurface = RagnarokGND.DEFAULT_GEOMETRY_SCALE_FACTOR +local worldUnitsPerTile = worldUnitsPerSurface / RagnarokGND.GAT_TILES_PER_GND_SURFACE +RagnarokGND.NORMALIZING_SCALE_FACTOR = 1 / worldUnitsPerTile + function RagnarokGND:Construct() local instance = { diffuseTexturePaths = {}, + groundMeshSections = {}, waterPlanes = {}, } @@ -154,9 +171,17 @@ function RagnarokGND:DecodeHeader() end function RagnarokGND:DecodeTexturePaths() - for textureIndex = 0, self.diffuseTextureCount - 1, 1 do + for textureIndex = 1, self.diffuseTextureCount, 1 do local windowsPathString = self.reader:GetNullTerminatedString(self.texturePathLength) - self.diffuseTexturePaths[textureIndex] = windowsPathString + table_insert(self.diffuseTexturePaths, windowsPathString) + + local groundMeshSection = { + vertexPositions = {}, + vertexColors = {}, + triangleConnections = {}, + diffuseTextureCoords = {}, + } + table_insert(self.groundMeshSections, groundMeshSection) end end @@ -226,6 +251,279 @@ function RagnarokGND:DecodeWaterPlanes() end end +function RagnarokGND:GenerateGroundMeshSections() + local startTime = uv.hrtime() + + for gridV = 1, self.gridSizeV do + for gridU = 1, self.gridSizeU do + local cubeID = self:GridPositionToCubeID(gridU, gridV) + local cube = self.cubeGrid[cubeID] + + -- Walls can't be raised if there's no adjacent cube to connect the surface to + local isOnMapBoundaryU = (gridU == self.gridSizeU) + local isOnMapBoundaryV = (gridV == self.gridSizeV) + + -- Despite this fact, EAST/NORTH surfaces sometimes appear on map boundaries (e.g., in c_tower4 and juperos_01) + -- Since that doesn't make any sense and is probably an oversight, just ignore them completely here ¯\_(ツ)_/¯ + local hasGroundSurface = (cube.top_surface_id >= 0) + local hasWallSurfaceNorth = (cube.north_surface_id >= 0) and not isOnMapBoundaryV + local hasWallSurfaceEast = (cube.east_surface_id >= 0) and not isOnMapBoundaryU + + assert(cube.top_surface_id < self.texturedSurfaceCount) + assert(cube.north_surface_id < self.texturedSurfaceCount) + assert(cube.east_surface_id < self.texturedSurfaceCount) + + local groundSurface = self.texturedSurfaces[cube.top_surface_id] + local wallSurfaceNorth = self.texturedSurfaces[cube.north_surface_id] + local wallSurfaceEast = self.texturedSurfaces[cube.east_surface_id] + + if hasGroundSurface then + self:GenerateSurfaceGeometry({ + surfaceDefinition = groundSurface, + gridU = gridU, + gridV = gridV, + facing = RagnarokGND.SURFACE_DIRECTION_UP, + }) + end + + if hasWallSurfaceNorth then + self:GenerateSurfaceGeometry({ + surfaceDefinition = wallSurfaceNorth, + gridU = gridU, + gridV = gridV, + facing = RagnarokGND.SURFACE_DIRECTION_NORTH, + }) + end + + if hasWallSurfaceEast then + self:GenerateSurfaceGeometry({ + surfaceDefinition = wallSurfaceEast, + gridU = gridU, + gridV = gridV, + facing = RagnarokGND.SURFACE_DIRECTION_EAST, + }) + end + end + end + + local endTime = uv.hrtime() + local terrainGenerationTimeInMilliseconds = (endTime - startTime) / 10E5 + printf( + "[RagnarokGND] Finished generating terrain geometry for %d ground mesh sections in %.2f ms", + self.diffuseTextureCount, + terrainGenerationTimeInMilliseconds + ) + + return self.groundMeshSections +end + +function RagnarokGND:GridPositionToCubeID(gridU, gridV) + if gridU <= 0 or gridV <= 0 or gridU > self.gridSizeU or gridV > self.gridSizeV then + return nil, format("Grid position (%d, %d) is out of bounds", gridU, gridV) + end + + return (gridU - 1) + (gridV - 1) * self.gridSizeU +end + +function RagnarokGND:GenerateSurfaceGeometry(surfaceConstructionInfo) + local gridU = surfaceConstructionInfo.gridU + local gridV = surfaceConstructionInfo.gridV + + local bottomLeftCorner, bottomRightCorner, topLeftCorner, topRightCorner + local bottomLeftVertexColor, bottomRightVertexColor, topLeftVertexColor, topRightVertexColor + + if surfaceConstructionInfo.facing == RagnarokGND.SURFACE_DIRECTION_UP then + bottomLeftCorner, bottomRightCorner, topLeftCorner, topRightCorner = self:GenerateGroundVertices(gridU, gridV) + + bottomLeftVertexColor = self:PickVertexColor(gridU, gridV) + bottomRightVertexColor = self:PickVertexColor(gridU + 1, gridV) + topLeftVertexColor = self:PickVertexColor(gridU, gridV + 1) + topRightVertexColor = self:PickVertexColor(gridU + 1, gridV + 1) + elseif surfaceConstructionInfo.facing == RagnarokGND.SURFACE_DIRECTION_NORTH then + bottomLeftCorner, bottomRightCorner, topLeftCorner, topRightCorner = + self:GenerateWallVerticesNorth(gridU, gridV) + + bottomLeftVertexColor = self:PickVertexColor(gridU, gridV + 1) + bottomRightVertexColor = self:PickVertexColor(gridU + 1, gridV + 1) + topLeftVertexColor = self:PickVertexColor(gridU, gridV + 1) + topRightVertexColor = self:PickVertexColor(gridU + 1, gridV + 1) + else -- Implicit: SURFACE_DIRECTION_EAST + bottomLeftCorner, bottomRightCorner, topLeftCorner, topRightCorner = self:GenerateWallVerticesEast(gridU, gridV) + + bottomLeftVertexColor = self:PickVertexColor(gridU + 1, gridV + 1) + bottomRightVertexColor = self:PickVertexColor(gridU + 1, gridV) + topLeftVertexColor = self:PickVertexColor(gridU + 1, gridV + 1) + topRightVertexColor = self:PickVertexColor(gridU + 1, gridV) + end + + local surface = surfaceConstructionInfo.surfaceDefinition + local mesh = self.groundMeshSections[surface.texture_id + 1] + local nextAvailableVertexID = #mesh.vertexPositions / 3 + + table_insert(mesh.vertexPositions, bottomLeftCorner.x) + table_insert(mesh.vertexPositions, bottomLeftCorner.y) + table_insert(mesh.vertexPositions, bottomLeftCorner.z) + table_insert(mesh.vertexPositions, bottomRightCorner.x) + table_insert(mesh.vertexPositions, bottomRightCorner.y) + table_insert(mesh.vertexPositions, bottomRightCorner.z) + table_insert(mesh.vertexPositions, topLeftCorner.x) + table_insert(mesh.vertexPositions, topLeftCorner.y) + table_insert(mesh.vertexPositions, topLeftCorner.z) + table_insert(mesh.vertexPositions, topRightCorner.x) + table_insert(mesh.vertexPositions, topRightCorner.y) + table_insert(mesh.vertexPositions, topRightCorner.z) + + table_insert(mesh.vertexColors, bottomLeftVertexColor.red / 255) + table_insert(mesh.vertexColors, bottomLeftVertexColor.green / 255) + table_insert(mesh.vertexColors, bottomLeftVertexColor.blue / 255) + table_insert(mesh.vertexColors, bottomRightVertexColor.red / 255) + table_insert(mesh.vertexColors, bottomRightVertexColor.green / 255) + table_insert(mesh.vertexColors, bottomRightVertexColor.blue / 255) + table_insert(mesh.vertexColors, topLeftVertexColor.red / 255) + table_insert(mesh.vertexColors, topLeftVertexColor.green / 255) + table_insert(mesh.vertexColors, topLeftVertexColor.blue / 255) + table_insert(mesh.vertexColors, topRightVertexColor.red / 255) + table_insert(mesh.vertexColors, topRightVertexColor.green / 255) + table_insert(mesh.vertexColors, topRightVertexColor.blue / 255) + + table_insert(mesh.triangleConnections, nextAvailableVertexID) + table_insert(mesh.triangleConnections, nextAvailableVertexID + 1) + table_insert(mesh.triangleConnections, nextAvailableVertexID + 2) + table_insert(mesh.triangleConnections, nextAvailableVertexID + 1) + table_insert(mesh.triangleConnections, nextAvailableVertexID + 3) + table_insert(mesh.triangleConnections, nextAvailableVertexID + 2) + + table_insert(mesh.diffuseTextureCoords, surface.uvs.bottom_left_u) + table_insert(mesh.diffuseTextureCoords, surface.uvs.bottom_left_v) + table_insert(mesh.diffuseTextureCoords, surface.uvs.bottom_right_u) + table_insert(mesh.diffuseTextureCoords, surface.uvs.bottom_right_v) + table_insert(mesh.diffuseTextureCoords, surface.uvs.top_left_u) + table_insert(mesh.diffuseTextureCoords, surface.uvs.top_left_v) + table_insert(mesh.diffuseTextureCoords, surface.uvs.top_right_u) + table_insert(mesh.diffuseTextureCoords, surface.uvs.top_right_v) +end + +function RagnarokGND:GenerateGroundVertices(gridU, gridV) + local cubeID = self:GridPositionToCubeID(gridU, gridV) + + assert(cubeID ~= nil, format("Failed to generate GROUND surface at (%d, %d)", gridU, gridV)) + + local cube = self.cubeGrid[cubeID] + + local bottomLeftCorner = {} + local bottomRightCorner = {} + local topLeftCorner = {} + local topRightCorner = {} + + bottomLeftCorner.x = (gridU - 1) * RagnarokGND.GAT_TILES_PER_GND_SURFACE + bottomLeftCorner.y = -1 * cube.southwest_corner_altitude * self.NORMALIZING_SCALE_FACTOR + bottomLeftCorner.z = (gridV - 1) * RagnarokGND.GAT_TILES_PER_GND_SURFACE + + bottomRightCorner.x = gridU * RagnarokGND.GAT_TILES_PER_GND_SURFACE + bottomRightCorner.y = -1 * cube.southeast_corner_altitude * self.NORMALIZING_SCALE_FACTOR + bottomRightCorner.z = (gridV - 1) * RagnarokGND.GAT_TILES_PER_GND_SURFACE + + topLeftCorner.x = (gridU - 1) * RagnarokGND.GAT_TILES_PER_GND_SURFACE + topLeftCorner.y = -1 * cube.northwest_corner_altitude * self.NORMALIZING_SCALE_FACTOR + topLeftCorner.z = (gridV + 0) * RagnarokGND.GAT_TILES_PER_GND_SURFACE + + topRightCorner.x = gridU * RagnarokGND.GAT_TILES_PER_GND_SURFACE + topRightCorner.y = -1 * cube.northeast_corner_altitude * self.NORMALIZING_SCALE_FACTOR + topRightCorner.z = gridV * RagnarokGND.GAT_TILES_PER_GND_SURFACE + + return bottomLeftCorner, bottomRightCorner, topLeftCorner, topRightCorner +end + +function RagnarokGND:GenerateWallVerticesNorth(gridU, gridV) + local cubeID = self:GridPositionToCubeID(gridU, gridV) + local adjacentCubeNorthID = self:GridPositionToCubeID(gridU, gridV + 1) + + assert(cubeID ~= nil, format("Failed to generate EAST surface at (%d, %d)", gridU, gridV)) + assert(adjacentCubeNorthID ~= nil, format("Failed to generate EAST surface at (%d, %d)", gridU, gridV)) + + local cube = self.cubeGrid[cubeID] + local adjacentCubeNorth = self.cubeGrid[adjacentCubeNorthID] + + local bottomLeftCorner = {} + local bottomRightCorner = {} + local topLeftCorner = {} + local topRightCorner = {} + + bottomLeftCorner.x = (gridU - 1) * RagnarokGND.GAT_TILES_PER_GND_SURFACE + bottomLeftCorner.y = -1 * cube.northwest_corner_altitude * self.NORMALIZING_SCALE_FACTOR + bottomLeftCorner.z = gridV * RagnarokGND.GAT_TILES_PER_GND_SURFACE + + bottomRightCorner.x = gridU * RagnarokGND.GAT_TILES_PER_GND_SURFACE + bottomRightCorner.y = -1 * cube.northeast_corner_altitude * self.NORMALIZING_SCALE_FACTOR + bottomRightCorner.z = gridV * RagnarokGND.GAT_TILES_PER_GND_SURFACE + + topLeftCorner.x = (gridU - 1) * RagnarokGND.GAT_TILES_PER_GND_SURFACE + topLeftCorner.y = -1 * adjacentCubeNorth.southwest_corner_altitude * self.NORMALIZING_SCALE_FACTOR + topLeftCorner.z = gridV * RagnarokGND.GAT_TILES_PER_GND_SURFACE + + topRightCorner.x = gridU * RagnarokGND.GAT_TILES_PER_GND_SURFACE + topRightCorner.y = -1 * adjacentCubeNorth.southeast_corner_altitude * self.NORMALIZING_SCALE_FACTOR + topRightCorner.z = gridV * RagnarokGND.GAT_TILES_PER_GND_SURFACE + + return bottomLeftCorner, bottomRightCorner, topLeftCorner, topRightCorner +end + +function RagnarokGND:GenerateWallVerticesEast(gridU, gridV) + local cubeID = self:GridPositionToCubeID(gridU, gridV) + local adjacentCubeEastID = self:GridPositionToCubeID(gridU + 1, gridV) + + assert(cubeID ~= nil, format("Failed to generate EAST surface at (%d, %d)", gridU, gridV)) + assert(adjacentCubeEastID ~= nil, format("Failed to generate EAST surface at (%d, %d)", gridU, gridV)) + + local cube = self.cubeGrid[cubeID] + local adjacentCubeEast = self.cubeGrid[adjacentCubeEastID] + + local bottomLeftCorner = {} + local bottomRightCorner = {} + local topLeftCorner = {} + local topRightCorner = {} + + bottomLeftCorner.x = gridU * RagnarokGND.GAT_TILES_PER_GND_SURFACE + bottomLeftCorner.y = -1 * cube.northeast_corner_altitude * self.NORMALIZING_SCALE_FACTOR + bottomLeftCorner.z = gridV * RagnarokGND.GAT_TILES_PER_GND_SURFACE + + bottomRightCorner.x = gridU * RagnarokGND.GAT_TILES_PER_GND_SURFACE + bottomRightCorner.y = -1 * cube.southeast_corner_altitude * self.NORMALIZING_SCALE_FACTOR + bottomRightCorner.z = (gridV - 1) * RagnarokGND.GAT_TILES_PER_GND_SURFACE + + topLeftCorner.x = gridU * RagnarokGND.GAT_TILES_PER_GND_SURFACE + topLeftCorner.y = -1 * adjacentCubeEast.northwest_corner_altitude * self.NORMALIZING_SCALE_FACTOR + topLeftCorner.z = gridV * RagnarokGND.GAT_TILES_PER_GND_SURFACE + + topRightCorner.x = gridU * RagnarokGND.GAT_TILES_PER_GND_SURFACE + topRightCorner.y = -1 * adjacentCubeEast.southwest_corner_altitude * self.NORMALIZING_SCALE_FACTOR + topRightCorner.z = (gridV - 1) * RagnarokGND.GAT_TILES_PER_GND_SURFACE + + return bottomLeftCorner, bottomRightCorner, topLeftCorner, topRightCorner +end + +function RagnarokGND:PickVertexColor(gridU, gridV) + local cubeID = self:GridPositionToCubeID(gridU, gridV) + if not cubeID then + -- No adjacent cube (grid position is out of bounds) + return RagnarokGND.FALLBACK_VERTEX_COLOR + end + + local cube = self.cubeGrid[cubeID] + if cube.top_surface_id == -1 then + -- No GROUND surface to copy from + return RagnarokGND.FALLBACK_VERTEX_COLOR + end + + local surface = self.texturedSurfaces[cube.top_surface_id] + return { + red = surface.bottom_left_color.red, + green = surface.bottom_left_color.green, + blue = surface.bottom_left_color.blue, + alpha = surface.bottom_left_color.alpha, + } +end + ffi.cdef(RagnarokGND.cdefs) return RagnarokGND diff --git a/Tests/FileFormats/RagnarokGND.spec.lua b/Tests/FileFormats/RagnarokGND.spec.lua index 8e097158..80ddafd4 100644 --- a/Tests/FileFormats/RagnarokGND.spec.lua +++ b/Tests/FileFormats/RagnarokGND.spec.lua @@ -43,8 +43,8 @@ describe("RagnarokGND", function() assertEquals(gnd.diffuseTextureCount, 2) assertEquals(gnd.texturePathLength, 80) - assertEquals(gnd.diffuseTexturePaths[0], "TEXTURE1.BMP") - assertEquals(gnd.diffuseTexturePaths[1], "somedir1\\texture2-01.bmp") + assertEquals(gnd.diffuseTexturePaths[1], "TEXTURE1.BMP") + assertEquals(gnd.diffuseTexturePaths[2], "somedir1\\texture2-01.bmp") assertEquals(gnd.lightmapFormat.numSlices, 4) assertEquals(gnd.lightmapFormat.pixelWidth, 8) From 7774f588ecabf65d9b9ebbc7f8187ff2d5e7a094 Mon Sep 17 00:00:00 2001 From: RDW Date: Sun, 17 Dec 2023 21:36:48 +0100 Subject: [PATCH 2/2] Tests: Add a snapshot test for the GND geometry generation I don't think anything else is feasible here, there's too much data. --- Tests/FileFormats/RagnarokGND.spec.lua | 104 +++ Tests/Fixtures/Snapshots/gnd-geometry.json | 810 +++++++++++++++++++++ 2 files changed, 914 insertions(+) create mode 100644 Tests/Fixtures/Snapshots/gnd-geometry.json diff --git a/Tests/FileFormats/RagnarokGND.spec.lua b/Tests/FileFormats/RagnarokGND.spec.lua index 80ddafd4..7f133a68 100644 --- a/Tests/FileFormats/RagnarokGND.spec.lua +++ b/Tests/FileFormats/RagnarokGND.spec.lua @@ -1,3 +1,5 @@ +local ffi = require("ffi") + local RagnarokGND = require("Core.FileFormats.RagnarokGND") local GND_WITHOUT_WATER_PLANE = C_FileSystem.ReadFile(path.join("Tests", "Fixtures", "no-water-plane.gnd")) @@ -127,4 +129,106 @@ describe("RagnarokGND", function() assertEquals(gnd.waterPlanes[0].texture_cycling_interval, 3) end) end) + + describe("GridPositionToCubeID", function() + local gnd = RagnarokGND() + gnd.gridSizeU = 3 + gnd.gridSizeV = 3 + it("should return the corresponding C-style cube index if a valid grid position was given", function() + assertEquals(gnd:GridPositionToCubeID(1, 1), 0) -- C arrays start at zero + assertEquals(gnd:GridPositionToCubeID(2, 1), 1) + assertEquals(gnd:GridPositionToCubeID(3, 1), 2) + assertEquals(gnd:GridPositionToCubeID(1, 2), 3) + assertEquals(gnd:GridPositionToCubeID(2, 2), 4) + assertEquals(gnd:GridPositionToCubeID(3, 2), 5) + assertEquals(gnd:GridPositionToCubeID(1, 3), 6) + assertEquals(gnd:GridPositionToCubeID(2, 3), 7) + assertEquals(gnd:GridPositionToCubeID(3, 3), 8) + end) + + it("should fail if the given grid position is out of bounds", function() + -- The purpose of this is to make sure OOB access raises an error, rather than potentially SEGFAULTing + assertFailure(function() + return gnd:GridPositionToCubeID(4, 1) + end, "Grid position (4, 1) is out of bounds") + + assertFailure(function() + return gnd:GridPositionToCubeID(1, 4) + end, "Grid position (1, 4) is out of bounds") + + assertFailure(function() + return gnd:GridPositionToCubeID(1, -1) + end, "Grid position (1, -1) is out of bounds") + + assertFailure(function() + return gnd:GridPositionToCubeID(-1, 1) + end, "Grid position (-1, 1) is out of bounds") + + assertFailure(function() + return gnd:GridPositionToCubeID(1, 0) + end, "Grid position (1, 0) is out of bounds") + + assertFailure(function() + return gnd:GridPositionToCubeID(0, 1) + end, "Grid position (0, 1) is out of bounds") + + assertFailure(function() + return gnd:GridPositionToCubeID(0, 0) + end, "Grid position (0, 0) is out of bounds") + end) + end) + + describe("GenerateGroundMeshSections", function() + it("should generate one ground mesh section per diffuse texture", function() + local gnd = RagnarokGND() + gnd.gridSizeU = 3 + gnd.gridSizeV = 3 + gnd.cubeGrid = ffi.new("gnd_groundmesh_cube_t[?]", gnd.gridSizeU * gnd.gridSizeV) + for gridV = 1, gnd.gridSizeV, 1 do + for gridU = 1, gnd.gridSizeU, 1 do + local cubeID = (gridU - 1) + (gridV - 1) * gnd.gridSizeU + local cube = gnd.cubeGrid[cubeID] + cube.southwest_corner_altitude = -4 * cubeID + 1 + cube.southeast_corner_altitude = -4 * cubeID + 2 + cube.northwest_corner_altitude = -4 * cubeID + 3 + cube.northeast_corner_altitude = -4 * cubeID + 4 + end + end + gnd.texturedSurfaces = ffi.new("gnd_textured_surface_t[?]", gnd.gridSizeU * gnd.gridSizeV) + gnd.texturedSurfaces[0].bottom_left_color.red = 255 + gnd.texturedSurfaces[0].bottom_left_color.green = 255 + gnd.texturedSurfaces[0].bottom_left_color.blue = 255 + gnd.texturedSurfaces[0].bottom_left_color.alpha = 255 + gnd.texturedSurfaces[0].uvs.bottom_left_u = 0 + gnd.texturedSurfaces[0].uvs.bottom_left_v = 0 + gnd.texturedSurfaces[0].uvs.bottom_right_u = 0.25 * 4 + gnd.texturedSurfaces[0].uvs.bottom_right_v = 0 + gnd.texturedSurfaces[0].uvs.top_left_u = 0 + gnd.texturedSurfaces[0].uvs.top_left_v = 0.25 * 4 + gnd.texturedSurfaces[0].uvs.top_right_u = 0.25 * 4 + gnd.texturedSurfaces[0].uvs.top_right_v = 0.25 * 4 + gnd.texturedSurfaceCount = gnd.gridSizeU * gnd.gridSizeV + gnd.diffuseTextureCount = 1 + gnd.diffuseTexturePaths = { + "whatever.bmp", + } + gnd.groundMeshSections = { + { + vertexPositions = {}, + vertexColors = {}, + triangleConnections = {}, + diffuseTextureCoords = {}, + }, + } + local sections = gnd:GenerateGroundMeshSections() + assertEquals(#sections, 1) -- Index starts at zero + assertEquals(table.count(sections), 1) + local json = require("json") + local jsonDump = json.prettier(sections) + -- Leaving this here because the snapshot likely needs to be recreated once lightmaps/normals are needed + -- C_FileSystem.WriteFile(path.join("Tests", "Fixtures", "Snapshots", "gnd-geometry.json"), jsonDump) + local snapshot = C_FileSystem.ReadFile(path.join("Tests", "Fixtures", "Snapshots", "gnd-geometry.json")) + assertEquals(json.parse(jsonDump), json.parse(snapshot)) -- Workaround for encoding issues in the CI :/ + end) + end) end) diff --git a/Tests/Fixtures/Snapshots/gnd-geometry.json b/Tests/Fixtures/Snapshots/gnd-geometry.json new file mode 100644 index 00000000..eb91fc03 --- /dev/null +++ b/Tests/Fixtures/Snapshots/gnd-geometry.json @@ -0,0 +1,810 @@ +[ + { + "diffuseTextureCoords": [ + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1 + ], + "triangleConnections": [ + 0, + 1, + 2, + 1, + 3, + 2, + 4, + 5, + 6, + 5, + 7, + 6, + 8, + 9, + 10, + 9, + 11, + 10, + 12, + 13, + 14, + 13, + 15, + 14, + 16, + 17, + 18, + 17, + 19, + 18, + 20, + 21, + 22, + 21, + 23, + 22, + 24, + 25, + 26, + 25, + 27, + 26, + 28, + 29, + 30, + 29, + 31, + 30, + 32, + 33, + 34, + 33, + 35, + 34, + 36, + 37, + 38, + 37, + 39, + 38, + 40, + 41, + 42, + 41, + 43, + 42, + 44, + 45, + 46, + 45, + 47, + 46, + 48, + 49, + 50, + 49, + 51, + 50, + 52, + 53, + 54, + 53, + 55, + 54, + 56, + 57, + 58, + 57, + 59, + 58, + 60, + 61, + 62, + 61, + 63, + 62, + 64, + 65, + 66, + 65, + 67, + 66, + 68, + 69, + 70, + 69, + 71, + 70, + 72, + 73, + 74, + 73, + 75, + 74, + 76, + 77, + 78, + 77, + 79, + 78, + 80, + 81, + 82, + 81, + 83, + 82 + ], + "vertexColors": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "vertexPositions": [ + 0, + -0.2, + 0, + 2, + -0.4, + 0, + 0, + -0.6000000000000001, + 2, + 2, + -0.8, + 2, + 0, + -0.6000000000000001, + 2, + 2, + -0.8, + 2, + 0, + 2.2, + 2, + 2, + 2, + 2, + 2, + -0.8, + 2, + 2, + -0.4, + 0, + 2, + 0.2, + 2, + 2, + 0.6000000000000001, + 0, + 2, + 0.6000000000000001, + 0, + 4, + 0.4, + 0, + 2, + 0.2, + 2, + 4, + 0, + 2, + 2, + 0.2, + 2, + 4, + 0, + 2, + 2, + 3, + 2, + 4, + 2.8000000000000004, + 2, + 4, + 0, + 2, + 4, + 0.4, + 0, + 4, + 1, + 2, + 4, + 1.4000000000000002, + 0, + 4, + 1.4000000000000002, + 0, + 6, + 1.2000000000000002, + 0, + 4, + 1, + 2, + 6, + 0.8, + 2, + 4, + 1, + 2, + 6, + 0.8, + 2, + 4, + 3.8000000000000004, + 2, + 6, + 3.6, + 2, + 0, + 2.2, + 2, + 2, + 2, + 2, + 0, + 1.8, + 4, + 2, + 1.6, + 4, + 0, + 1.8, + 4, + 2, + 1.6, + 4, + 0, + 4.6000000000000009, + 4, + 2, + 4.4, + 4, + 2, + 1.6, + 4, + 2, + 2, + 2, + 2, + 2.6, + 4, + 2, + 3, + 2, + 2, + 3, + 2, + 4, + 2.8000000000000004, + 2, + 2, + 2.6, + 4, + 4, + 2.4000000000000005, + 4, + 2, + 2.6, + 4, + 4, + 2.4000000000000005, + 4, + 2, + 5.4, + 4, + 4, + 5.2, + 4, + 4, + 2.4000000000000005, + 4, + 4, + 2.8000000000000004, + 2, + 4, + 3.4000000000000005, + 4, + 4, + 3.8000000000000004, + 2, + 4, + 3.8000000000000004, + 2, + 6, + 3.6, + 2, + 4, + 3.4000000000000005, + 4, + 6, + 3.2, + 4, + 4, + 3.4000000000000005, + 4, + 6, + 3.2, + 4, + 4, + 6.2, + 4, + 6, + 6, + 4, + 0, + 4.6000000000000009, + 4, + 2, + 4.4, + 4, + 0, + 4.2, + 6, + 2, + 4, + 6, + 2, + 4, + 6, + 2, + 4.4, + 4, + 2, + 5, + 6, + 2, + 5.4, + 4, + 2, + 5.4, + 4, + 4, + 5.2, + 4, + 2, + 5, + 6, + 4, + 4.800000000000001, + 6, + 4, + 4.800000000000001, + 6, + 4, + 5.2, + 4, + 4, + 5.800000000000001, + 6, + 4, + 6.2, + 4, + 4, + 6.2, + 4, + 6, + 6, + 4, + 4, + 5.800000000000001, + 6, + 6, + 5.6000000000000009, + 6 + ] + } +] \ No newline at end of file