Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement geometry generation for GND ground mesh sections #230

Merged
merged 2 commits into from
Dec 17, 2023
Merged
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
304 changes: 301 additions & 3 deletions Core/FileFormats/RagnarokGND.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = {},
}

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Loading