// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // 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 heightmapByLoadNum = new HashMap<>(); public static final HashMap _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.zoneTemplate, 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.blendBounds) == 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.blendBounds.getHalfExtents().x) * (heightMapZone.blendBounds.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 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; } }