diff --git a/build.gradle b/build.gradle index 7ab7c4f2ac..518b8bd277 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,6 @@ plugins { } repositories { - mavenLocal() maven { url = 'https://repo.runelite.net' content { diff --git a/schemas/areas.schema.json b/schemas/areas.schema.json index e5834231c1..7321526900 100644 --- a/schemas/areas.schema.json +++ b/schemas/areas.schema.json @@ -24,6 +24,19 @@ "type": "boolean", "description": "Whether to fill gaps in the ground. Can only be disabled if hideOtherAreas is true." }, + "horizonTileReference": { + "type": "array", + "description": "World tile coordinates [x, y] or [x, y, plane] used to sample horizon fill height and material. When set, empty tiles outside this area's AABBs are filled to the scene edge.", + "items": { + "type": "integer" + }, + "minItems": 2, + "maxItems": 3 + }, + "horizonFlatTerrain": { + "type": "boolean", + "description": "When true with horizonTileReference set, extend flat land instead of water (no underwater procgen)." + }, "areas": { "type": "array", "description": "A list of other areas to include in this area.", diff --git a/src/main/java/rs117/hd/HdPlugin.java b/src/main/java/rs117/hd/HdPlugin.java index 6cdf8b6741..9f7e311c02 100644 --- a/src/main/java/rs117/hd/HdPlugin.java +++ b/src/main/java/rs117/hd/HdPlugin.java @@ -1800,6 +1800,7 @@ public void processPendingConfigChanges() { client.setExpandedMapLoading(getExpandedMapLoadingChunks()); // fall-through case KEY_HIDE_UNRELATED_AREAS: + case KEY_HORIZON_TILES: if (client.getGameState() == GameState.LOGGED_IN) client.setGameState(GameState.LOADING); break; diff --git a/src/main/java/rs117/hd/HdPluginConfig.java b/src/main/java/rs117/hd/HdPluginConfig.java index e601c56d2a..4b841dab34 100644 --- a/src/main/java/rs117/hd/HdPluginConfig.java +++ b/src/main/java/rs117/hd/HdPluginConfig.java @@ -138,6 +138,20 @@ default boolean hideUnrelatedAreas() { return true; } + String KEY_HORIZON_TILES = "horizonTiles"; + @ConfigItem( + keyName = KEY_HORIZON_TILES, + name = "Horizon tiles", + description = + "Expands water outside hidden areas to the loaded map edge so scenery is not cut off abruptly.
" + + "Only applies while Hide unrelated areas is enabled.", + position = 5, + section = generalSettings + ) + default boolean horizonTiles() { + return true; + } + String KEY_ANTI_ALIASING_MODE = "antiAliasingMode"; @ConfigItem( keyName = KEY_ANTI_ALIASING_MODE, @@ -145,7 +159,7 @@ default boolean hideUnrelatedAreas() { description = "Improves pixelated edges at the cost of significantly higher GPU usage.
" + "MSAA x16 is very expensive, so x8 is recommended if anti-aliasing is desired.", - position = 5, + position = 6, section = generalSettings ) default AntiAliasingMode antiAliasingMode() @@ -160,7 +174,7 @@ default AntiAliasingMode antiAliasingMode() description = "Render the game at a different resolution and stretch it to fit the screen.
" + "Reducing this can improve performance, particularly on very high resolution displays.", - position = 6, + position = 7, section = generalSettings ) @Units(Units.PERCENT) @@ -173,7 +187,7 @@ default int sceneResolutionScale() { keyName = "sceneScalingMode", name = "Game Scaling Mode", description = "The sampling function to use when upscaling the above reduced game resolution.", - position = 7, + position = 8, section = generalSettings ) default SceneScalingMode sceneScalingMode() diff --git a/src/main/java/rs117/hd/renderer/zone/HorizonExtender.java b/src/main/java/rs117/hd/renderer/zone/HorizonExtender.java new file mode 100644 index 0000000000..8742afad44 --- /dev/null +++ b/src/main/java/rs117/hd/renderer/zone/HorizonExtender.java @@ -0,0 +1,1715 @@ +package rs117.hd.renderer.zone; + +import java.util.HashMap; +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.SceneTileModel; +import net.runelite.api.SceneTilePaint; +import net.runelite.api.Tile; +import rs117.hd.HdPlugin; +import rs117.hd.scene.MaterialManager; +import rs117.hd.scene.ProceduralGenerator; +import rs117.hd.scene.SceneContext; +import rs117.hd.scene.TileOverrideManager; +import rs117.hd.scene.areas.AABB; +import rs117.hd.scene.areas.Area; +import rs117.hd.scene.ground_materials.GroundMaterial; +import rs117.hd.scene.materials.Material; +import rs117.hd.scene.model_overrides.ModelOverride; +import rs117.hd.scene.model_overrides.UvType; +import rs117.hd.scene.tile_overrides.TileOverride; +import rs117.hd.scene.water_types.WaterType; +import rs117.hd.utils.HDUtils; +import rs117.hd.utils.buffer.GpuIntBuffer; + +import static java.lang.Math.max; +import static net.runelite.api.Constants.CHUNK_SIZE; +import static net.runelite.api.Constants.EXTENDED_SCENE_SIZE; +import static net.runelite.api.Constants.SCENE_SIZE; +import static net.runelite.api.Perspective.LOCAL_TILE_SIZE; +import static rs117.hd.scene.SceneContext.TILE_WATER_FLAG; +import static rs117.hd.scene.tile_overrides.TileOverride.OVERLAY_FLAG; +import static rs117.hd.utils.HDUtils.HIDDEN_HSL; +import static rs117.hd.utils.HDUtils.UNDERWATER_HSL; +import static rs117.hd.utils.HDUtils.tileVertexHash; +import static rs117.hd.utils.MathUtils.fract; + +@Slf4j +@Singleton +public class HorizonExtender { + private static final int EDGE_INSET = 4; + private static final int GPU_EXTRA_CHUNKS = 4; + private static final int[][] CORNER_TILES = { + { 0, 0, 0 }, + { -1, 0, 1 }, + { 0, -1, 2 }, + { -1, -1, 3 }, + }; + private static final ThreadLocal UPLOAD_SCRATCH = ThreadLocal.withInitial(UploadScratch::new); + + @Inject + private HdPlugin plugin; + + @Inject + private TileOverrideManager tileOverrideManager; + + @Inject + private MaterialManager materialManager; + + @Inject + private ProceduralGenerator proceduralGenerator; + + private enum ZoneUploadMode { + WATER_SURFACE, + WATER_UNDERWATER, + TERRAIN + } + + private static final class UploadScratch { + final VertexWriteCache.Collection writeCache = new VertexWriteCache.Collection(); + final int[][] cornerHeights = new int[CHUNK_SIZE + 1][CHUNK_SIZE + 1]; + final int[] clipBounds = new int[4]; + + void begin(GpuIntBuffer vb, GpuIntBuffer fb) { + writeCache.setOutputBuffers(vb, vb, fb); + } + + void end() { + writeCache.release(); + } + } + + @FunctionalInterface + private interface ShellStripFn { + void apply(int minLocalX, int minLocalZ, int maxLocalX, int maxLocalZ); + } + + public static final class Sample { + public final int plane; + public final int flatHeight; + public final WaterType waterType; + public final HashMap boundarySurfaceHeights; + public final int flatUnderwaterHeight; + @Nullable + public final HashMap boundaryUnderwaterHeights; + public final Material material; + public final GroundMaterial groundMaterial; + public final int swColor; + public final int seColor; + public final int neColor; + public final int nwColor; + + private Sample( + int plane, + int flatHeight, + WaterType waterType, + HashMap boundarySurfaceHeights, + int flatUnderwaterHeight, + @Nullable HashMap boundaryUnderwaterHeights, + Material material, + GroundMaterial groundMaterial, + int swColor, + int seColor, + int neColor, + int nwColor + ) { + this.plane = plane; + this.flatHeight = flatHeight; + this.waterType = waterType; + this.boundarySurfaceHeights = boundarySurfaceHeights; + this.flatUnderwaterHeight = flatUnderwaterHeight; + this.boundaryUnderwaterHeights = boundaryUnderwaterHeights; + this.material = material; + this.groundMaterial = groundMaterial; + this.swColor = swColor; + this.seColor = seColor; + this.neColor = neColor; + this.nwColor = nwColor; + } + + public static Sample water( + WaterType waterType, + int plane, + int flatHeight, + int flatUnderwaterHeight, + HashMap boundarySurfaceHeights, + HashMap boundaryUnderwaterHeights + ) { + return new Sample( + plane, flatHeight, waterType, boundarySurfaceHeights, flatUnderwaterHeight, + boundaryUnderwaterHeights, Material.NONE, null, 127, 127, 127, 127 + ); + } + + public static Sample terrain( + int plane, + int flatHeight, + HashMap boundarySurfaceHeights, + WaterType waterType, + Material material, + GroundMaterial groundMaterial, + int swColor, + int seColor, + int neColor, + int nwColor + ) { + return new Sample( + plane, flatHeight, waterType, boundarySurfaceHeights, flatHeight, null, + material, groundMaterial, swColor, seColor, neColor, nwColor + ); + } + + public boolean isWaterSurface() { + return waterType != WaterType.NONE; + } + + public static long packWorldVertex(int worldX, int worldY) { + return ((long) worldX << 32) | (worldY & 0xffffffffL); + } + } + + private static final class ZonePlan { + public long tileMask; + public int tileCount; + public boolean fullZone; + + public boolean hasTile(int xoff, int zoff) { + return (tileMask & (1L << (xoff * CHUNK_SIZE + zoff))) != 0; + } + } + + private static final class ShellEdges { + public final int extra; + public final int extent; + public final boolean west; + public final boolean east; + public final boolean north; + public final boolean south; + + ShellEdges(ZoneSceneContext ctx, int mzx, int mzz) { + int zoneMinSceneX = (mzx << 3) - ctx.sceneOffset; + int zoneMaxSceneX = zoneMinSceneX + CHUNK_SIZE - 1; + int zoneMinSceneY = (mzz << 3) - ctx.sceneOffset; + int zoneMaxSceneY = zoneMinSceneY + CHUNK_SIZE - 1; + int mapMin = loadedSceneMin(ctx); + int mapMax = loadedSceneMax(ctx); + + extra = gpuExtraLocalExtent(ctx); + extent = CHUNK_SIZE * LOCAL_TILE_SIZE; + west = zoneMinSceneX == mapMin; + east = zoneMaxSceneX == mapMax - 1; + north = zoneMinSceneY == mapMin; + south = zoneMaxSceneY == mapMax - 1; + } + + public boolean any() { + return west || east || north || south; + } + } + + private static final class Reference { + public final int worldX; + public final int worldY; + public final int plane; + public final int flatHeight; + public final int textureId; + public TileOverride override; + public WaterType waterType; + public Material material = Material.NONE; + public GroundMaterial groundMaterial; + public int swColor = 127; + public int seColor = 127; + public int neColor = 127; + public int nwColor = 127; + + private Reference(int worldX, int worldY, int plane, int flatHeight, int textureId) { + this.worldX = worldX; + this.worldY = worldY; + this.plane = plane; + this.flatHeight = flatHeight; + this.textureId = textureId; + } + } + + public static boolean isEnabled(SceneContext ctx) { + if (!ctx.enableHorizonTiles || ctx.sceneBase == null) + return false; + Area playArea = getPlayArea(ctx); + return playArea != null && playArea.hasHorizonTiles(); + } + + public static boolean shouldDiscardVisibleZone(SceneContext ctx) { + if (!ctx.enableHorizonTiles || ctx.horizonTileArea == null) + return false; + return getPlayArea(ctx) == null; + } + + public static boolean shouldPatchWaterAtTile(SceneContext ctx, int tileExX, int tileExY, int plane) { + Area area = getPlayArea(ctx); + if (area != null && area.horizonFlatTerrain) + return false; + if (!isEnabled(ctx) || plane != getPlane(ctx)) + return false; + if (tileExX < 0 || tileExY < 0 || tileExX >= ctx.sizeX || tileExY >= ctx.sizeZ) + return false; + + boolean[][] mask = ctx.horizonTileMask; + if (mask != null) + return mask[tileExX][tileExY]; + + Tile tile = ctx.scene.getExtendedTiles()[plane][tileExX][tileExY]; + return isWithinGpuRange(ctx, tileExX, tileExY) && + isOutsidePlayAreaBounds(ctx, tileExX, tileExY, plane) && + isEligiblePatchTile(tile); + } + + private static boolean isFlatTerrain(SceneContext ctx) { + Area area = getPlayArea(ctx); + return area != null && area.horizonFlatTerrain; + } + + @Nullable + private static Area getArea(SceneContext ctx) { + if (!ctx.enableHorizonTiles) + return null; + if (ctx.horizonTileArea != null) + return ctx.horizonTileArea; + if (ctx.currentArea != null && ctx.currentArea.hasHorizonTiles()) + return ctx.currentArea; + return null; + } + + @Nullable + private static Area getPlayArea(SceneContext ctx) { + if (ctx.currentArea != null && ctx.currentArea.hasHorizonTiles()) + return ctx.currentArea; + return null; + } + + @Nullable + private static Area getReferenceArea(SceneContext ctx) { + Area playArea = getPlayArea(ctx); + return playArea != null ? playArea : getArea(ctx); + } + + private static int getPlane(SceneContext ctx) { + Area area = getReferenceArea(ctx); + if (area == null || area.horizonTileReference == null) + return 0; + int[] ref = area.horizonTileReference; + return ref.length > 2 ? ref[2] : 0; + } + + private static int loadedSceneMin(SceneContext ctx) { + return -ctx.expandedMapLoadingChunks * CHUNK_SIZE; + } + + private static int loadedSceneMax(SceneContext ctx) { + return SCENE_SIZE + ctx.expandedMapLoadingChunks * CHUNK_SIZE; + } + + private static int gpuSceneMin(SceneContext ctx) { + return loadedSceneMin(ctx) - GPU_EXTRA_CHUNKS * CHUNK_SIZE; + } + + private static int gpuSceneMax(SceneContext ctx) { + return loadedSceneMax(ctx) + GPU_EXTRA_CHUNKS * CHUNK_SIZE; + } + + private static int gpuExtraLocalExtent(SceneContext ctx) { + return GPU_EXTRA_CHUNKS * CHUNK_SIZE * LOCAL_TILE_SIZE; + } + + private static boolean isWithinGpuRange(SceneContext ctx, int tileExX, int tileExY) { + if (ctx.sceneBase == null) + return true; + int tileX = tileExX - ctx.sceneOffset; + int tileY = tileExY - ctx.sceneOffset; + int min = gpuSceneMin(ctx); + int max = gpuSceneMax(ctx); + return tileX >= min && tileY >= min && tileX < max && tileY < max; + } + + private static boolean needsGpuShell(SceneContext ctx) { + if (ctx.sceneBase == null) + return false; + int gpuMinEx = gpuSceneMin(ctx) + ctx.sceneOffset; + int gpuMaxEx = gpuSceneMax(ctx) + ctx.sceneOffset; + return gpuMinEx < 0 || gpuMaxEx > EXTENDED_SCENE_SIZE; + } + + private static boolean isHiddenGroundTile(Tile tile) { + SceneTilePaint paint = tile.getSceneTilePaint(); + SceneTileModel model = tile.getSceneTileModel(); + + if (model == null) { + Tile bridge = tile.getBridge(); + if (bridge != null) { + if (bridge.getSceneTileModel() != null) { + model = bridge.getSceneTileModel(); + paint = null; + } else { + paint = bridge.getSceneTilePaint(); + } + } + } + + if (model != null) { + for (int color : model.getTriangleColorA()) { + if (color != HIDDEN_HSL) + return false; + } + return true; + } + + return paint == null || paint.getNeColor() == HIDDEN_HSL; + } + + private static boolean isEligiblePatchTile(@Nullable Tile tile) { + return tile == null || isHiddenGroundTile(tile); + } + + private static boolean isOutsidePlayAreaBounds(SceneContext ctx, int tileExX, int tileExY, int plane) { + Area area = getPlayArea(ctx); + if (area == null) + return true; + int[] worldPos = ctx.extendedSceneToWorld(tileExX, tileExY, plane); + return !area.containsPoint(false, worldPos[0], worldPos[1], worldPos[2]); + } + + private static void buildPatchMask(SceneContext ctx, Tile[][][] tiles) { + if (!isEnabled(ctx)) { + ctx.horizonTileMask = null; + return; + } + + int plane = getPlane(ctx); + boolean[][] mask = new boolean[ctx.sizeX][ctx.sizeZ]; + Tile[][] planeTiles = tiles[plane]; + for (int x = 0; x < ctx.sizeX; ++x) { + for (int y = 0; y < ctx.sizeZ; ++y) { + mask[x][y] = isWithinGpuRange(ctx, x, y) && + isOutsidePlayAreaBounds(ctx, x, y, plane) && + isEligiblePatchTile(planeTiles[x][y]); + } + } + ctx.horizonTileMask = mask; + } + + private static boolean shouldRenderAtTile(SceneContext ctx, int tileExX, int tileExY, int plane) { + if (!isEnabled(ctx) || plane != getPlane(ctx)) + return false; + if (!isWithinGpuRange(ctx, tileExX, tileExY)) + return false; + return isOutsidePlayAreaBounds(ctx, tileExX, tileExY, plane); + } + + private static boolean shouldExtendFlatTerrainTile( + ZoneSceneContext ctx, + int tileExX, + int tileExY, + int plane + ) { + if (!isEnabled(ctx) || plane != getPlane(ctx)) + return false; + + if (tileExX < 0 || tileExY < 0 || tileExX >= EXTENDED_SCENE_SIZE || tileExY >= EXTENDED_SCENE_SIZE) { + if (!isOutsidePlayAreaBounds(ctx, tileExX, tileExY, plane)) + return false; + return isWithinGpuRange(ctx, tileExX, tileExY); + } + + if (isOutsidePlayAreaBounds(ctx, tileExX, tileExY, plane)) + return true; + + Tile tile = ctx.scene.getExtendedTiles()[plane][tileExX][tileExY]; + return tile == null || isHiddenGroundTile(tile); + } + + private static boolean clipLocal( + ZoneSceneContext ctx, + Area area, + int mzx, + int mzz, + int plane, + int minLocalX, + int minLocalZ, + int maxLocalX, + int maxLocalZ, + int[] out + ) { + if (ctx.sceneBase == null) + return false; + + int zoneBaseTileX = (mzx << 3) - ctx.sceneOffset; + int zoneBaseTileY = (mzz << 3) - ctx.sceneOffset; + int zoneMinWx = ctx.sceneBase[0] + zoneBaseTileX; + int zoneMaxWx = ctx.sceneBase[0] + zoneBaseTileX + CHUNK_SIZE - 1; + int zoneMinWy = ctx.sceneBase[1] + zoneBaseTileY; + int zoneMaxWy = ctx.sceneBase[1] + zoneBaseTileY + CHUNK_SIZE - 1; + + long clipMinX = minLocalX; + long clipMaxX = maxLocalX; + long clipMinZ = minLocalZ; + long clipMaxZ = maxLocalZ; + + area.normalize(); + for (AABB aabb : area.aabbs) { + if (aabb.minZ != Integer.MIN_VALUE && aabb.maxZ != Integer.MAX_VALUE && + (plane < aabb.minZ || plane > aabb.maxZ)) + continue; + + if (zoneMaxWx < aabb.minX) { + clipMaxX = Math.min(clipMaxX, (long) (aabb.minX - zoneMinWx) * LOCAL_TILE_SIZE); + } else if (zoneMinWx > aabb.maxX) { + clipMinX = Math.max(clipMinX, (long) (aabb.maxX + 1 - zoneMinWx) * LOCAL_TILE_SIZE); + } + + if (zoneMaxWy < aabb.minY) { + clipMaxZ = Math.min(clipMaxZ, (long) (aabb.minY - zoneMinWy) * LOCAL_TILE_SIZE); + } else if (zoneMinWy > aabb.maxY) { + clipMinZ = Math.max(clipMinZ, (long) (aabb.maxY + 1 - zoneMinWy) * LOCAL_TILE_SIZE); + } + } + + if (clipMinX >= clipMaxX || clipMinZ >= clipMaxZ) + return false; + + out[0] = (int) clipMinX; + out[1] = (int) clipMinZ; + out[2] = (int) clipMaxX; + out[3] = (int) clipMaxZ; + return true; + } + + private static int countTiles(int minX, int minZ, int maxX, int maxZ) { + int tiles = 0; + int startTileX = floorDiv(minX, LOCAL_TILE_SIZE); + int startTileZ = floorDiv(minZ, LOCAL_TILE_SIZE); + int endTileX = floorDiv(maxX - 1, LOCAL_TILE_SIZE); + int endTileZ = floorDiv(maxZ - 1, LOCAL_TILE_SIZE); + + for (int tx = startTileX; tx <= endTileX; ++tx) { + for (int tz = startTileZ; tz <= endTileZ; ++tz) { + int baseX = tx * LOCAL_TILE_SIZE; + int baseZ = tz * LOCAL_TILE_SIZE; + if (baseX + LOCAL_TILE_SIZE <= minX || baseZ + LOCAL_TILE_SIZE <= minZ || + baseX >= maxX || baseZ >= maxZ) + continue; + tiles++; + } + } + return tiles; + } + + private static int floorDiv(int a, int b) { + if (a >= 0) + return a / b; + return (a - b + 1) / b; + } + + private static AABB nearestAabb(Area area, int worldX, int worldY, int plane) { + area.normalize(); + AABB nearest = null; + int nearestDist = Integer.MAX_VALUE; + + for (AABB aabb : area.aabbs) { + if (aabb.minZ != Integer.MIN_VALUE && aabb.maxZ != Integer.MAX_VALUE && + (plane < aabb.minZ || plane > aabb.maxZ)) + continue; + + int dx = 0; + if (worldX < aabb.minX) + dx = aabb.minX - worldX; + else if (worldX > aabb.maxX) + dx = worldX - aabb.maxX; + + int dy = 0; + if (worldY < aabb.minY) + dy = aabb.minY - worldY; + else if (worldY > aabb.maxY) + dy = worldY - aabb.maxY; + + int dist = dx + dy; + if (nearest == null || dist < nearestDist) { + nearest = aabb; + nearestDist = dist; + } + } + return nearest; + } + + private static int clampVertexX(AABB aabb, int worldX) { + return Math.min(Math.max(worldX, aabb.minX), aabb.maxX + 1); + } + + private static int clampVertexY(AABB aabb, int worldY) { + return Math.min(Math.max(worldY, aabb.minY), aabb.maxY + 1); + } + + private static void putFlatQuad( + VertexWriteCache.Collection writeCache, + WaterType waterType, + int plane, + int minX, + int minZ, + int maxX, + int maxZ, + int height, + int color, + int depth + ) { + int terrainData = HDUtils.packTerrainData(true, depth, waterType, plane); + int packedMaterial = Material.NONE.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); + + putTriangle( + writeCache, + color, color, color, + packedMaterial, packedMaterial, packedMaterial, + terrainData, terrainData, terrainData, + maxX, height, maxZ, 0, 0, + minX, height, maxZ, 0, 0, + maxX, height, minZ, 0, 0 + ); + putTriangle( + writeCache, + color, color, color, + packedMaterial, packedMaterial, packedMaterial, + terrainData, terrainData, terrainData, + minX, height, minZ, 0, 0, + maxX, height, minZ, 0, 0, + minX, height, maxZ, 0, 0 + ); + } + + private static void putTriangle( + VertexWriteCache.Collection writeCache, + int colorA, + int colorB, + int colorC, + int packedMaterialA, + int packedMaterialB, + int packedMaterialC, + int terrainDataA, + int terrainDataB, + int terrainDataC, + int x0, int y0, int z0, float u0, float v0, + int x1, int y1, int z1, float u1, float v1, + int x2, int y2, int z2, float u2, float v2 + ) { + var vb = writeCache.getVertexBuffer(); + var tb = writeCache.getTextureBuffer(); + int faceIdx = tb.putFace( + colorA, colorB, colorC, + packedMaterialA, packedMaterialB, packedMaterialC, + terrainDataA, terrainDataB, terrainDataC + ); + vb.putStaticVertex(x0, y0, z0, u0, v0, 0, 0, 0, -1, faceIdx); + vb.putStaticVertex(x1, y1, z1, u1, v1, 0, 0, 0, -1, faceIdx); + vb.putStaticVertex(x2, y2, z2, u2, v2, 0, 0, 0, -1, faceIdx); + } + + private static void forEachShellStrip(@Nullable ShellEdges shell, ShellStripFn fn) { + if (shell == null) + return; + + int x = shell.extra; + int z = shell.extent; + if (shell.west) + fn.apply(-x, 0, 0, z); + if (shell.east) + fn.apply(z, 0, z + x, z); + if (shell.north) + fn.apply(0, -x, z, 0); + if (shell.south) + fn.apply(0, z, z, z + x); + if (shell.west && shell.north) + fn.apply(-x, -x, 0, 0); + if (shell.east && shell.north) + fn.apply(z, -x, z + x, 0); + if (shell.west && shell.south) + fn.apply(-x, z, 0, z + x); + if (shell.east && shell.south) + fn.apply(z, z, z + x, z + x); + } + + private Reference resolveReference(ZoneSceneContext ctx, Area area) { + int[] ref = area.horizonTileReference; + int worldX = ref[0]; + int worldY = ref[1]; + int plane = ref.length > 2 ? ref[2] : 0; + + int textureId = -1; + int flatHeight = 0; + TileOverride override = TileOverride.NONE; + int swColor = 127; + int seColor = 127; + int neColor = 127; + int nwColor = 127; + + if (ctx.sceneBase != null) { + int sceneX = worldX - ctx.sceneBase[0]; + int sceneY = worldY - ctx.sceneBase[1]; + int tileExX = sceneX + ctx.sceneOffset; + int tileExY = sceneY + ctx.sceneOffset; + + if (tileExX >= 0 && tileExY >= 0 && tileExX < EXTENDED_SCENE_SIZE && tileExY < EXTENDED_SCENE_SIZE) { + Tile[][][] extendedTiles = ctx.scene.getExtendedTiles(); + int[][][] tileHeights = ctx.scene.getTileHeights(); + Tile tile = extendedTiles[plane][tileExX][tileExY]; + if (tile != null) { + int[] worldPos = ctx.sceneToWorld(sceneX, sceneY, plane); + override = tileOverrideManager.getOverride(ctx, tile, worldPos); + SceneTilePaint paint = tile.getSceneTilePaint(); + if (paint != null) { + textureId = paint.getTexture(); + flatHeight = averageHeight(tileHeights, plane, tileExX, tileExY); + swColor = paint.getSwColor(); + seColor = paint.getSeColor(); + neColor = paint.getNeColor(); + nwColor = paint.getNwColor(); + } else { + SceneTileModel model = tile.getSceneTileModel(); + if (model != null && model.getTriangleTextureId() != null) { + for (int faceTexture : model.getTriangleTextureId()) { + if (faceTexture != -1) { + textureId = faceTexture; + break; + } + } + for (int c : model.getTriangleColorA()) { + if (c != HIDDEN_HSL) { + swColor = seColor = neColor = nwColor = c; + break; + } + } + } + flatHeight = averageHeight(tileHeights, plane, tileExX, tileExY); + } + } + } + } + + Reference result = new Reference(worldX, worldY, plane, flatHeight, textureId); + result.override = override; + result.waterType = proceduralGenerator.seasonalWaterType(override, textureId); + result.swColor = swColor; + result.seColor = seColor; + result.neColor = neColor; + result.nwColor = nwColor; + + if (override == TileOverride.NONE) { + int[] worldPos = { worldX, worldY, plane }; + override = tileOverrideManager.getOverrideBeforeReplacements(worldPos); + result.override = override; + if (result.waterType == WaterType.NONE) + result.waterType = proceduralGenerator.seasonalWaterType(override, textureId); + } + + if (result.waterType == WaterType.NONE) { + if (textureId != -1) { + result.material = materialManager.fromVanillaTexture(textureId); + if (result.material.isFallbackVanillaMaterial) + override = TileOverride.NONE; + } + + if (override != TileOverride.NONE) { + result.groundMaterial = override.groundMaterial; + result.swColor = override.modifyColor(result.swColor); + result.seColor = override.modifyColor(result.seColor); + result.neColor = override.modifyColor(result.neColor); + result.nwColor = override.modifyColor(result.nwColor); + } else if (textureId == -1) { + result.groundMaterial = override.groundMaterial; + } + } + + return result; + } + + private static int averageHeight(int[][][] tileHeights, int plane, int tileExX, int tileExY) { + int sw = tileHeights[plane][tileExX][tileExY]; + int se = tileHeights[plane][tileExX + 1][tileExY]; + int ne = tileHeights[plane][tileExX + 1][tileExY + 1]; + int nw = tileHeights[plane][tileExX][tileExY + 1]; + return (sw + se + ne + nw) / 4; + } + + @Nullable + private static ShellEdges shellEdges(ZoneSceneContext ctx, int mzx, int mzz) { + if (!needsGpuShell(ctx)) + return null; + return new ShellEdges(ctx, mzx, mzz); + } + + private static int countShellQuads(ShellEdges shell) { + if (shell == null || !shell.any()) + return 0; + + int[] quads = { 0 }; + forEachShellStrip(shell, (minX, minZ, maxX, maxZ) -> quads[0]++); + return quads[0]; + } + + private static int countShellStripTiles( + ZoneSceneContext ctx, + Area area, + int mzx, + int mzz, + int plane, + int minLocalX, + int minLocalZ, + int maxLocalX, + int maxLocalZ, + int[] clipBounds + ) { + if (!clipLocal(ctx, area, mzx, mzz, plane, minLocalX, minLocalZ, maxLocalX, maxLocalZ, clipBounds)) + return 0; + return countTiles(clipBounds[0], clipBounds[1], clipBounds[2], clipBounds[3]); + } + + private ZonePlan planZone(ZoneSceneContext ctx, int mzx, int mzz, Sample sample) { + ZonePlan plan = new ZonePlan(); + boolean flatTerrain = isFlatTerrain(ctx); + for (int xoff = 0; xoff < CHUNK_SIZE; ++xoff) { + for (int zoff = 0; zoff < CHUNK_SIZE; ++zoff) { + int tileExX = (mzx << 3) + xoff; + int tileExY = (mzz << 3) + zoff; + boolean extend = flatTerrain ? + shouldExtendFlatTerrainTile(ctx, tileExX, tileExY, sample.plane) : + shouldExtendTile(ctx, sample, tileExX, tileExY) > 0; + if (extend) { + plan.tileMask |= 1L << (xoff * CHUNK_SIZE + zoff); + plan.tileCount++; + } + } + } + plan.fullZone = plan.tileCount == CHUNK_SIZE * CHUNK_SIZE; + return plan; + } + + private static boolean isUniformHeightGrid(int[][] heights) { + int h = heights[0][0]; + for (int x = 0; x <= CHUNK_SIZE; ++x) { + for (int z = 0; z <= CHUNK_SIZE; ++z) { + if (heights[x][z] != h) + return false; + } + } + return true; + } + + private static int resolveCornerSurfaceHeight( + ZoneSceneContext ctx, + Sample sample, + AABB aabb, + int worldX, + int worldY + ) { + int boundaryX = clampVertexX(aabb, worldX); + int boundaryY = clampVertexY(aabb, worldY); + + Integer live = liveSurfaceHeightAtWorldVertex(ctx, sample.plane, boundaryX, boundaryY); + if (live != null) + return live; + + long key = Sample.packWorldVertex(boundaryX, boundaryY); + Integer height = sample.boundarySurfaceHeights.get(key); + return height != null ? height : sample.flatHeight; + } + + @Nullable + private static Integer liveSurfaceHeightAtWorldVertex( + ZoneSceneContext ctx, + int plane, + int worldX, + int worldY + ) { + if (ctx.sceneBase == null) + return null; + + int sceneX = worldX - ctx.sceneBase[0]; + int sceneY = worldY - ctx.sceneBase[1]; + int tileExX = sceneX + ctx.sceneOffset; + int tileExY = sceneY + ctx.sceneOffset; + int[][][] tileHeights = ctx.scene.getTileHeights(); + if (tileExX < 0 || tileExY < 0 || + tileExX >= tileHeights[plane].length || + tileExY >= tileHeights[plane][tileExX].length) + return null; + + return tileHeights[plane][tileExX][tileExY]; + } + + private static int cornerSurfaceHeightAtWorldVertex( + ZoneSceneContext ctx, + int plane, + int worldX, + int worldY, + int fallback + ) { + Integer live = liveSurfaceHeightAtWorldVertex(ctx, plane, worldX, worldY); + return live != null ? live : fallback; + } + + public void prepareSceneTerrain(SceneContext ctx, Tile[][][] tiles) { + Area area = getPlayArea(ctx); + if (isFlatTerrain(ctx)) + return; + if (!isEnabled(ctx)) + return; + + buildPatchMask(ctx, tiles); + boolean[][] mask = ctx.horizonTileMask; + if (mask == null || ctx.underwaterDepthLevels == null) + return; + + int z = getPlane(ctx); + int sizeX = ctx.sizeX; + int sizeY = ctx.sizeZ; + + for (int x = 0; x < sizeX; ++x) { + for (int y = 0; y < sizeY; ++y) { + if (!mask[x][y] || ctx.isTileFlagSet(z, x, y, TILE_WATER_FLAG)) + continue; + + ctx.setTileFlag(z, x, y, TILE_WATER_FLAG); + Tile tile = tiles[z][x][y]; + if (tile == null || isHiddenGroundTile(tile)) { + ctx.underwaterDepthLevels[z][x][y] = 1; + ctx.underwaterDepthLevels[z][x + 1][y] = 1; + ctx.underwaterDepthLevels[z][x][y + 1] = 1; + ctx.underwaterDepthLevels[z][x + 1][y + 1] = 1; + } + } + } + + for (int x = 0; x < sizeX; ++x) { + for (int y = 0; y < sizeY; ++y) { + if (!mask[x][y]) + continue; + + Tile tile = tiles[z][x][y]; + if (tile != null && !isHiddenGroundTile(tile)) + continue; + + for (int nx = x - 1; nx <= x + 1; ++nx) { + for (int ny = y - 1; ny <= y + 1; ++ny) { + if (nx == x && ny == y) + continue; + if (nx < 0 || ny < 0 || nx >= sizeX || ny >= sizeY) + continue; + if (!ctx.isTileFlagSet(z, nx, ny, TILE_WATER_FLAG)) + continue; + + Tile neighbor = tiles[z][nx][ny]; + if (neighbor == null) + continue; + + clearLandFlagsOnWaterFaces(ctx, neighbor, nx, ny, z); + } + } + } + } + } + + private void clearLandFlagsOnWaterFaces(SceneContext ctx, Tile tile, int x, int y, int z) { + if (tile.getBridge() != null) + tile = tile.getBridge(); + + int[] worldPos = ctx.extendedSceneToWorld(x, y, z); + SceneTilePaint paint = tile.getSceneTilePaint(); + if (paint != null) { + var override = tileOverrideManager.getOverride(ctx, tile, worldPos); + if (proceduralGenerator.seasonalWaterType(override, paint.getTexture()) != WaterType.NONE) { + for (int key : ProceduralGenerator.tileVertexKeys(ctx, tile)) + ctx.clearVertexIsLand(key); + } + return; + } + + SceneTileModel model = tile.getSceneTileModel(); + if (model == null) + return; + + int overlayId = OVERLAY_FLAG | ctx.scene.getOverlayIds()[z][x][y]; + int underlayId = ctx.scene.getUnderlayIds()[z][x][y]; + var overlayOverride = tileOverrideManager.getOverride(ctx, tile, worldPos, overlayId); + var underlayOverride = tileOverrideManager.getOverride(ctx, tile, worldPos, underlayId); + + final int[] triangleTextures = model.getTriangleTextureId(); + final int[] triangleColorA = model.getTriangleColorA(); + for (int face = 0; face < triangleColorA.length; face++) { + if (triangleColorA[face] == HIDDEN_HSL) + continue; + + int textureId = triangleTextures == null ? -1 : triangleTextures[face]; + boolean isOverlay = ProceduralGenerator.isOverlayFace(tile, face); + var override = isOverlay ? overlayOverride : underlayOverride; + if (proceduralGenerator.seasonalWaterType(override, textureId) == WaterType.NONE) + continue; + + for (int key : ProceduralGenerator.faceVertexKeys(tile, face)) + ctx.clearVertexIsLand(key); + } + } + + public void estimateForZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) { + if (!isEnabled(ctx)) + return; + + Sample sample = resolveSample(ctx); + if (sample == null) + return; + + boolean terrain = isFlatTerrain(ctx); + ZonePlan plan = planZone(ctx, mzx, mzz, sample); + ShellEdges shell = shellEdges(ctx, mzx, mzz); + int shellFaces = terrain ? + countTerrainShellFaces(ctx, getPlayArea(ctx), sample, mzx, mzz, shell, UPLOAD_SCRATCH.get().clipBounds) : + countShellQuads(shell) * 2; + if (plan.tileCount == 0 && shellFaces == 0) + return; + + int faces = plan.tileCount * 2 + shellFaces; + if (terrain) { + zone.sizeO += faces; + zone.sizeF += faces; + if (faces > 0) + zone.hasWater = true; + } else { + zone.hasWater = true; + zone.sizeO += faces * 2; + zone.sizeF += faces * 2; + } + } + + public void uploadUnderwaterForZone( + ZoneSceneContext ctx, + Zone zone, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + if (!isEnabled(ctx) || isFlatTerrain(ctx)) + return; + uploadForZone(ctx, zone, mzx, mzz, vb, fb, ZoneUploadMode.WATER_UNDERWATER); + } + + public void uploadSurfaceForZone( + ZoneSceneContext ctx, + Zone zone, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + if (!isEnabled(ctx)) + return; + uploadForZone( + ctx, zone, mzx, mzz, vb, fb, + isFlatTerrain(ctx) ? ZoneUploadMode.TERRAIN : ZoneUploadMode.WATER_SURFACE + ); + } + + private void uploadForZone( + ZoneSceneContext ctx, + Zone zone, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb, + ZoneUploadMode mode + ) { + if (!isEnabled(ctx) || vb == null) + return; + + var scratch = UPLOAD_SCRATCH.get(); + scratch.begin(vb, fb); + try { + Sample sample = resolveSample(ctx); + if (sample == null) + return; + + Area area = getPlayArea(ctx); + assert area != null; + + int posBefore = vb.position(); + ZonePlan plan = planZone(ctx, mzx, mzz, sample); + if (plan.tileCount > 0) { + boolean underwater = mode == ZoneUploadMode.WATER_UNDERWATER; + fillZoneCornerHeights(ctx, area, sample, mzx, mzz, scratch.cornerHeights, underwater); + if (mode == ZoneUploadMode.TERRAIN) + uploadTerrainZoneTiles(scratch.writeCache, ctx, sample, plan, scratch.cornerHeights, mzx, mzz); + else if (plan.fullZone && isUniformHeightGrid(scratch.cornerHeights)) + uploadZoneQuad(scratch.writeCache, ctx, sample, scratch.cornerHeights, mzx, mzz, underwater); + else + uploadWaterZoneTiles(scratch.writeCache, ctx, sample, plan, scratch.cornerHeights, mzx, mzz, underwater); + } + + uploadShell(scratch.writeCache, ctx, area, sample, mzx, mzz, mode); + + if (vb.position() > posBefore) + zone.hasWater = true; + } finally { + scratch.end(); + } + } + + private void uploadShell( + VertexWriteCache.Collection writeCache, + ZoneSceneContext ctx, + Area area, + Sample sample, + int mzx, + int mzz, + ZoneUploadMode mode + ) { + int[] clipBounds = UPLOAD_SCRATCH.get().clipBounds; + forEachShellStrip(shellEdges(ctx, mzx, mzz), (minLocalX, minLocalZ, maxLocalX, maxLocalZ) -> + uploadShellStrip(writeCache, ctx, area, sample, mzx, mzz, minLocalX, minLocalZ, maxLocalX, maxLocalZ, mode, clipBounds)); + } + + private void uploadShellStrip( + VertexWriteCache.Collection writeCache, + ZoneSceneContext ctx, + Area area, + Sample sample, + int mzx, + int mzz, + int minLocalX, + int minLocalZ, + int maxLocalX, + int maxLocalZ, + ZoneUploadMode mode, + int[] clipBounds + ) { + if (!clipLocal(ctx, area, mzx, mzz, sample.plane, minLocalX, minLocalZ, maxLocalX, maxLocalZ, clipBounds)) + return; + + switch (mode) { + case TERRAIN: + uploadFlatTilesInBounds( + writeCache, ctx, sample, mzx, mzz, + clipBounds[0], clipBounds[1], clipBounds[2], clipBounds[3], sample.flatHeight + ); + break; + case WATER_UNDERWATER: + putFlatQuad( + writeCache, + sample.waterType, sample.plane, + clipBounds[0], clipBounds[1], clipBounds[2], clipBounds[3], + sample.flatUnderwaterHeight, UNDERWATER_HSL, max(1, sample.flatUnderwaterHeight - sample.flatHeight) + ); + break; + case WATER_SURFACE: + putFlatQuad( + writeCache, + sample.waterType, sample.plane, + clipBounds[0], clipBounds[1], clipBounds[2], clipBounds[3], + sample.flatHeight, 127, 0 + ); + break; + } + } + + private static void fillZoneCornerHeights( + ZoneSceneContext ctx, + Area area, + Sample sample, + int mzx, + int mzz, + int[][] heights, + boolean underwater + ) { + int plane = sample.plane; + for (int vx = 0; vx <= CHUNK_SIZE; ++vx) { + for (int vz = 0; vz <= CHUNK_SIZE; ++vz) { + int tileX = (mzx << 3) + vx - ctx.sceneOffset; + int tileY = (mzz << 3) + vz - ctx.sceneOffset; + int[] worldPos = ctx.sceneToWorld(tileX, tileY, plane); + AABB aabb = nearestAabb(area, worldPos[0], worldPos[1], plane); + if (aabb == null) { + heights[vx][vz] = underwater ? sample.flatUnderwaterHeight : sample.flatHeight; + continue; + } + heights[vx][vz] = underwater ? + resolveCornerUnderwaterHeight(ctx, sample, aabb, worldPos[0], worldPos[1]) : + resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0], worldPos[1]); + } + } + } + + private void uploadZoneQuad( + VertexWriteCache.Collection writeCache, + ZoneSceneContext ctx, + Sample sample, + int[][] cornerHeights, + int mzx, + int mzz, + boolean underwater + ) { + int sw = cornerHeights[0][0]; + int se = cornerHeights[CHUNK_SIZE][0]; + int nw = cornerHeights[0][CHUNK_SIZE]; + int ne = cornerHeights[CHUNK_SIZE][CHUNK_SIZE]; + int extent = CHUNK_SIZE * LOCAL_TILE_SIZE; + + if (underwater) + uploadUnderwaterTile(writeCache, ctx, sample, 0, 0, extent, extent, sw, se, nw, ne, mzx, mzz); + else + putFlatQuad(writeCache, sample.waterType, sample.plane, 0, 0, extent, extent, sw, 127, 0); + } + + private void uploadWaterZoneTiles( + VertexWriteCache.Collection writeCache, + ZoneSceneContext ctx, + Sample sample, + ZonePlan plan, + int[][] cornerHeights, + int mzx, + int mzz, + boolean underwater + ) { + for (int xoff = 0; xoff < CHUNK_SIZE; ++xoff) { + for (int zoff = 0; zoff < CHUNK_SIZE; ++zoff) { + if (!plan.hasTile(xoff, zoff)) + continue; + + int sw = cornerHeights[xoff][zoff]; + int se = cornerHeights[xoff + 1][zoff]; + int nw = cornerHeights[xoff][zoff + 1]; + int ne = cornerHeights[xoff + 1][zoff + 1]; + int baseX = xoff * LOCAL_TILE_SIZE; + int baseZ = zoff * LOCAL_TILE_SIZE; + + if (underwater) + uploadUnderwaterTile(writeCache, ctx, sample, baseX, baseZ, baseX + LOCAL_TILE_SIZE, baseZ + LOCAL_TILE_SIZE, sw, se, nw, ne, mzx, mzz); + else + putFlatQuad(writeCache, sample.waterType, sample.plane, baseX, baseZ, baseX + LOCAL_TILE_SIZE, baseZ + LOCAL_TILE_SIZE, sw, 127, 0); + } + } + } + + private void uploadUnderwaterTile( + VertexWriteCache.Collection writeCache, + ZoneSceneContext ctx, + Sample sample, + int minX, + int minZ, + int maxX, + int maxZ, + int sw, + int se, + int nw, + int ne, + int mzx, + int mzz + ) { + int tileZ = sample.plane; + int baseTileX = (mzx << 3) - ctx.sceneOffset; + int baseTileY = (mzz << 3) - ctx.sceneOffset; + int[] swWorld = ctx.sceneToWorld(baseTileX + minX / LOCAL_TILE_SIZE, baseTileY + minZ / LOCAL_TILE_SIZE, tileZ); + int[] seWorld = ctx.sceneToWorld(baseTileX + maxX / LOCAL_TILE_SIZE, baseTileY + minZ / LOCAL_TILE_SIZE, tileZ); + int[] nwWorld = ctx.sceneToWorld(baseTileX + minX / LOCAL_TILE_SIZE, baseTileY + maxZ / LOCAL_TILE_SIZE, tileZ); + int[] neWorld = ctx.sceneToWorld(baseTileX + maxX / LOCAL_TILE_SIZE, baseTileY + maxZ / LOCAL_TILE_SIZE, tileZ); + + int swDepth = max(1, sw - resolveSurfaceAt(ctx, sample, swWorld[0], swWorld[1])); + int seDepth = max(1, se - resolveSurfaceAt(ctx, sample, seWorld[0], seWorld[1])); + int nwDepth = max(1, nw - resolveSurfaceAt(ctx, sample, nwWorld[0], nwWorld[1])); + int neDepth = max(1, ne - resolveSurfaceAt(ctx, sample, neWorld[0], neWorld[1])); + + Material swMaterial = Material.NONE; + Material seMaterial = Material.NONE; + Material neMaterial = Material.NONE; + Material nwMaterial = Material.NONE; + if (plugin.configGroundTextures) { + GroundMaterial groundMaterial = GroundMaterial.UNDERWATER_GENERIC; + swMaterial = groundMaterial.getRandomMaterial(swWorld[0], swWorld[1], swWorld[2]); + seMaterial = groundMaterial.getRandomMaterial(seWorld[0], seWorld[1], seWorld[2]); + nwMaterial = groundMaterial.getRandomMaterial(nwWorld[0], nwWorld[1], nwWorld[2]); + neMaterial = groundMaterial.getRandomMaterial(neWorld[0], neWorld[1], neWorld[2]); + } + + float uvx = fract(swWorld[0]); + float uvy = fract(swWorld[1]); + + putTriangle( + writeCache, + UNDERWATER_HSL, UNDERWATER_HSL, UNDERWATER_HSL, + neMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), + nwMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), + seMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), + HDUtils.packTerrainData(true, neDepth, sample.waterType, tileZ), + HDUtils.packTerrainData(true, nwDepth, sample.waterType, tileZ), + HDUtils.packTerrainData(true, seDepth, sample.waterType, tileZ), + maxX, ne, maxZ, fract(neWorld[0]), fract(neWorld[1]), + minX, nw, maxZ, uvx - 1, fract(nwWorld[1]), + maxX, se, minZ, fract(seWorld[0]), uvy - 1 + ); + putTriangle( + writeCache, + UNDERWATER_HSL, UNDERWATER_HSL, UNDERWATER_HSL, + swMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), + seMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), + nwMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), + HDUtils.packTerrainData(true, swDepth, sample.waterType, tileZ), + HDUtils.packTerrainData(true, seDepth, sample.waterType, tileZ), + HDUtils.packTerrainData(true, nwDepth, sample.waterType, tileZ), + minX, sw, minZ, uvx - 1, uvy - 1, + maxX, se, minZ, fract(seWorld[0]), uvy - 1, + minX, nw, maxZ, uvx - 1, fract(nwWorld[1]) + ); + } + + private static int resolveSurfaceAt(ZoneSceneContext ctx, Sample sample, int worldX, int worldY) { + Area area = getPlayArea(ctx); + if (area == null) + return sample.flatHeight; + AABB aabb = nearestAabb(area, worldX, worldY, sample.plane); + if (aabb == null) + return sample.flatHeight; + return resolveCornerSurfaceHeight(ctx, sample, aabb, worldX, worldY); + } + + @Nullable + private Sample resolveSample(ZoneSceneContext ctx) { + if (!isEnabled(ctx)) + return null; + if (ctx.horizonTileSample != null) + return ctx.horizonTileSample; + + Area area = getReferenceArea(ctx); + assert area != null; + + Reference ref = resolveReference(ctx, area); + HashMap boundarySurfaceHeights = new HashMap<>(); + boolean terrain = isFlatTerrain(ctx); + + if (terrain) { + buildBoundaryHeightMaps(ctx, area, ref.plane, ref.flatHeight, boundarySurfaceHeights, null); + ctx.horizonTileSample = Sample.terrain( + ref.plane, + ref.flatHeight, + boundarySurfaceHeights, + ref.waterType, + ref.material, + ref.groundMaterial, + ref.swColor, + ref.seColor, + ref.neColor, + ref.nwColor + ); + log.info( + "Horizon terrain enabled for {} using reference tile [{}, {}, {}]: {} at height {}", + area.name, ref.worldX, ref.worldY, ref.plane, ref.waterType, ref.flatHeight + ); + } else { + HashMap boundaryUnderwaterHeights = new HashMap<>(); + buildBoundaryHeightMaps(ctx, area, ref.plane, ref.flatHeight, boundarySurfaceHeights, boundaryUnderwaterHeights); + + WaterType waterType = ref.waterType; + if (waterType == WaterType.NONE) + waterType = WaterType.WATER; + + int flatUnderwaterHeight = ref.flatHeight + (int) (ProceduralGenerator.MAX_DEPTH * .55f); + if (!boundaryUnderwaterHeights.isEmpty()) { + int sum = 0; + for (int h : boundaryUnderwaterHeights.values()) + sum += h; + flatUnderwaterHeight = sum / boundaryUnderwaterHeights.size(); + } + + ctx.horizonTileSample = Sample.water( + waterType, + ref.plane, + ref.flatHeight, + flatUnderwaterHeight, + boundarySurfaceHeights, + boundaryUnderwaterHeights + ); + log.info( + "Horizon water enabled for {} using reference tile [{}, {}, {}]: {} at height {}, {} boundary vertices sampled", + area.name, ref.worldX, ref.worldY, ref.plane, waterType, ref.flatHeight, boundaryUnderwaterHeights.size() + ); + } + return ctx.horizonTileSample; + } + + private static void buildBoundaryHeightMaps( + ZoneSceneContext ctx, + Area area, + int plane, + int flatHeight, + HashMap surfaceHeights, + HashMap underwaterHeights + ) { + area.normalize(); + for (AABB aabb : area.aabbs) { + if (aabb.minZ != Integer.MIN_VALUE && aabb.maxZ != Integer.MAX_VALUE && + (plane < aabb.minZ || plane > aabb.maxZ)) + continue; + + for (int x = aabb.minX; x <= aabb.maxX + 1; x++) { + sampleEdgeWithInwardSearch(ctx, plane, aabb, x, aabb.minY, 0, 1, flatHeight, surfaceHeights, underwaterHeights); + sampleEdgeWithInwardSearch(ctx, plane, aabb, x, aabb.maxY + 1, 0, -1, flatHeight, surfaceHeights, underwaterHeights); + } + for (int y = aabb.minY + 1; y <= aabb.maxY; y++) { + sampleEdgeWithInwardSearch(ctx, plane, aabb, aabb.minX, y, 1, 0, flatHeight, surfaceHeights, underwaterHeights); + sampleEdgeWithInwardSearch(ctx, plane, aabb, aabb.maxX + 1, y, -1, 0, flatHeight, surfaceHeights, underwaterHeights); + } + } + } + + private static void sampleEdgeWithInwardSearch( + ZoneSceneContext ctx, + int plane, + AABB aabb, + int boundaryX, + int boundaryY, + int stepX, + int stepY, + int flatHeight, + HashMap surfaceHeights, + @Nullable HashMap underwaterHeights + ) { + long boundaryKey = Sample.packWorldVertex(boundaryX, boundaryY); + if (underwaterHeights != null && underwaterHeights.containsKey(boundaryKey)) + return; + + if (underwaterHeights == null) { + surfaceHeights.putIfAbsent( + boundaryKey, + cornerSurfaceHeightAtWorldVertex(ctx, plane, boundaryX, boundaryY, flatHeight) + ); + return; + } + + for (int i = 0; i <= EDGE_INSET; i++) { + int worldX = boundaryX + stepX * i; + int worldY = boundaryY + stepY * i; + if (worldX < aabb.minX || worldX > aabb.maxX + 1 || + worldY < aabb.minY || worldY > aabb.maxY + 1) + break; + + int depth = interiorUnderwaterOffsetAtVertex(ctx, plane, worldX, worldY); + if (depth <= 0) + depth = cornerUnderwaterOffsetAtWorldVertex(ctx, plane, worldX, worldY); + + if (depth > 0) { + int boundarySurface = cornerSurfaceHeightAtWorldVertex(ctx, plane, boundaryX, boundaryY, flatHeight); + surfaceHeights.put(boundaryKey, boundarySurface); + underwaterHeights.put(boundaryKey, boundarySurface + depth); + return; + } + } + + surfaceHeights.putIfAbsent( + boundaryKey, + cornerSurfaceHeightAtWorldVertex(ctx, plane, boundaryX, boundaryY, flatHeight) + ); + } + + private static int resolveCornerUnderwaterHeight( + ZoneSceneContext ctx, + Sample sample, + AABB aabb, + int worldX, + int worldY + ) { + int boundaryX = clampVertexX(aabb, worldX); + int boundaryY = clampVertexY(aabb, worldY); + + Integer live = liveUnderwaterHeightAtWorldVertex(ctx, sample.plane, boundaryX, boundaryY); + if (live != null) + return live; + + long key = Sample.packWorldVertex(boundaryX, boundaryY); + HashMap underwaterHeights = sample.boundaryUnderwaterHeights; + Integer height = underwaterHeights.get(key); + if (height != null) { + Integer liveSurface = liveSurfaceHeightAtWorldVertex(ctx, sample.plane, boundaryX, boundaryY); + Integer cachedSurface = sample.boundarySurfaceHeights.get(key); + if (liveSurface != null && cachedSurface != null && !liveSurface.equals(cachedSurface)) + return height + (liveSurface - cachedSurface); + return height; + } + + Integer surface = sample.boundarySurfaceHeights.get(key); + if (surface != null) + return surface + Math.max(1, sample.flatUnderwaterHeight - sample.flatHeight); + + Integer liveSurface = liveSurfaceHeightAtWorldVertex(ctx, sample.plane, boundaryX, boundaryY); + if (liveSurface != null) + return liveSurface + Math.max(1, sample.flatUnderwaterHeight - sample.flatHeight); + + return sample.flatUnderwaterHeight; + } + + @Nullable + private static Integer liveUnderwaterHeightAtWorldVertex( + ZoneSceneContext ctx, + int plane, + int worldX, + int worldY + ) { + Integer surface = liveSurfaceHeightAtWorldVertex(ctx, plane, worldX, worldY); + if (surface == null) + return null; + + int depth = interiorUnderwaterOffsetAtVertex(ctx, plane, worldX, worldY); + if (depth <= 0) + depth = cornerUnderwaterOffsetAtWorldVertex(ctx, plane, worldX, worldY); + if (depth <= 0) + return null; + + return surface + depth; + } + + private static int interiorUnderwaterOffsetAtVertex( + ZoneSceneContext ctx, + int plane, + int worldX, + int worldY + ) { + if (ctx.sceneBase == null || ctx.vertexTerrainData == null) + return 0; + + int sceneX = worldX - ctx.sceneBase[0]; + int sceneY = worldY - ctx.sceneBase[1]; + int tileExX = sceneX + ctx.sceneOffset; + int tileExY = sceneY + ctx.sceneOffset; + + Tile[][][] tiles = ctx.scene.getExtendedTiles(); + for (int[] corner : CORNER_TILES) { + int tx = tileExX + corner[0]; + int ty = tileExY + corner[1]; + int vertexIndex = corner[2]; + if (tx < 0 || ty < 0 || tx >= EXTENDED_SCENE_SIZE || ty >= EXTENDED_SCENE_SIZE) + continue; + + Tile tile = tiles[plane][tx][ty]; + if (tile == null) + continue; + + if (tile.getBridge() != null) + tile = tile.getBridge(); + + if (tile.getSceneTilePaint() == null && tile.getSceneTileModel() == null) + continue; + + int[] keys = ProceduralGenerator.tileVertexKeys(ctx, tile); + if (ctx.vertexTerrainData.containsKey(keys[vertexIndex])) { + int depth = ctx.getVertexUnderwaterDepth(keys[vertexIndex]); + if (depth > 0) + return depth; + } + } + + return 0; + } + + private static int cornerUnderwaterOffsetAtWorldVertex( + ZoneSceneContext ctx, + int plane, + int worldX, + int worldY + ) { + if (ctx.sceneBase == null) + return 0; + + int sceneX = worldX - ctx.sceneBase[0]; + int sceneY = worldY - ctx.sceneBase[1]; + int tileExX = sceneX + ctx.sceneOffset; + int tileExY = sceneY + ctx.sceneOffset; + if (tileExX < 0 || tileExY < 0 || tileExX >= EXTENDED_SCENE_SIZE || tileExY >= EXTENDED_SCENE_SIZE) + return 0; + + if (ctx.underwaterDepthLevels != null && + tileExX < ctx.underwaterDepthLevels[plane].length && + tileExY < ctx.underwaterDepthLevels[plane][tileExX].length) { + int level = ctx.underwaterDepthLevels[plane][tileExX][tileExY]; + if (level > 0 && level <= ProceduralGenerator.DEPTH_LEVEL_SLOPE.length) { + int depth = ProceduralGenerator.DEPTH_LEVEL_SLOPE[level - 1]; + return (int) (depth * .55f); + } + } + + if (ctx.vertexTerrainData != null) { + int height = cornerSurfaceHeightAtWorldVertex(ctx, plane, worldX, worldY, 0); + int key = tileVertexHash(new int[] { + sceneX * LOCAL_TILE_SIZE, + height, + sceneY * LOCAL_TILE_SIZE + }); + if (ctx.vertexTerrainData.containsKey(key)) { + int depth = ctx.getVertexUnderwaterDepth(key); + if (depth > 0) + return depth; + } + } + + return 0; + } + + private int shouldExtendTile(ZoneSceneContext ctx, Sample sample, int tileExX, int tileExY) { + if (isFlatTerrain(ctx)) + return shouldExtendFlatTerrainTile(ctx, tileExX, tileExY, sample.plane) ? 2 : 0; + + return shouldRenderAtTile(ctx, tileExX, tileExY, sample.plane) ? 2 : 0; + } + + private int countTerrainShellFaces( + ZoneSceneContext ctx, + Area area, + Sample sample, + int mzx, + int mzz, + ShellEdges shell, + int[] clipBounds + ) { + int[] faces = { 0 }; + forEachShellStrip(shell, (minX, minZ, maxX, maxZ) -> + faces[0] += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, minX, minZ, maxX, maxZ, clipBounds) * 2); + return faces[0]; + } + + private void uploadTerrainZoneTiles( + VertexWriteCache.Collection writeCache, + ZoneSceneContext ctx, + Sample sample, + ZonePlan plan, + int[][] cornerHeights, + int mzx, + int mzz + ) { + int baseTileX = (mzx << 3) - ctx.sceneOffset; + int baseTileY = (mzz << 3) - ctx.sceneOffset; + + for (int xoff = 0; xoff < CHUNK_SIZE; ++xoff) { + for (int zoff = 0; zoff < CHUNK_SIZE; ++zoff) { + if (!plan.hasTile(xoff, zoff)) + continue; + + int sw = cornerHeights[xoff][zoff]; + int se = cornerHeights[xoff + 1][zoff]; + int nw = cornerHeights[xoff][zoff + 1]; + int ne = cornerHeights[xoff + 1][zoff + 1]; + int baseX = xoff * LOCAL_TILE_SIZE; + int baseZ = zoff * LOCAL_TILE_SIZE; + + int[] swWorld = ctx.sceneToWorld(baseTileX + xoff, baseTileY + zoff, sample.plane); + int[] seWorld = ctx.sceneToWorld(baseTileX + xoff + 1, baseTileY + zoff, sample.plane); + int[] nwWorld = ctx.sceneToWorld(baseTileX + xoff, baseTileY + zoff + 1, sample.plane); + int[] neWorld = ctx.sceneToWorld(baseTileX + xoff + 1, baseTileY + zoff + 1, sample.plane); + + uploadTerrainTile(writeCache, ctx, sample, baseX, baseZ, sw, se, nw, ne, swWorld, seWorld, nwWorld, neWorld); + } + } + } + + private void uploadFlatTilesInBounds( + VertexWriteCache.Collection writeCache, + ZoneSceneContext ctx, + Sample sample, + int mzx, + int mzz, + int minX, + int minZ, + int maxX, + int maxZ, + int height + ) { + int baseTileX = (mzx << 3) - ctx.sceneOffset; + int baseTileY = (mzz << 3) - ctx.sceneOffset; + int plane = sample.plane; + + int startTileX = floorDiv(minX, LOCAL_TILE_SIZE); + int startTileZ = floorDiv(minZ, LOCAL_TILE_SIZE); + int endTileX = floorDiv(maxX - 1, LOCAL_TILE_SIZE); + int endTileZ = floorDiv(maxZ - 1, LOCAL_TILE_SIZE); + + for (int tx = startTileX; tx <= endTileX; ++tx) { + for (int tz = startTileZ; tz <= endTileZ; ++tz) { + int baseX = tx * LOCAL_TILE_SIZE; + int baseZ = tz * LOCAL_TILE_SIZE; + if (baseX + LOCAL_TILE_SIZE <= minX || baseZ + LOCAL_TILE_SIZE <= minZ || + baseX >= maxX || baseZ >= maxZ) + continue; + + int[] swWorld = ctx.sceneToWorld(baseTileX + baseX / LOCAL_TILE_SIZE, baseTileY + baseZ / LOCAL_TILE_SIZE, plane); + int[] seWorld = ctx.sceneToWorld(baseTileX + baseX / LOCAL_TILE_SIZE + 1, baseTileY + baseZ / LOCAL_TILE_SIZE, plane); + int[] nwWorld = ctx.sceneToWorld(baseTileX + baseX / LOCAL_TILE_SIZE, baseTileY + baseZ / LOCAL_TILE_SIZE + 1, plane); + int[] neWorld = ctx.sceneToWorld(baseTileX + baseX / LOCAL_TILE_SIZE + 1, baseTileY + baseZ / LOCAL_TILE_SIZE + 1, plane); + + uploadTerrainTile(writeCache, ctx, sample, baseX, baseZ, height, height, height, height, swWorld, seWorld, nwWorld, neWorld); + } + } + } + + private Material resolveMaterial(Sample sample, int[] worldPos) { + if (plugin.configGroundTextures && sample.groundMaterial != null) + return sample.groundMaterial.getRandomMaterial(worldPos[0], worldPos[1], worldPos[2]); + return sample.material; + } + + private void uploadTerrainTile( + VertexWriteCache.Collection writeCache, + ZoneSceneContext ctx, + Sample sample, + int baseX, + int baseZ, + int sw, + int se, + int nw, + int ne, + int[] swWorld, + int[] seWorld, + int[] nwWorld, + int[] neWorld + ) { + if (sample.isWaterSurface()) { + putFlatQuad( + writeCache, + sample.waterType, sample.plane, + baseX, baseZ, baseX + LOCAL_TILE_SIZE, baseZ + LOCAL_TILE_SIZE, + sw, 127, 0 + ); + return; + } + + int terrainData = HDUtils.packTerrainData(true, 0, WaterType.NONE, sample.plane); + Material swMaterial = resolveMaterial(sample, swWorld); + Material seMaterial = resolveMaterial(sample, seWorld); + Material nwMaterial = resolveMaterial(sample, nwWorld); + Material neMaterial = resolveMaterial(sample, neWorld); + float uvx = fract(swWorld[0]); + float uvy = fract(swWorld[1]); + + putTriangle( + writeCache, + sample.neColor, sample.nwColor, sample.seColor, + neMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), + nwMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), + seMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), + terrainData, terrainData, terrainData, + baseX + LOCAL_TILE_SIZE, ne, baseZ + LOCAL_TILE_SIZE, fract(neWorld[0]), fract(neWorld[1]), + baseX, nw, baseZ + LOCAL_TILE_SIZE, uvx - 1, uvy, + baseX + LOCAL_TILE_SIZE, se, baseZ, fract(seWorld[0]), uvy - 1 + ); + putTriangle( + writeCache, + sample.swColor, sample.seColor, sample.nwColor, + swMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), + seMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), + nwMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), + terrainData, terrainData, terrainData, + baseX, sw, baseZ, uvx - 1, uvy - 1, + baseX + LOCAL_TILE_SIZE, se, baseZ, fract(seWorld[0]), uvy - 1, + baseX, nw, baseZ + LOCAL_TILE_SIZE, uvx - 1, uvy + ); + } +} diff --git a/src/main/java/rs117/hd/renderer/zone/SceneManager.java b/src/main/java/rs117/hd/renderer/zone/SceneManager.java index a9887a89d1..c3f5aee648 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneManager.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneManager.java @@ -29,6 +29,7 @@ import rs117.hd.scene.FishingSpotReplacer; import rs117.hd.scene.LightManager; import rs117.hd.scene.ProceduralGenerator; +import rs117.hd.scene.SceneContext; import rs117.hd.scene.areas.AABB; import rs117.hd.scene.areas.Area; import rs117.hd.utils.DestructibleHandler; @@ -82,6 +83,9 @@ public class SceneManager { @Inject private ProceduralGenerator proceduralGenerator; + @Inject + private HorizonExtender horizonExtender; + @Inject private NpcDisplacementCache npcDisplacementCache; @@ -287,6 +291,8 @@ private void updateAreaHiding() { // This should happen very rarely, so we invalidate all zones for simplicity root.invalidate(); } + root.sceneContext.horizonTileSample = null; + root.sceneContext.horizonTileMask = null; root.sceneContext.currentArea = newArea; } else { plugin.justChangedArea = false; @@ -350,6 +356,26 @@ public void invalidateZone(Scene scene, int zx, int zz) { log.trace("Zone invalidated: wx={} x={} z={}", scene.getWorldViewId(), zx, zz); } + @Nullable + private Area resolveHorizonTileArea(SceneContext ctx) { + if (ctx.sceneBase == null) + return null; + + for (Area area : areaManager.areasWithAreaHiding) { + if (area.hasHorizonTiles() && ctx.intersects(area)) + return area; + } + return null; + } + + private static boolean horizonRenderingChanged(@Nullable SceneContext prev, SceneContext next) { + if (prev == null) + return next.horizonTileArea != null; + boolean prevHorizon = prev.enableHorizonTiles && prev.horizonTileArea != null; + boolean nextHorizon = next.enableHorizonTiles && next.horizonTileArea != null; + return prevHorizon != nextHorizon; + } + private static boolean isEdgeTile(Zone[][] zones, int zx, int zz) { for (int x = zx - 2; x <= zx + 2; ++x) { if (x < 0 || x >= NUM_ZONES) @@ -466,6 +492,11 @@ public synchronized void loadScene(WorldView worldView, Scene scene) { nextSceneContext.enableAreaHiding = nextSceneContext.sceneBase != null && config.hideUnrelatedAreas(); nextSceneContext.fillGaps = config.fillGapsInTerrain(); + nextSceneContext.enableHorizonTiles = + nextSceneContext.enableAreaHiding && + config.horizonTiles(); + nextSceneContext.horizonTileArea = nextSceneContext.enableHorizonTiles ? + resolveHorizonTileArea(nextSceneContext) : null; if (nextSceneContext.intersects(areaManager.getArea("PLAYER_OWNED_HOUSE"))) { nextSceneContext.isInHouse = true; @@ -542,7 +573,8 @@ public synchronized void loadScene(WorldView worldView, Scene scene) { client.getGameState() == GameState.LOGGED_IN && // only reuse for async loads to respect roof removal state changes ctx.sceneContext.expandedMapLoadingChunks == nextSceneContext.expandedMapLoadingChunks && ctx.sceneContext.currentArea == nextSceneContext.currentArea && - !nextSceneContext.isInChambersOfXeric + !nextSceneContext.isInChambersOfXeric && + !horizonRenderingChanged(ctx.sceneContext, nextSceneContext) ) { for (int x = 0; x < NUM_ZONES; ++x) { for (int z = 0; z < NUM_ZONES; ++z) { @@ -557,7 +589,7 @@ public synchronized void loadScene(WorldView worldView, Scene scene) { old.needsRoofUpdate = true; - if (old.hasWater || old.dirty || isEdgeTile(ctx.zones, ox, oz)) { + if (old.hasWater || old.dirty || isEdgeTile(ctx.zones, ox, oz) || nextSceneContext.horizonTileArea != null) { float dist = distance(vec(x, z), vec(NUM_ZONES / 2, NUM_ZONES / 2)); sortedZones.add(SortedZone.getZone(old, x, z, dist)); nextSceneContext.totalDeferred++; @@ -577,6 +609,9 @@ public synchronized void loadScene(WorldView worldView, Scene scene) { !nextSceneContext.isInHouse && root.sceneContext != null && nextSceneContext.totalReused + nextSceneContext.totalDeferred > 0; + boolean leavingHorizon = ctx.sceneContext != null && + ctx.sceneContext.horizonTileArea != null && + nextSceneContext.horizonTileArea == null; for (int x = 0; x < NUM_ZONES; ++x) { for (int z = 0; z < NUM_ZONES; ++z) { Zone zone = nextZones[x][z]; @@ -602,7 +637,7 @@ public synchronized void loadScene(WorldView worldView, Scene scene) { for (SortedZone sorted : sortedZones) { Zone newZone = injector.getInstance(Zone.class); newZone.dirty = sorted.zone.dirty; - if (staggerLoad) { + if (staggerLoad && !leavingHorizon) { // Reuse the old zone while uploading a correct one sorted.zone.cull = false; sorted.zone.uploadJob = ZoneUploadJob @@ -610,6 +645,8 @@ public synchronized void loadScene(WorldView worldView, Scene scene) { sorted.zone.uploadJob.revealAfterTimestampMs = timeMs + ceil(clamp(sorted.dist / 15.0f, 0.25f, 1.5f) * 1000.0f); } else { + if (leavingHorizon) + sorted.zone.cull = true; nextZones[sorted.x][sorted.z] = newZone; ZoneUploadJob .build(ctx, nextSceneContext, newZone, true, sorted.x, sorted.z) diff --git a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java index 3a59e3b1cb..2310d8f815 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java @@ -110,6 +110,9 @@ public class SceneUploader implements AutoCloseable { @Inject private ProceduralGenerator proceduralGenerator; + @Inject + private HorizonExtender horizonExtender; + @FunctionalInterface public interface OnBeforeProcessTileFunc { void invoke(Tile t, boolean isEstimate) throws InterruptedException; @@ -176,6 +179,11 @@ public void clear() { faceOverrides.release(); faceMaterials.release(); faceUVTypes.release(); + + if (writeCache != null) { + writeCache.release(); + writeCache = null; + } } private void ensureVerticesAllocated(int vertexCount) { @@ -201,6 +209,7 @@ public void estimateZoneSize(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) if (ctx.fillGaps) estimateZoneGapFillers(ctx, zone, mzx, mzz); + horizonExtender.estimateForZone(ctx, zone, mzx, mzz); } public void uploadZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws InterruptedException { @@ -232,6 +241,8 @@ public void uploadZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws if (z == 0) { uploadZoneLevel(ctx, zone, mzx, mzz, 0, false, vb, ab, fb); + if (vb != null) + horizonExtender.uploadUnderwaterForZone(ctx, zone, mzx, mzz, vb, fb); uploadZoneLevel(ctx, zone, mzx, mzz, 0, true, vb, ab, fb); uploadZoneLevel(ctx, zone, mzx, mzz, 1, true, vb, ab, fb); uploadZoneLevel(ctx, zone, mzx, mzz, 2, true, vb, ab, fb); @@ -248,6 +259,7 @@ public void uploadZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws // Upload water surface tiles to be drawn after everything else if (zone.hasWater) uploadZoneWater(ctx, zone, mzx, mzz, vb, fb); + horizonExtender.uploadSurfaceForZone(ctx, zone, mzx, mzz, vb, fb); zone.levelOffsets[Zone.LEVEL_WATER_SURFACE] = vb.position(); if (ctx.fillGaps) @@ -2161,7 +2173,7 @@ else if (color3 == -1) } public void estimateZoneGapFillers(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) { - if (ctx.sceneBase == null || ctx.currentArea != null && !ctx.currentArea.fillGaps) + if (ctx.sceneBase == null || ctx.currentArea != null && (!ctx.currentArea.fillGaps || ctx.currentArea.hasHorizonTiles())) return; int sceneMin = -ctx.expandedMapLoadingChunks * CHUNK_SIZE; @@ -2202,7 +2214,8 @@ public void uploadZoneGapFillers( GpuIntBuffer vb, GpuIntBuffer fb ) { - if (ctx.sceneBase == null || !zone.hasGapFiller || ctx.currentArea != null && !ctx.currentArea.fillGaps) + if (ctx.sceneBase == null || !zone.hasGapFiller || + ctx.currentArea != null && (!ctx.currentArea.fillGaps || ctx.currentArea.hasHorizonTiles())) return; assert vb != null && fb != null; diff --git a/src/main/java/rs117/hd/renderer/zone/WaterHorizonExtender.java b/src/main/java/rs117/hd/renderer/zone/WaterHorizonExtender.java new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java index 4ba110f7a6..a159141950 100644 --- a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java +++ b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java @@ -819,7 +819,7 @@ public boolean zoneInFrustum(int zx, int zz, int maxY, int minY) { if (plugin.enableDetailedTimers) frameTimer.begin(Timer.VISIBILITY_CHECK); int minX = zx * CHUNK_SIZE - ctx.sceneContext.sceneOffset; int minZ = zz * CHUNK_SIZE - ctx.sceneContext.sceneOffset; - if (ctx.sceneContext.currentArea != null) { + if (ctx.sceneContext.currentArea != null && ctx.sceneContext.horizonTileArea == null) { var base = ctx.sceneContext.sceneBase; assert base != null; boolean inArea = ctx.sceneContext.currentArea.intersects( diff --git a/src/main/java/rs117/hd/scene/ProceduralGenerator.java b/src/main/java/rs117/hd/scene/ProceduralGenerator.java index 097b6bd3ad..48885d3765 100644 --- a/src/main/java/rs117/hd/scene/ProceduralGenerator.java +++ b/src/main/java/rs117/hd/scene/ProceduralGenerator.java @@ -30,6 +30,7 @@ import lombok.extern.slf4j.Slf4j; import net.runelite.api.*; import rs117.hd.renderer.legacy.LegacySceneContext; +import rs117.hd.renderer.zone.HorizonExtender; import rs117.hd.scene.materials.Material; import rs117.hd.scene.model_overrides.ModelOverride; import rs117.hd.scene.model_overrides.TzHaarRecolorType; @@ -84,6 +85,9 @@ public class ProceduralGenerator { @Inject private WaterTypeManager waterTypeManager; + @Inject + private HorizonExtender horizonExtender; + private final ConcurrentPool GENERATOR_POOL = new ConcurrentPool<>(GeneratorContext::new); final class GeneratorContext implements AutoCloseable { @@ -120,6 +124,9 @@ public void clearSceneData(SceneContext sceneContext) { sceneContext.tileOverrideIndices = null; sceneContext.vertexTerrainTexture = null; sceneContext.vertexTerrainNormals = null; + sceneContext.horizonTileSample = null; + sceneContext.horizonTileMask = null; + sceneContext.underwaterDepthLevels = null; } public static void faceVertexKeys(Tile tile, int face, int[][] vertices, int[] vertexHashes) { @@ -138,6 +145,9 @@ public static int[] faceVertexKeys(Tile tile, int face) { } public void generateSceneData(SceneContext sceneCtx, SceneContext prevSceneCtx) { + sceneCtx.horizonTileSample = null; + sceneCtx.horizonTileMask = null; + sceneCtx.underwaterDepthLevels = null; try (GeneratorContext ctx = GENERATOR_POOL.acquire()) { long timerTotal = System.currentTimeMillis(); long timerCalculateMainOverrides, timerCalculateTerrainNormals, timerGenerateTerrainData, timerGenerateUnderwaterTerrain; @@ -821,6 +831,8 @@ private void generate(SceneContext sceneContext, SceneContext prevSceneCtx) { } } + sceneContext.underwaterDepthLevels = underwaterDepthLevels; + int minZ = MAX_Z, maxZ = 0; Arrays.fill(minX, sizeX); Arrays.fill(minY, sizeY); @@ -983,6 +995,8 @@ private void generate(SceneContext sceneContext, SceneContext prevSceneCtx) { } } + horizonExtender.prepareSceneTerrain(sceneContext, tiles); + // Sink terrain further from shore by desired levels. // noinspection ConstantValue assert DEPTH_LEVEL_SLOPE.length <= Byte.MAX_VALUE; @@ -999,8 +1013,12 @@ private void generate(SceneContext sceneContext, SceneContext prevSceneCtx) { // it creates a 'wall' to prevent fog from passing through. // Not incredibly effective, but better than nothing. if (x == 0 || y == 0 || x == EXTENDED_SCENE_SIZE || y == EXTENDED_SCENE_SIZE) { - zUnderwaterDepthLevels[x][y] = 0; - continue; + if (!(HorizonExtender.isEnabled(sceneContext) && + HorizonExtender.shouldPatchWaterAtTile(sceneContext, x, y, z))) + { + zUnderwaterDepthLevels[x][y] = 0; + continue; + } } if (zUnderwaterDepthLevels[x - 1][y] < tileHeight || diff --git a/src/main/java/rs117/hd/scene/SceneContext.java b/src/main/java/rs117/hd/scene/SceneContext.java index a031ee5201..72ce6721df 100644 --- a/src/main/java/rs117/hd/scene/SceneContext.java +++ b/src/main/java/rs117/hd/scene/SceneContext.java @@ -7,6 +7,7 @@ import javax.annotation.Nullable; import net.runelite.api.*; import net.runelite.api.coords.*; +import rs117.hd.renderer.zone.HorizonExtender; import rs117.hd.scene.areas.AABB; import rs117.hd.scene.areas.Area; import rs117.hd.scene.environments.Environment; @@ -59,6 +60,16 @@ public class SceneContext { public int numVisibleLights = 0; public boolean enableAreaHiding; public boolean fillGaps; + public boolean enableHorizonTiles; + + @Nullable + public HorizonExtender.Sample horizonTileSample; + @Nullable + public boolean[][] horizonTileMask; + @Nullable + public byte[][][] underwaterDepthLevels; + @Nullable + public Area horizonTileArea; public boolean isInChambersOfXeric; public boolean isInHouse; @@ -94,6 +105,12 @@ public void setVertexIsLand(int hash) { vertexTerrainData.or(hash, VERTEX_IS_LAND, 0); } + public void clearVertexIsLand(int hash) { + if (vertexTerrainData == null || !vertexTerrainData.containsKey(hash)) + return; + vertexTerrainData.and(hash, ~VERTEX_IS_LAND, 0); + } + public void setVertexIsWater(int hash) { vertexTerrainData.or(hash, VERTEX_IS_WATER, 0); } diff --git a/src/main/java/rs117/hd/scene/areas/Area.java b/src/main/java/rs117/hd/scene/areas/Area.java index 866a3012b5..b9c05b4b75 100644 --- a/src/main/java/rs117/hd/scene/areas/Area.java +++ b/src/main/java/rs117/hd/scene/areas/Area.java @@ -20,6 +20,9 @@ public class Area { public boolean hideOtherAreas; public boolean fillGaps = true; + public int[] horizonTileReference; + public boolean horizonFlatTerrain; + public String[] areas; public int[] regions; @@ -249,6 +252,10 @@ public boolean intersects(AABB... otherAabbs) { return false; } + public boolean hasHorizonTiles() { + return horizonTileReference != null && horizonTileReference.length >= 2; + } + @Override public String toString() { return name; diff --git a/src/main/resources/rs117/hd/scene/areas.json b/src/main/resources/rs117/hd/scene/areas.json index ab0b40f433..a5a873cfb7 100644 --- a/src/main/resources/rs117/hd/scene/areas.json +++ b/src/main/resources/rs117/hd/scene/areas.json @@ -7713,7 +7713,9 @@ "name": "PEST_CONTROL", "regions": [ 10536 - ] + ], + "horizonTileReference": [ 2666, 6212 ], + "hideOtherAreas": true }, { "name": "CRASH_ISLAND", @@ -8326,6 +8328,7 @@ "aabbs": [ [ 1856, 4800, 1919, 4862 ] ], + "horizonTileReference": [ 1886, 4818 ], "hideOtherAreas": true }, { @@ -8333,6 +8336,7 @@ "aabbs": [ [ 1984, 4800, 2047, 4862 ] ], + "horizonTileReference": [ 2014, 4819 ], "hideOtherAreas": true }, { @@ -8340,6 +8344,7 @@ "aabbs": [ [ 1920, 4800, 1983, 4862 ] ], + "horizonTileReference": [ 2014, 4819 ], "hideOtherAreas": true }, { @@ -8421,7 +8426,9 @@ "aabbs": [ [ 2553, 4802, 2622, 4864 ] ], - "hideOtherAreas": true + "hideOtherAreas": true, + "horizonTileReference": [ 2580, 4825 ], + "horizonFlatTerrain": true }, { "name": "FIRE_ALTAR_CULL_FIRE_LIGHTS", @@ -8494,9 +8501,10 @@ { "name": "WATER_ALTAR", "aabbs": [ - [ 2688, 4800, 2750, 4862 ] + [ 2689, 4859, 2748, 4802 ] ], - "hideOtherAreas": true + "hideOtherAreas": true, + "horizonTileReference": [ 2709, 4804 ] }, { "name": "MIND_ALTAR", @@ -8518,7 +8526,8 @@ "aabbs": [ [ 3202, 4800, 3263, 4863 ] ], - "hideOtherAreas": true + "hideOtherAreas": true, + "horizonTileReference": [ 3216, 3437 ] }, { "name": "TRUE_BLOOD_ALTAR_CRYSTAL_POOL",