// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2022 // www.magicbane.com package engine.InterestManagement; import engine.gameManager.ZoneManager; import engine.math.Vector2f; import engine.math.Vector3fImmutable; import engine.objects.Zone; import org.pmw.tinylog.Logger; import java.util.HashMap; import static java.lang.Math.PI; public class Terrain { // Class variables public static final HashMap _heightmap_pixel_cache = new HashMap<>(); public short[][] terrain_pixel_data; public Vector2f terrain_size = new Vector2f(); public Vector2f cell_size = new Vector2f(); public Vector2f cell_count = new Vector2f(); public float terrain_scale; public float min_blend; public float max_blend; public int heightmap; Zone zone; public Terrain(Zone zone) { this.zone = zone; this.heightmap = this.zone.terrain_image; // Configure PLANAR if (this.zone.terrain_type.equals("PLANAR")) this.heightmap = 1006300; this.terrain_size.x = this.zone.major_radius * 2; this.terrain_size.y = this.zone.minor_radius * 2; this.terrain_pixel_data = Terrain._heightmap_pixel_cache.get(heightmap); if (terrain_pixel_data == null) Logger.error("Pixel map empty for zone: " + zone.getObjectUUID() + ":" + zone.zoneName); this.cell_count.x = this.terrain_pixel_data.length - 1; this.cell_count.y = this.terrain_pixel_data[0].length - 1; this.cell_size.x = terrain_size.x / this.cell_count.x; this.cell_size.y = terrain_size.y / this.cell_count.y; this.max_blend = this.zone.max_blend / this.zone.major_radius; this.min_blend = this.zone.min_blend / this.zone.minor_radius; this.terrain_scale = this.zone.terrain_max_y / 255f; } public static Zone getNextZoneWithTerrain(Zone zone) { Zone terrain_zone = zone; if (zone == null) return ZoneManager.seaFloor; if (zone.terrain != null) return zone; if (zone.equals(ZoneManager.seaFloor)) return zone; while (terrain_zone.terrain == null) terrain_zone = terrain_zone.parent; return terrain_zone; } public static float getWorldHeight(Zone currentZone, Vector3fImmutable worldLoc) { Zone terrainZone; Zone parentZone; // Retrieve the next zone with a heightmap attached. // Zones without a heightmap use the next zone up the // tree to calculate heights from. terrainZone = getNextZoneWithTerrain(currentZone); parentZone = getNextZoneWithTerrain(currentZone.parent); // Transform world loc into zone space coordinate system Vector2f terrainLoc = ZoneManager.worldToZoneSpace(worldLoc, terrainZone); Vector2f parentLoc = ZoneManager.worldToZoneSpace(worldLoc, parentZone); // Interpolate height for this position in terrain float interpolatedChildHeight = terrainZone.terrain.getInterpolatedTerrainHeight(terrainLoc); interpolatedChildHeight += terrainZone.worldAltitude; // Interpolate height for this position in parent float interpolatedParentTerrainHeight = parentZone.terrain.getInterpolatedTerrainHeight(parentLoc); interpolatedParentTerrainHeight += parentZone.worldAltitude; // Blend between heights return interpolatedChildHeight + interpolatedParentTerrainHeight * (1 - terrainZone.terrain.heightBlend(terrainLoc)); } public static float getWorldHeight(Vector3fImmutable worldLoc) { Zone currentZone = ZoneManager.findSmallestZone(worldLoc); if (currentZone == null) return 0; return getWorldHeight(currentZone, worldLoc); } public Vector2f getTerrainCell(Vector2f terrainLoc) { Vector2f terrain_cell = new Vector2f(terrainLoc.x / this.cell_size.x, terrainLoc.y / this.cell_size.y); // Clamp values when standing directly on max pole if (terrain_cell.x >= this.cell_count.x) terrain_cell.x = terrain_cell.x - 1; if (terrain_cell.x >= this.cell_count.y) terrain_cell.y = terrain_cell.y - 1; return terrain_cell; } public float getInterpolatedTerrainHeight(Vector2f terrainLoc) { float interpolatedHeight; Vector2f terrain_cell = getTerrainCell(terrainLoc); int gridX = (int) Math.floor(terrain_cell.x); int gridY = (int) Math.floor(terrain_cell.y); float offsetX = terrain_cell.x % 1; float offsetY = terrain_cell.y % 1; //get 4 surrounding vertices from the pixel array. float topLeftHeight; float topRightHeight; float bottomLeftHeight; float bottomRightHeight; topLeftHeight = terrain_pixel_data[gridX][gridY]; topRightHeight = terrain_pixel_data[gridX + 1][gridY]; bottomLeftHeight = terrain_pixel_data[gridX][gridY + 1]; bottomRightHeight = terrain_pixel_data[gridX + 1][gridY + 1]; // Interpolate between the 4 vertices interpolatedHeight = topLeftHeight * (1 - offsetX) * (1 - offsetY); interpolatedHeight += topRightHeight * (1 - offsetY) * (offsetX); interpolatedHeight += (bottomLeftHeight * (1 - offsetX) * offsetY); interpolatedHeight += (bottomRightHeight * offsetY * offsetX); interpolatedHeight *= this.terrain_scale; // Scale height return interpolatedHeight; } public float heightBlend(Vector2f terrainLoc) { // Normalize terrain loc Vector2f normalizedLoc = new Vector2f(terrainLoc.x / this.terrain_size.x, terrainLoc.y / terrain_size.y); float minp = this.zone.min_blend / this.zone.major_radius; float maxp = this.zone.max_blend / this.zone.major_radius; float xval; if (minp > 0.4f) xval = minp; else xval = Math.min(maxp, 0.4f); float minpy = this.zone.min_blend / this.zone.minor_radius; float maxpy = this.zone.max_blend / this.zone.minor_radius; float yval; if (minpy > 0.4f) yval = minpy; else yval = Math.min(maxpy, 0.4f); float value; if (normalizedLoc.x <= 1 - xval || normalizedLoc.x <= normalizedLoc.y) { if (normalizedLoc.x < 1 - yval) return 1; value = (normalizedLoc.y - (1 - yval)) / yval; } else value = (normalizedLoc.x - (1 - xval)) / xval; value = (float) Math.atan((0.5f - value) * PI); return (value + 1) * 0.5f; } }