mirror of
https://github.com/ProjectSWGCore/pswgcommon.git
synced 2026-07-13 22:00:57 -04:00
Optimized memory allocations/speed of terrain height lookup
This commit is contained in:
+127
-103
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user