// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // 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 { 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 Vector2f blend_ratio = new Vector2f(); public int heightmap; Zone zone; public Terrain(Zone zone) { this.zone = zone; this.heightmap = this.zone.terrain_image; // Configure PLANAR zones to use the same 16x16 pixel image // that all similar terrains share. (See JSON) if (this.zone.terrain_type.equals("PLANAR")) this.heightmap = 1006301; // all 0 // Load pixel data for this terrain from cache this.terrain_pixel_data = Terrain._heightmap_pixel_cache.get(heightmap); if (terrain_pixel_data == null) Logger.error("Pixel map empty for zone: " + this.zone.getObjectUUID() + ":" + this.zone.zoneName); // Configure terrain based on zone properties this.terrain_size.x = this.zone.major_radius * 2; this.terrain_size.y = this.zone.minor_radius * 2; 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; // Blending configuration. These ratios are used to calculate // the blending area between child and parent terrains when // they are stitched together. Vector2f major_blend = new Vector2f(this.zone.max_blend / this.zone.major_radius, this.zone.min_blend / this.zone.major_radius); Vector2f minor_blend = new Vector2f(this.zone.max_blend / this.zone.minor_radius, this.zone.min_blend / this.zone.minor_radius); if (major_blend.y > 0.4f) blend_ratio.x = major_blend.y; else blend_ratio.x = Math.min(major_blend.x, 0.4f); if (minor_blend.y > 0.4f) blend_ratio.y = minor_blend.y; else blend_ratio.y = Math.min(minor_blend.x, 0.4f); // Scale coefficient for this terrain this.terrain_scale = this.zone.terrain_max_y / 255f; } public static Zone getNextZoneWithTerrain(Zone zone) { // Not all zones have a terrain. Some are for display only // and heights returned are from the parent heightmap. This // is controlled in the JSON via the has_terrain_gen field. 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 zone, Vector3fImmutable world_loc) { // Retrieve the next zone with a terrain defined. Zone terrainZone = getNextZoneWithTerrain(zone); Zone parentZone = getNextZoneWithTerrain(zone.parent); // Transform world loc into zone space coordinate system Vector2f terrainLoc = ZoneManager.worldToTerrainSpace(world_loc, terrainZone); Vector2f parentLoc = ZoneManager.worldToTerrainSpace(world_loc, parentZone); // Offset from origin needed for blending function Vector2f terrainOffset = ZoneManager.worldToZoneOffset(world_loc, terrainZone); // Interpolate height for this position in both terrains float interpolatedChildHeight = terrainZone.terrain.getInterpolatedTerrainHeight(terrainLoc); interpolatedChildHeight += terrainZone.global_height; float interpolatedParentTerrainHeight = parentZone.terrain.getInterpolatedTerrainHeight(parentLoc); interpolatedParentTerrainHeight += parentZone.global_height; // Blend between terrains float blendCoefficient = terrainZone.terrain.getTerrainBlendCoefficient(terrainOffset); float terrainHeight = interpolatedChildHeight * blendCoefficient; terrainHeight += interpolatedParentTerrainHeight * (1 - blendCoefficient); return terrainHeight; } public static float getWorldHeight(Vector3fImmutable world_loc) { Zone currentZone = ZoneManager.findSmallestZone(world_loc); return getWorldHeight(currentZone, world_loc); } public Vector2f getTerrainCell(Vector2f terrain_loc) { // Calculate terrain cell with offset Vector2f terrain_cell = new Vector2f(terrain_loc.x / this.cell_size.x, terrain_loc.y / this.cell_size.y); // Clamp values when standing directly on pole terrain_cell.x = Math.max(0, Math.min(this.cell_count.x - 1, terrain_cell.x)); terrain_cell.y = Math.max(0, Math.min(this.cell_count.y - 1, terrain_cell.y)); return terrain_cell; } public float getInterpolatedTerrainHeight(Vector2f terrain_loc) { float interpolatedHeight; // Early exit for guild zones if (this.zone.guild_zone) return 5.0f; Vector2f terrain_cell = getTerrainCell(terrain_loc); int pixel_x = (int) Math.floor(terrain_cell.x); int pixel_y = (int) Math.floor(terrain_cell.y); Vector2f pixel_offset = new Vector2f(terrain_cell.x % 1, terrain_cell.y % 1); // 4 surrounding vertices from the pixel array. float top_left_pixel = terrain_pixel_data[pixel_x][pixel_y]; float top_right_pixel = terrain_pixel_data[pixel_x + 1][pixel_y]; float bottom_left_pixel = terrain_pixel_data[pixel_x][pixel_y + 1]; float bottom_right_pixel = terrain_pixel_data[pixel_x + 1][pixel_y + 1]; // Interpolate between the 4 vertices interpolatedHeight = top_left_pixel * (1 - pixel_offset.x) * (1 - pixel_offset.y); interpolatedHeight += top_right_pixel * (1 - pixel_offset.y) * (pixel_offset.x); interpolatedHeight += (bottom_left_pixel * (1 - pixel_offset.x) * pixel_offset.y); interpolatedHeight += (bottom_right_pixel * pixel_offset.y * pixel_offset.x); interpolatedHeight *= this.terrain_scale; // Scale height return interpolatedHeight; } public float getTerrainBlendCoefficient(Vector2f zone_offset) { // Normalize terrain offset Vector2f normalizedOffset = new Vector2f(Math.abs(zone_offset.x) / this.zone.major_radius, Math.abs(zone_offset.y) / this.zone.minor_radius); float value; if (normalizedOffset.x <= 1 - blend_ratio.x || normalizedOffset.x <= normalizedOffset.y) { if (normalizedOffset.y < 1 - blend_ratio.y) return 1; value = (normalizedOffset.y - (1 - blend_ratio.y)) / blend_ratio.y; } else value = (normalizedOffset.x - (1 - blend_ratio.x)) / blend_ratio.x; value = (float) Math.atan((0.5f - value) * PI); return (value + 1) * 0.5f; } }