|
|
|
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
|
|
|
|
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
|
|
|
|
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
|
|
|
|
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
|
|
|
|
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
|
|
|
|
// Magicbane Emulator Project © 2013 - 2022
|
|
|
|
// www.magicbane.com
|
|
|
|
|
|
|
|
package engine.InterestManagement;
|
|
|
|
|
|
|
|
import engine.Enum;
|
|
|
|
import engine.gameManager.ConfigManager;
|
|
|
|
import engine.gameManager.DbManager;
|
|
|
|
import engine.gameManager.ZoneManager;
|
|
|
|
import engine.math.Bounds;
|
|
|
|
import engine.math.FastMath;
|
|
|
|
import engine.math.Vector2f;
|
|
|
|
import engine.math.Vector3fImmutable;
|
|
|
|
import engine.objects.Zone;
|
|
|
|
import org.pmw.tinylog.Logger;
|
|
|
|
|
|
|
|
import javax.imageio.ImageIO;
|
|
|
|
import java.awt.*;
|
|
|
|
import java.awt.image.BufferedImage;
|
|
|
|
import java.io.File;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.nio.file.Files;
|
|
|
|
import java.nio.file.Path;
|
|
|
|
import java.nio.file.Paths;
|
|
|
|
import java.sql.ResultSet;
|
|
|
|
import java.sql.SQLException;
|
|
|
|
import java.util.HashMap;
|
|
|
|
import java.util.stream.Stream;
|
|
|
|
|
|
|
|
import static java.lang.Math.abs;
|
|
|
|
|
|
|
|
public class HeightMap {
|
|
|
|
|
|
|
|
// Class variables
|
|
|
|
|
|
|
|
public static final HashMap<Integer, HeightMap> heightmapByLoadNum = new HashMap<>();
|
|
|
|
|
|
|
|
public static final HashMap<String, int[][]> _pixelData = new HashMap<>();
|
|
|
|
|
|
|
|
// Heightmap data for all zones.
|
|
|
|
public static float SCALEVALUE = 1.0f / 255;
|
|
|
|
|
|
|
|
// Bootstrap Tracking
|
|
|
|
public static int heightMapsCreated = 0;
|
|
|
|
public static HeightMap PlayerCityHeightMap;
|
|
|
|
|
|
|
|
// Heightmap data for this heightmap
|
|
|
|
public final int heightMapID;
|
|
|
|
public final int maxHeight;
|
|
|
|
public final int fullExtentsX;
|
|
|
|
public final int fullExtentsY;
|
|
|
|
public final int zoneLoadID;
|
|
|
|
public BufferedImage heightmapImage;
|
|
|
|
public float bucketWidthX;
|
|
|
|
public float bucketWidthY;
|
|
|
|
public float seaLevel = 0;
|
|
|
|
public int[][] pixelColorValues;
|
|
|
|
|
|
|
|
public float zone_minBlend;
|
|
|
|
public float zone_maxBlend;
|
|
|
|
|
|
|
|
public HeightMap(ResultSet rs) throws SQLException {
|
|
|
|
|
|
|
|
this.heightMapID = rs.getInt("heightMapID");
|
|
|
|
this.maxHeight = rs.getInt("maxHeight");
|
|
|
|
int halfExtentsX = rs.getInt("xRadius");
|
|
|
|
int halfExtentsY = rs.getInt("zRadius");
|
|
|
|
this.zoneLoadID = rs.getInt("zoneLoadID");
|
|
|
|
this.seaLevel = rs.getFloat("seaLevel");
|
|
|
|
|
|
|
|
this.zone_minBlend = rs.getFloat("outsetZ");
|
|
|
|
this.zone_maxBlend = rs.getFloat("outsetX");
|
|
|
|
|
|
|
|
// Cache the full extents to avoid the calculation
|
|
|
|
|
|
|
|
this.fullExtentsX = halfExtentsX * 2;
|
|
|
|
this.fullExtentsY = halfExtentsY * 2;
|
|
|
|
|
|
|
|
this.heightmapImage = null;
|
|
|
|
File imageFile = new File(ConfigManager.DEFAULT_DATA_DIR + "heightmaps/" + this.heightMapID + ".bmp");
|
|
|
|
|
|
|
|
// early exit if no image file was found. Will log in caller.
|
|
|
|
|
|
|
|
if (!imageFile.exists())
|
|
|
|
return;
|
|
|
|
|
|
|
|
// load the heightmap image.
|
|
|
|
|
|
|
|
try {
|
|
|
|
this.heightmapImage = ImageIO.read(imageFile);
|
|
|
|
} catch (IOException e) {
|
|
|
|
Logger.error("***Error loading heightmap data for heightmap " + this.heightMapID + e);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate the data we do not load from table
|
|
|
|
|
|
|
|
float numOfBucketsX = this.heightmapImage.getWidth() - 1;
|
|
|
|
float calculatedWidthX = this.fullExtentsX / numOfBucketsX;
|
|
|
|
this.bucketWidthX = calculatedWidthX;
|
|
|
|
|
|
|
|
float numOfBucketsY = this.heightmapImage.getHeight() - 1;
|
|
|
|
float calculatedWidthY = this.fullExtentsY / numOfBucketsY;
|
|
|
|
this.bucketWidthY = calculatedWidthY;
|
|
|
|
|
|
|
|
// Generate pixel array from image data
|
|
|
|
|
|
|
|
generatePixelData(this);
|
|
|
|
|
|
|
|
HeightMap.heightmapByLoadNum.put(this.zoneLoadID, this);
|
|
|
|
|
|
|
|
heightMapsCreated++;
|
|
|
|
}
|
|
|
|
|
|
|
|
//Created for PlayerCities
|
|
|
|
public HeightMap() {
|
|
|
|
|
|
|
|
this.heightMapID = 999999;
|
|
|
|
this.maxHeight = 5; // for real...
|
|
|
|
int halfExtentsX = (int) Enum.CityBoundsType.ZONE.halfExtents;
|
|
|
|
int halfExtentsY = (int) Enum.CityBoundsType.ZONE.halfExtents;
|
|
|
|
this.zoneLoadID = 0;
|
|
|
|
this.seaLevel = 0;
|
|
|
|
this.zone_minBlend = 0;
|
|
|
|
this.zone_maxBlend = 0;
|
|
|
|
|
|
|
|
// Cache the full extents to avoid the calculation
|
|
|
|
|
|
|
|
this.fullExtentsX = halfExtentsX * 2;
|
|
|
|
this.fullExtentsY = halfExtentsY * 2;
|
|
|
|
|
|
|
|
this.heightmapImage = null;
|
|
|
|
|
|
|
|
// Calculate the data we do not load from table
|
|
|
|
|
|
|
|
this.bucketWidthX = halfExtentsX;
|
|
|
|
this.bucketWidthY = halfExtentsY;
|
|
|
|
|
|
|
|
this.pixelColorValues = new int[this.fullExtentsX][this.fullExtentsY];
|
|
|
|
|
|
|
|
for (int y = 0; y < this.fullExtentsY; y++) {
|
|
|
|
for (int x = 0; x < this.fullExtentsX; x++) {
|
|
|
|
pixelColorValues[x][y] = 255;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
public HeightMap(Zone zone) {
|
|
|
|
|
|
|
|
this.heightMapID = 999999;
|
|
|
|
this.maxHeight = 0;
|
|
|
|
int halfExtentsX = (int) zone.bounds.getHalfExtents().x;
|
|
|
|
int halfExtentsY = (int) zone.bounds.getHalfExtents().y;
|
|
|
|
this.zoneLoadID = 0;
|
|
|
|
this.seaLevel = 0;
|
|
|
|
|
|
|
|
// Cache the full extents to avoid the calculation
|
|
|
|
|
|
|
|
this.fullExtentsX = halfExtentsX * 2;
|
|
|
|
this.fullExtentsY = halfExtentsY * 2;
|
|
|
|
|
|
|
|
this.heightmapImage = null;
|
|
|
|
|
|
|
|
// Calculate the data we do not load from table
|
|
|
|
|
|
|
|
this.bucketWidthX = halfExtentsX;
|
|
|
|
this.bucketWidthY = halfExtentsY;
|
|
|
|
|
|
|
|
this.pixelColorValues = new int[this.fullExtentsX][this.fullExtentsY];
|
|
|
|
|
|
|
|
for (int y = 0; y < this.fullExtentsY; y++) {
|
|
|
|
for (int x = 0; x < this.fullExtentsX; x++) {
|
|
|
|
pixelColorValues[x][y] = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void GeneratePlayerCityHeightMap() {
|
|
|
|
|
|
|
|
HeightMap.PlayerCityHeightMap = new HeightMap();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void GenerateCustomHeightMap(Zone zone) {
|
|
|
|
|
|
|
|
HeightMap heightMap = new HeightMap(zone);
|
|
|
|
|
|
|
|
HeightMap.heightmapByLoadNum.put(zone.loadNum, heightMap);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
public static Zone getNextZoneWithTerrain(Zone zone) {
|
|
|
|
|
|
|
|
Zone nextZone = zone;
|
|
|
|
|
|
|
|
if (zone.getHeightMap() != null)
|
|
|
|
return zone;
|
|
|
|
|
|
|
|
if (zone.equals(ZoneManager.getSeaFloor()))
|
|
|
|
return zone;
|
|
|
|
|
|
|
|
while (nextZone.getHeightMap() == null)
|
|
|
|
nextZone = nextZone.parent;
|
|
|
|
|
|
|
|
return nextZone;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static float getWorldHeight(Zone currentZone, Vector3fImmutable worldLoc) {
|
|
|
|
|
|
|
|
Zone heightMapZone;
|
|
|
|
|
|
|
|
// Seafloor is rather flat.
|
|
|
|
|
|
|
|
if (currentZone == ZoneManager.getSeaFloor())
|
|
|
|
return currentZone.worldAltitude;
|
|
|
|
|
|
|
|
// Retrieve the next zone with a heightmap attached.
|
|
|
|
// Zones without a heightmap use the next zone up the
|
|
|
|
// tree to calculate heights from.
|
|
|
|
|
|
|
|
heightMapZone = getNextZoneWithTerrain(currentZone);
|
|
|
|
|
|
|
|
// Transform world loc into zone space coordinate system
|
|
|
|
|
|
|
|
Vector2f zoneLoc = ZoneManager.worldToZoneSpace(worldLoc, heightMapZone);
|
|
|
|
|
|
|
|
// Interpolate height for this position using pixel array.
|
|
|
|
|
|
|
|
float interpolatedTerrainHeight = heightMapZone.getHeightMap().getInterpolatedTerrainHeight(zoneLoc);
|
|
|
|
interpolatedTerrainHeight += heightMapZone.worldAltitude;
|
|
|
|
|
|
|
|
// Heightmap blending is based on distance to edge of zone.
|
|
|
|
|
|
|
|
if (Bounds.collide(worldLoc, heightMapZone.maxBlend) == true)
|
|
|
|
return interpolatedTerrainHeight;
|
|
|
|
|
|
|
|
// We will need the parent height if we got this far into the method
|
|
|
|
|
|
|
|
return interpolatePLANAR(worldLoc, heightMapZone, zoneLoc, interpolatedTerrainHeight);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private static float interpolatePLANAR(Vector3fImmutable worldLoc, Zone heightMapZone, Vector2f zoneLoc, float interpolatedTerrainHeight) {
|
|
|
|
|
|
|
|
// zones of type PLANAR
|
|
|
|
|
|
|
|
Zone parentZone;
|
|
|
|
float interpolatedParentTerrainHeight;
|
|
|
|
|
|
|
|
parentZone = HeightMap.getNextZoneWithTerrain(heightMapZone.parent);
|
|
|
|
|
|
|
|
interpolatedParentTerrainHeight = HeightMap.getWorldHeight(parentZone, worldLoc);
|
|
|
|
interpolatedParentTerrainHeight += parentZone.worldAltitude;
|
|
|
|
|
|
|
|
Bounds blendBounds = Bounds.borrow();
|
|
|
|
zoneLoc.x = abs(zoneLoc.x);
|
|
|
|
zoneLoc.y = abs(zoneLoc.x);
|
|
|
|
|
|
|
|
blendBounds.setBounds(new Vector2f(heightMapZone.absX, heightMapZone.absZ), zoneLoc, 0.0f);
|
|
|
|
|
|
|
|
float maxBlendArea = (heightMapZone.maxBlend.getHalfExtents().x) *
|
|
|
|
(heightMapZone.maxBlend.getHalfExtents().y);
|
|
|
|
float currentArea = (blendBounds.getHalfExtents().x) *
|
|
|
|
(blendBounds.getHalfExtents().y);
|
|
|
|
float zoneArea = (heightMapZone.bounds.getHalfExtents().x) *
|
|
|
|
(heightMapZone.bounds.getHalfExtents().y);
|
|
|
|
|
|
|
|
blendBounds.release();
|
|
|
|
|
|
|
|
float blendDelta = zoneArea - maxBlendArea;
|
|
|
|
float currentDelta = zoneArea - currentArea;
|
|
|
|
|
|
|
|
float percentage;
|
|
|
|
|
|
|
|
if (currentDelta != 0 && blendDelta != 0)
|
|
|
|
percentage = currentDelta / blendDelta;
|
|
|
|
else
|
|
|
|
percentage = 0.0f;
|
|
|
|
|
|
|
|
interpolatedTerrainHeight = FastMath.LERP(percentage, interpolatedTerrainHeight, interpolatedParentTerrainHeight);
|
|
|
|
return interpolatedTerrainHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static float getWorldHeight(Vector3fImmutable worldLoc) {
|
|
|
|
|
|
|
|
Zone currentZone = ZoneManager.findSmallestZone(worldLoc);
|
|
|
|
|
|
|
|
if (currentZone == null)
|
|
|
|
return 0;
|
|
|
|
return getWorldHeight(currentZone, worldLoc);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void loadAlHeightMaps() {
|
|
|
|
|
|
|
|
// Load the heightmaps into staging hashmap keyed by HashMapID
|
|
|
|
|
|
|
|
DbManager.HeightMapQueries.LOAD_ALL_HEIGHTMAPS();
|
|
|
|
|
|
|
|
//generate static player city heightmap.
|
|
|
|
|
|
|
|
HeightMap.GeneratePlayerCityHeightMap();
|
|
|
|
|
|
|
|
// Clear all heightmap image data as it's no longer needed.
|
|
|
|
|
|
|
|
for (HeightMap heightMap : HeightMap.heightmapByLoadNum.values()) {
|
|
|
|
heightMap.heightmapImage = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
Logger.info(HeightMap.heightmapByLoadNum.size() + " Heightmaps cached.");
|
|
|
|
|
|
|
|
// Load pixel data for heightmaps
|
|
|
|
|
|
|
|
try (Stream<Path> filePathStream = Files.walk(Paths.get(ConfigManager.DEFAULT_DATA_DIR + "heightmaps/TARGA/"))) {
|
|
|
|
filePathStream.forEach(filePath -> {
|
|
|
|
|
|
|
|
if (Files.isRegularFile(filePath)) {
|
|
|
|
File imageFile = filePath.toFile();
|
|
|
|
|
|
|
|
try {
|
|
|
|
BufferedImage heightmapImage = ImageIO.read(imageFile);
|
|
|
|
|
|
|
|
// Generate pixel for this heightmap. RPG channels are all the same
|
|
|
|
// in this greyscale TGA heightmap. We will choose red.
|
|
|
|
|
|
|
|
int[][] colorData = new int[heightmapImage.getWidth()][heightmapImage.getHeight()];
|
|
|
|
|
|
|
|
for (int y = 0; y < heightmapImage.getHeight(); y++) {
|
|
|
|
for (int x = 0; x < heightmapImage.getWidth(); x++) {
|
|
|
|
|
|
|
|
Color color = new Color(heightmapImage.getRGB(x, y));
|
|
|
|
colorData[x][y] = color.getRed();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Insert color data into lookup table
|
|
|
|
|
|
|
|
_pixelData.put(imageFile.getName().substring(0, imageFile.getName().lastIndexOf(".")), colorData);
|
|
|
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
Logger.error(e);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} catch (IOException e) {
|
|
|
|
Logger.error(e);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private static void generatePixelData(HeightMap heightMap) {
|
|
|
|
|
|
|
|
Color color;
|
|
|
|
|
|
|
|
// Generate altitude lookup table for this heightmap
|
|
|
|
|
|
|
|
heightMap.pixelColorValues = new int[heightMap.heightmapImage.getWidth()][heightMap.heightmapImage.getHeight()];
|
|
|
|
|
|
|
|
for (int y = 0; y < heightMap.heightmapImage.getHeight(); y++) {
|
|
|
|
for (int x = 0; x < heightMap.heightmapImage.getWidth(); x++) {
|
|
|
|
|
|
|
|
color = new Color(heightMap.heightmapImage.getRGB(x, y));
|
|
|
|
heightMap.pixelColorValues[x][y] = color.getRed();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
public Vector2f getGridSquare(Vector2f zoneLoc) {
|
|
|
|
|
|
|
|
// Clamp values.
|
|
|
|
|
|
|
|
if (zoneLoc.x < 0)
|
|
|
|
zoneLoc.setX(0);
|
|
|
|
|
|
|
|
if (zoneLoc.x >= this.fullExtentsX)
|
|
|
|
zoneLoc.setX(this.fullExtentsX);
|
|
|
|
|
|
|
|
if (zoneLoc.y < 0)
|
|
|
|
zoneLoc.setY(0);
|
|
|
|
|
|
|
|
if (zoneLoc.y > this.fullExtentsY)
|
|
|
|
zoneLoc.setY(this.fullExtentsY);
|
|
|
|
|
|
|
|
// Flip Y coordinates
|
|
|
|
|
|
|
|
zoneLoc.setY(this.fullExtentsY - zoneLoc.y);
|
|
|
|
|
|
|
|
float xBucket = (zoneLoc.x / this.bucketWidthX);
|
|
|
|
float yBucket = (zoneLoc.y / this.bucketWidthY);
|
|
|
|
|
|
|
|
return new Vector2f(xBucket, yBucket);
|
|
|
|
}
|
|
|
|
|
|
|
|
public float getInterpolatedTerrainHeight(Vector2f zoneLoc) {
|
|
|
|
|
|
|
|
float interpolatedHeight;
|
|
|
|
|
|
|
|
Vector2f gridSquare = getGridSquare(zoneLoc);
|
|
|
|
|
|
|
|
int gridX = (int) gridSquare.x;
|
|
|
|
int gridY = (int) gridSquare.y;
|
|
|
|
|
|
|
|
//get 4 surrounding vertices from the pixel array.
|
|
|
|
|
|
|
|
float topLeftHeight;
|
|
|
|
float topRightHeight;
|
|
|
|
float bottomLeftHeight;
|
|
|
|
float bottomRightHeight;
|
|
|
|
|
|
|
|
topLeftHeight = pixelColorValues[gridX][gridY];
|
|
|
|
topRightHeight = pixelColorValues[gridX + 1][gridY];
|
|
|
|
bottomLeftHeight = pixelColorValues[gridX][gridY + 1];
|
|
|
|
bottomRightHeight = pixelColorValues[gridX + 1][gridY + 1];
|
|
|
|
|
|
|
|
// Interpolate between the 4 vertices
|
|
|
|
|
|
|
|
float offsetX = (gridSquare.x - gridX);
|
|
|
|
float offsetY = gridSquare.y - gridY;
|
|
|
|
|
|
|
|
interpolatedHeight = topRightHeight * (1 - offsetY) * (offsetX);
|
|
|
|
interpolatedHeight += (bottomRightHeight * offsetY * offsetX);
|
|
|
|
interpolatedHeight += (bottomLeftHeight * (1 - offsetX) * offsetY);
|
|
|
|
interpolatedHeight += (topLeftHeight * (1 - offsetX) * (1 - offsetY));
|
|
|
|
|
|
|
|
interpolatedHeight *= (float) this.maxHeight * SCALEVALUE; // Scale height
|
|
|
|
|
|
|
|
return interpolatedHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|