Optimized memory allocations/speed of terrain height lookup

This commit is contained in:
Josh-Larson
2025-05-21 02:03:31 -05:00
parent fdad8e7ad7
commit 39e4379864
@@ -1,3 +1,28 @@
/***********************************************************************************
* Copyright (c) 2025 /// Project SWG /// www.projectswg.com *
* *
* ProjectSWG is an emulation project for Star Wars Galaxies founded on *
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
* Our goal is to create one or more emulators which will provide servers for *
* players to continue playing a game similar to the one they used to play. *
* *
* This file is part of PSWGCommon. *
* *
* --------------------------------------------------------------------------------*
* *
* PSWGCommon is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation, either version 3 of the *
* License, or (at your option) any later version. *
* *
* PSWGCommon is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with PSWGCommon. If not, see <http://www.gnu.org/licenses/>. *
***********************************************************************************/
package com.projectswg.common.data.swgiff.parsers.terrain
import com.projectswg.common.data.location.Point2f
@@ -13,7 +38,7 @@ import kotlin.math.min
import kotlin.math.sqrt
class TerrainTemplate : SWGParser {
var name = ""
var mapWidth = 0f
var chunkWidth = 0f
@@ -44,16 +69,16 @@ class TerrainTemplate : SWGParser {
var farRadialTileBorder = 0f
var farRadialSeed = 0
var legacyMap = true
val fractalGroup = FractalGroup()
val bitmapGroup = BitmapGroup()
private val lookupInformation = TerrainInfoLookup(fractalGroup.fractals, bitmapGroup.bitmaps)
private val topTerrainLayer = TerrainListLayer()
override fun read(form: IffForm) {
assert(form.tag == "PTAT")
// all versions are supported
form.readChunk("DATA").use { chunk ->
name = chunk.readString()
mapWidth = chunk.readFloat()
@@ -64,7 +89,7 @@ class TerrainTemplate : SWGParser {
globalWaterTableShaderSize = chunk.readFloat()
globalWaterTableShaderTemplateName = chunk.readString()
environmentCycleTime = chunk.readFloat()
if (form.version == 13) {
chunk.readString()
chunk.readString()
@@ -78,7 +103,7 @@ class TerrainTemplate : SWGParser {
chunk.readInt()
chunk.readString()
}
collidableMinimumDistance = chunk.readFloat()
collidableMaximumDistance = chunk.readFloat()
collidableTileSize = chunk.readFloat()
@@ -99,16 +124,16 @@ class TerrainTemplate : SWGParser {
farRadialTileSize = chunk.readFloat()
farRadialTileBorder = chunk.readFloat()
farRadialSeed = chunk.readInt()
legacyMap = if (form.version >= 15) chunk.readBoolean() else true
}
form.readForm("TGEN").use { tgen ->
tgen.readForm("MGRP")?.use { fractalGroup.read(it) }
tgen.readForm("MGRP")?.use { bitmapGroup.read(it) }
// Load layers
tgen.readForm("LYRS").use { lyrs ->
tgen.readForm("LYRS").use { lyrs ->
lyrs.readAllForms("LAYR") { layer ->
layer.use {
val terrainListLayer = TerrainListLayer()
@@ -119,7 +144,7 @@ class TerrainTemplate : SWGParser {
}
}
}
override fun write(): IffForm {
val data = IffChunk("DATA")
data.writeString(name)
@@ -131,7 +156,7 @@ class TerrainTemplate : SWGParser {
data.writeFloat(globalWaterTableShaderSize)
data.writeString(globalWaterTableShaderTemplateName)
data.writeFloat(environmentCycleTime)
data.writeFloat(collidableMinimumDistance)
data.writeFloat(collidableMaximumDistance)
data.writeFloat(collidableTileSize)
@@ -152,74 +177,70 @@ class TerrainTemplate : SWGParser {
data.writeFloat(farRadialTileSize)
data.writeFloat(farRadialTileBorder)
data.writeInt(farRadialSeed)
if (!legacyMap)
data.writeBoolean(false) // Only use the new v15 format if it's not legacy
if (!legacyMap) data.writeBoolean(false) // Only use the new v15 format if it's not legacy
val version = if (legacyMap) 14 else 15
val children = ArrayList<IffForm>()
children.add(fractalGroup.write())
children.add(bitmapGroup.write())
val layerChildren = ArrayList<IffForm>();
val layerChildren = ArrayList<IffForm>()
for (child in topTerrainLayer.children) {
layerChildren.add(child.write())
}
children.add(IffForm.of("LYRS", layerChildren));
children.add(IffForm.of("LYRS", layerChildren))
val terrainGeneratorForm = IffForm.of("TGEN", 0, children)
return IffForm.of("PTAT", version, data, terrainGeneratorForm)
}
fun isBitmapReferenced(bitmapId: Int): Boolean {
return topTerrainLayer.isBitmapReferenced(bitmapId)
}
fun getHeight(x: Float, z: Float): TerrainInformation {
val waterHeight = getWaterHeight(x, z)
val terrainHeight = getTerrainHeight(x, z)
if (waterHeight.isNaN() || terrainHeight.height >= waterHeight)
return terrainHeight
if (waterHeight.isNaN() || terrainHeight.height >= waterHeight) return terrainHeight
return TerrainInformation(waterHeight, 0f, 1f, 0f)
}
fun getWaterHeight(x: Float, z: Float): Float {
var height = -Float.MAX_VALUE
if (useGlobalWaterTable)
height = globalWaterTableHeight
if (useGlobalWaterTable) height = globalWaterTableHeight
height = max(height, getWaterHeightRecursive(Point2f(x, z), topTerrainLayer))
return if (height == -Float.MAX_VALUE) Float.NaN else height
}
fun isWater(x: Float, z: Float): Boolean {
val waterHeight = getWaterHeight(x, z)
if (waterHeight.isNaN())
return false
if (waterHeight.isNaN()) return false
return getTerrainHeight(x, z).height <= waterHeight
}
fun getTerrainHeight(x: Float, z: Float): TerrainInformation {
// can be cached
val tileSize = chunkWidth / (2*numberOfTilesPerChunk)
val tileSize = chunkWidth / (2 * numberOfTilesPerChunk)
val halfMap = mapWidth / 2
// smart stuff
val tileLocalX = ((x + halfMap) % chunkWidth) / tileSize
val tileLocalZ = ((z + halfMap) % chunkWidth) / tileSize
val tileX = x - (tileLocalX - tileLocalX.toInt()) * tileSize
val tileZ = z - (tileLocalZ - tileLocalZ.toInt()) * tileSize
val angleRight = ((tileLocalX.toInt() xor tileLocalZ.toInt()) and 1) == 0
val sideLeft = when (angleRight) {
false -> ((tileLocalX - tileLocalX.toInt()) <= 1 - (tileLocalZ - tileLocalZ.toInt())) // left
true -> ((tileLocalX - tileLocalX.toInt()) <= (tileLocalZ - tileLocalZ.toInt())) // top
true -> ((tileLocalX - tileLocalX.toInt()) <= (tileLocalZ - tileLocalZ.toInt())) // top
}
val planeInfo = FloatArray(9)
planeInfo[0] = tileX
planeInfo[2] = tileZ
@@ -240,84 +261,48 @@ class TerrainTemplate : SWGParser {
}
}
// Set the y value for each vertex
planeInfo[1] = getHeightAt(planeInfo[0], planeInfo[2]).height
planeInfo[4] = getHeightAt(planeInfo[3], planeInfo[5]).height
planeInfo[7] = getHeightAt(planeInfo[6], planeInfo[8]).height
// Setup the a and b vectors for the cross product
planeInfo[1] = getHeightAt(planeInfo[0], planeInfo[2])
planeInfo[4] = getHeightAt(planeInfo[3], planeInfo[5])
planeInfo[7] = getHeightAt(planeInfo[6], planeInfo[8])
// Set up the a and b vectors for the cross product
for (i in 0..2) {
planeInfo[3+i] -= planeInfo[i]
planeInfo[6+i] -= planeInfo[i]
planeInfo[3 + i] -= planeInfo[i]
planeInfo[6 + i] -= planeInfo[i]
}
// Cross product! This is the plane normal
val cx = planeInfo[4] * planeInfo[8] - planeInfo[5] * planeInfo[7]
val cy = planeInfo[5] * planeInfo[6] - planeInfo[3] * planeInfo[8]
val cz = planeInfo[3] * planeInfo[7] - planeInfo[4] * planeInfo[6]
val planeOffset = planeInfo[0] * cx + planeInfo[1] * cy + planeInfo[2] * cz
val height = (planeOffset - cx * x - cz * z) / cy
return TerrainInformation(height, cx, cy, cz)
}
private fun getHeightAt(x: Float, z: Float): HeightInformation {
private fun getHeightAt(x: Float, z: Float): Float {
val p = Point2f(x, z)
val height = HeightInformation(1f, 0f)
getLayerHeight(topTerrainLayer, p, height)
return height
return getLayerHeight(topTerrainLayer, p, 1f, 0f)
}
private fun getLayerHeight(layer: TerrainListLayer, p: Point2f, height: HeightInformation) {
var transformValue = if (layer.boundaries.isEmpty()) 1f else 0f
val rectangle = layer.extent
for (boundary in layer.boundaries) {
if (!boundary.isContained(p))
continue
transformValue = max(transformValue, calculateFeathering(boundary.process(p), boundary.featherType))
}
if (layer.invertBoundaries)
transformValue = 1.0f - transformValue
for (filter in layer.filters) {
transformValue = min(transformValue, calculateFeathering(filter.process(p, transformValue, height.height, rectangle, lookupInformation), filter.featherType))
if (transformValue == 0f)
break
}
assert(transformValue in 0f..1f)
if (layer.invertFilters)
transformValue = 1.0f - transformValue
private fun getLayerHeight(layer: TerrainListLayer, p: Point2f, previousTransformValue: Float, previousHeight: Float): Float {
val transformValue = calculateTransformValue(lookupInformation, layer, p, previousHeight)
var newHeight = previousHeight
if (transformValue > 0f) {
for (affector in layer.heights) {
height.height = affector.process(p, transformValue * height.transformValue, height.height, lookupInformation)
newHeight = affector.process(p, transformValue * previousTransformValue, newHeight, lookupInformation)
}
}
val onlySubLayers = layer.boundaries.isEmpty() && layer.filters.isEmpty() && layer.heights.isEmpty()
if (onlySubLayers)
transformValue = 1f
val returnHeight = HeightInformation(transformValue * height.transformValue, height.height)
val newTransformValue = transformValue * previousTransformValue
for (child in layer.children) {
getLayerHeight(child, p, returnHeight)
newHeight = getLayerHeight(child, p, newTransformValue, newHeight)
}
height.height = returnHeight.height
return newHeight
}
private fun calculateFeathering(value: Float, featheringType: Int): Float {
return when (featheringType) {
0 -> value
1 -> value * value
2 -> sqrt(value)
3 -> value * value * (3 - 2 * value)
else -> 0f
}
}
private fun getWaterHeightRecursive(p: Point2f, layer: TerrainListLayer): Float {
var waterHeight = -Float.MAX_VALUE
for (boundary in layer.boundaries) {
@@ -332,8 +317,47 @@ class TerrainTemplate : SWGParser {
}
return waterHeight
}
data class TerrainInformation(val height: Float, val normalX: Float, val normalY: Float, val normalZ: Float)
private class HeightInformation(var transformValue: Float, var height: Float)
companion object {
private fun calculateTransformValue(lookupInformation: TerrainInfoLookup, layer: TerrainListLayer, p: Point2f, height: Float): Float {
var transformValue = if (layer.boundaries.isEmpty()) 1f else 0f
val rectangle = layer.extent
for (boundary in layer.boundaries) {
if (!boundary.isContained(p)) continue
transformValue = max(transformValue, calculateFeathering(boundary.process(p), boundary.featherType))
}
if (layer.invertBoundaries) transformValue = 1.0f - transformValue
for (filter in layer.filters) {
transformValue = min(transformValue, calculateFeathering(filter.process(p, transformValue, height, rectangle, lookupInformation), filter.featherType))
if (transformValue == 0f) break
}
if (layer.invertFilters) transformValue = 1.0f - transformValue
val onlySubLayers = layer.boundaries.isEmpty() && layer.filters.isEmpty() && layer.heights.isEmpty()
if (onlySubLayers) transformValue = 1f
assert(transformValue in 0f..1f)
return transformValue
}
private fun calculateFeathering(value: Float, featheringType: Int): Float {
return when (featheringType) {
0 -> value
1 -> value * value
2 -> sqrt(value)
3 -> value * value * (3 - 2 * value)
else -> 0f
}
}
}
}