From 8d46fd3a5aa42fe89718a6b8bdd72f51ee59d82f Mon Sep 17 00:00:00 2001 From: Mark7625 <72366279+Mark7625@users.noreply.github.com> Date: Sun, 31 May 2026 02:15:36 +0100 Subject: [PATCH 1/4] init --- build.gradle | 1 - schemas/areas.schema.json | 13 + src/main/java/rs117/hd/HdPlugin.java | 1 + src/main/java/rs117/hd/HdPluginConfig.java | 20 +- .../rs117/hd/renderer/zone/GapFiller.java | 4 +- .../rs117/hd/renderer/zone/SceneManager.java | 19 +- .../rs117/hd/renderer/zone/SceneUploader.java | 8 + .../rs117/hd/renderer/zone/WaterExtender.java | 905 ++++++++++++++++++ .../rs117/hd/renderer/zone/ZoneRenderer.java | 2 +- .../rs117/hd/scene/ExtendWaterSample.java | 33 + .../rs117/hd/scene/ExtendWaterTileUtil.java | 134 +++ .../rs117/hd/scene/ProceduralGenerator.java | 19 +- .../java/rs117/hd/scene/SceneContext.java | 8 + src/main/java/rs117/hd/scene/areas/Area.java | 2 + src/main/resources/rs117/hd/scene/areas.json | 10 +- 15 files changed, 1165 insertions(+), 14 deletions(-) create mode 100644 src/main/java/rs117/hd/renderer/zone/WaterExtender.java create mode 100644 src/main/java/rs117/hd/scene/ExtendWaterSample.java create mode 100644 src/main/java/rs117/hd/scene/ExtendWaterTileUtil.java 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..b041482d25 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." }, + "extendWater": { + "type": "boolean", + "description": "Whether to fill empty scene tiles outside this area's AABBs with water matching extendWaterReferenceTile." + }, + "extendWaterReferenceTile": { + "type": "array", + "description": "World tile coordinates [x, y] or [x, y, plane] used to sample water type, height and material.", + "items": { + "type": "integer" + }, + "minItems": 2, + "maxItems": 3 + }, "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 5819dac606..8f67bb5d26 100644 --- a/src/main/java/rs117/hd/HdPlugin.java +++ b/src/main/java/rs117/hd/HdPlugin.java @@ -1794,6 +1794,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 b24ab75201..c568cd0d54 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 terrain at the edge of the loaded map outside hidden areas 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/GapFiller.java b/src/main/java/rs117/hd/renderer/zone/GapFiller.java index a146b19b2e..0d9e5f2eed 100644 --- a/src/main/java/rs117/hd/renderer/zone/GapFiller.java +++ b/src/main/java/rs117/hd/renderer/zone/GapFiller.java @@ -24,7 +24,7 @@ public class GapFiller { private MaterialManager materialManager; public void estimateForZone(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.extendWater)) return; int sceneMin = -ctx.expandedMapLoadingChunks * CHUNK_SIZE; @@ -53,7 +53,7 @@ public void uploadForZone( GpuIntBuffer vb, GpuIntBuffer fb ) { - if (ctx.sceneBase == null || ctx.currentArea != null && !ctx.currentArea.fillGaps) + if (ctx.sceneBase == null || ctx.currentArea != null && (!ctx.currentArea.fillGaps || ctx.currentArea.extendWater)) return; int basex = (mzx - (ctx.sceneOffset >> 3)) << 10; diff --git a/src/main/java/rs117/hd/renderer/zone/SceneManager.java b/src/main/java/rs117/hd/renderer/zone/SceneManager.java index 38b52ef87e..48415f6155 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneManager.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneManager.java @@ -31,6 +31,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; @@ -350,6 +351,18 @@ public void invalidateZone(Scene scene, int zx, int zz) { log.trace("Zone invalidated: wx={} x={} z={}", scene.getWorldViewId(), zx, zz); } + @Nullable + private Area resolveExtendWaterArea(SceneContext ctx) { + if (ctx.sceneBase == null) + return null; + + for (Area area : areaManager.areasWithAreaHiding) { + if (area.extendWater && ctx.intersects(area)) + return area; + } + return null; + } + 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) @@ -457,6 +470,10 @@ public synchronized void loadScene(WorldView worldView, Scene scene) { nextSceneContext.enableAreaHiding = nextSceneContext.sceneBase != null && config.hideUnrelatedAreas(); nextSceneContext.fillGaps = config.fillGapsInTerrain(); + nextSceneContext.enableExtendWater = + nextSceneContext.enableAreaHiding && config.horizonTiles(); + nextSceneContext.extendWaterArea = nextSceneContext.enableExtendWater ? + resolveExtendWaterArea(nextSceneContext) : null; if (nextSceneContext.intersects(areaManager.getArea("PLAYER_OWNED_HOUSE"))) { nextSceneContext.isInHouse = true; @@ -548,7 +565,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.extendWaterArea != null) { float dist = distance(vec(x, z), vec(NUM_ZONES / 2, NUM_ZONES / 2)); sortedZones.add(SortedZone.getZone(old, x, z, dist)); nextSceneContext.totalDeferred++; diff --git a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java index 7dd20b2408..f5b2562778 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java @@ -112,6 +112,9 @@ public class SceneUploader implements AutoCloseable { @Inject private GapFiller gapFiller; + @Inject + private WaterExtender waterExtender; + @FunctionalInterface public interface OnBeforeProcessTileFunc { void invoke(Tile t, boolean isEstimate) throws InterruptedException; @@ -196,6 +199,8 @@ public void estimateZoneSize(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) if (ctx.fillGaps) gapFiller.estimateForZone(ctx, zone, mzx, mzz); + + waterExtender.estimateForZone(ctx, zone, mzx, mzz); } public void uploadZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws InterruptedException { @@ -224,6 +229,8 @@ public void uploadZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws if (z == 0) { uploadZoneLevel(ctx, zone, mzx, mzz, 0, false, roofIds, vb, ab, fb); + if (vb != null) + waterExtender.uploadUnderwaterForZone(ctx, zone, mzx, mzz, vb, fb); uploadZoneLevel(ctx, zone, mzx, mzz, 0, true, roofIds, vb, ab, fb); uploadZoneLevel(ctx, zone, mzx, mzz, 1, true, roofIds, vb, ab, fb); uploadZoneLevel(ctx, zone, mzx, mzz, 2, true, roofIds, vb, ab, fb); @@ -240,6 +247,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); + waterExtender.uploadSurfaceForZone(ctx, zone, mzx, mzz, vb, fb); zone.levelOffsets[Zone.LEVEL_WATER_SURFACE] = vb.position(); if (ctx.fillGaps) diff --git a/src/main/java/rs117/hd/renderer/zone/WaterExtender.java b/src/main/java/rs117/hd/renderer/zone/WaterExtender.java new file mode 100644 index 0000000000..ad2bbd2907 --- /dev/null +++ b/src/main/java/rs117/hd/renderer/zone/WaterExtender.java @@ -0,0 +1,905 @@ +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.*; +import rs117.hd.HdPlugin; +import rs117.hd.scene.ExtendWaterSample; +import rs117.hd.scene.ExtendWaterTileUtil; +import rs117.hd.scene.ProceduralGenerator; +import rs117.hd.scene.SceneContext; +import rs117.hd.scene.TileOverrideManager; +import rs117.hd.scene.areas.Area; +import rs117.hd.scene.areas.AABB; +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.*; +import static net.runelite.api.Perspective.LOCAL_TILE_SIZE; +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.MathUtils.fract; + +@Slf4j +@Singleton +public class WaterExtender { + private static final int[] UP_NORMAL = { 0, -1, 0 }; + private static final int EDGE_INSET = 4; + + @Inject + private HdPlugin plugin; + + @Inject + private TileOverrideManager tileOverrideManager; + + @Inject + private ProceduralGenerator proceduralGenerator; + + /** + * Marks horizon tiles as water in procedural terrain data and clears land flags on adjacent water faces. + * Call once during underwater terrain generation before depth sinking. + */ + public void prepareSceneTerrain(SceneContext ctx, Tile[][][] tiles) { + if (!ExtendWaterTileUtil.isEnabled(ctx)) + return; + + ExtendWaterTileUtil.buildHorizonTileMask(ctx); + boolean[][] mask = ctx.horizonTileMask; + if (mask == null) + return; + + int z = ExtendWaterTileUtil.getExtendWaterPlane(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]) + continue; + + if (ctx.tileIsWater[z][x][y]) + continue; + + Tile tile = tiles[z][x][y]; + if (tile != null && hasVisibleLand(ctx, tile, x, y, z)) + continue; + + ctx.tileIsWater[z][x][y] = true; + + if (tile == null || ExtendWaterTileUtil.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 && !ExtendWaterTileUtil.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.tileIsWater[z][nx][ny]) + 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.vertexIsLand.remove(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.vertexIsLand.remove(key); + } + } + + public void estimateForZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) { + if (!isEnabled(ctx)) + return; + + 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; + if (shouldExtendTile(ctx, tileExX, tileExY) > 0) { + zone.hasWater = true; + // underwater + surface + zone.sizeO += 4; + zone.sizeF += 4; + } + } + } + } + + public void uploadUnderwaterForZone( + ZoneSceneContext ctx, + Zone zone, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + uploadForZone(ctx, zone, mzx, mzz, vb, fb, true); + } + + public void uploadSurfaceForZone( + ZoneSceneContext ctx, + Zone zone, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + uploadForZone(ctx, zone, mzx, mzz, vb, fb, false); + } + + private void uploadForZone( + ZoneSceneContext ctx, + Zone zone, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb, + boolean underwater + ) { + ExtendWaterSample sample = resolveSample(ctx); + if (sample == null) + return; + + Area area = ExtendWaterTileUtil.getExtendWaterArea(ctx); + assert area != null; + + int basex = (mzx - (ctx.sceneOffset >> 3)) << 10; + int basez = (mzz - (ctx.sceneOffset >> 3)) << 10; + + int posBefore = vb.position(); + 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; + if (shouldExtendTile(ctx, tileExX, tileExY) > 0) { + int tileX = tileExX - ctx.sceneOffset; + int tileY = tileExY - ctx.sceneOffset; + int[] worldPos = ctx.sceneToWorld(tileX, tileY, sample.plane); + if (underwater) { + uploadUnderwaterTile(ctx, area, sample, worldPos, tileX, tileY, basex, basez, vb, fb); + } else { + uploadSurfaceTile(ctx, area, sample, worldPos, tileX, tileY, basex, basez, vb, fb); + } + } + } + } + + if (vb.position() > posBefore) + zone.hasWater = true; + } + + private static boolean isEnabled(ZoneSceneContext ctx) { + return ExtendWaterTileUtil.isEnabled(ctx); + } + + @Nullable + private ExtendWaterSample resolveSample(ZoneSceneContext ctx) { + Area area = ExtendWaterTileUtil.getExtendWaterArea(ctx); + if (!isEnabled(ctx)) + return null; + + if (ctx.extendWaterSample != null) + return ctx.extendWaterSample; + + assert area != null; + int[] ref = area.extendWaterReferenceTile; + int worldX = ref[0]; + int worldY = ref[1]; + int plane = ref.length > 2 ? ref[2] : 0; + + WaterType waterType = WaterType.NONE; + int textureId = -1; + int flatHeight = 0; + TileOverride override = TileOverride.NONE; + + 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); + } else { + SceneTileModel model = tile.getSceneTileModel(); + if (model != null && model.getTriangleTextureId() != null) { + for (int faceTexture : model.getTriangleTextureId()) { + if (faceTexture != -1) { + textureId = faceTexture; + break; + } + } + } + flatHeight = averageHeight(tileHeights, plane, tileExX, tileExY); + } + waterType = proceduralGenerator.seasonalWaterType(override, textureId); + } + } + } + + if (waterType == WaterType.NONE) { + int[] worldPos = { worldX, worldY, plane }; + if (override == TileOverride.NONE) + override = tileOverrideManager.getOverrideBeforeReplacements(worldPos); + waterType = proceduralGenerator.seasonalWaterType(override, textureId); + } + + if (waterType == WaterType.NONE) + waterType = WaterType.WATER; + + HashMap boundarySurfaceHeights = new HashMap<>(); + HashMap boundaryUnderwaterHeights = new HashMap<>(); + buildBoundaryHeightMaps(ctx, area, plane, flatHeight, boundarySurfaceHeights, boundaryUnderwaterHeights); + + int flatUnderwaterHeight = flatHeight + (int) (ProceduralGenerator.MAX_DEPTH * .55f); + if (!boundaryUnderwaterHeights.isEmpty()) { + int sum = 0; + for (int h : boundaryUnderwaterHeights.values()) + sum += h; + flatUnderwaterHeight = sum / boundaryUnderwaterHeights.size(); + } + + ctx.extendWaterSample = new ExtendWaterSample( + waterType, + plane, + flatHeight, + flatUnderwaterHeight, + boundarySurfaceHeights, + boundaryUnderwaterHeights + ); + log.info( + "ExtendWater enabled for {} using reference tile [{}, {}, {}]: {} at height {}, {} boundary vertices sampled", + area.name, + worldX, + worldY, + plane, + waterType, + flatHeight, + boundaryUnderwaterHeights.size() + ); + return ctx.extendWaterSample; + } + + 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 + ); + } + } + } + + /** + * Sample underwater height along an AABB edge, stepping inward when the boundary + * vertex has no water depth (common on the south shore where minY is land). + * Results are stored under the boundary vertex key so extended tiles can look them up. + */ + private static void sampleEdgeWithInwardSearch( + ZoneSceneContext ctx, + int plane, + AABB aabb, + int boundaryX, + int boundaryY, + int stepX, + int stepY, + int flatHeight, + HashMap surfaceHeights, + HashMap underwaterHeights + ) { + long boundaryKey = ExtendWaterSample.packWorldVertex(boundaryX, boundaryY); + if (underwaterHeights.containsKey(boundaryKey)) + 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) + ); + } + + @Nullable + 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 clampBoundaryVertexX(AABB aabb, int worldX) { + return Math.min(Math.max(worldX, aabb.minX), aabb.maxX + 1); + } + + private static int clampBoundaryVertexY(AABB aabb, int worldY) { + return Math.min(Math.max(worldY, aabb.minY), aabb.maxY + 1); + } + + private static int resolveCornerSurfaceHeight( + ZoneSceneContext ctx, + ExtendWaterSample sample, + AABB aabb, + int worldX, + int worldY + ) { + int boundaryX = clampBoundaryVertexX(aabb, worldX); + int boundaryY = clampBoundaryVertexY(aabb, worldY); + + Integer live = liveSurfaceHeightAtWorldVertex(ctx, sample.plane, boundaryX, boundaryY); + if (live != null) + return live; + + long key = ExtendWaterSample.packWorldVertex(boundaryX, boundaryY); + Integer height = sample.boundarySurfaceHeights.get(key); + return height != null ? height : sample.flatHeight; + } + + private static int resolveCornerUnderwaterHeight( + ZoneSceneContext ctx, + ExtendWaterSample sample, + AABB aabb, + int worldX, + int worldY + ) { + int boundaryX = clampBoundaryVertexX(aabb, worldX); + int boundaryY = clampBoundaryVertexY(aabb, worldY); + + Integer live = liveUnderwaterHeightAtWorldVertex(ctx, sample.plane, boundaryX, boundaryY); + if (live != null) + return live; + + long key = ExtendWaterSample.packWorldVertex(boundaryX, boundaryY); + Integer height = sample.boundaryUnderwaterHeights.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 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]; + } + + @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.vertexUnderwaterDepth == 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; + + int[][] cornerTiles = { + { 0, 0, 0 }, + { -1, 0, 1 }, + { 0, -1, 2 }, + { -1, -1, 3 }, + }; + + Tile[][][] tiles = ctx.scene.getExtendedTiles(); + for (int[] corner : cornerTiles) { + 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); + Integer depth = ctx.vertexUnderwaterDepth.get(keys[vertexIndex]); + if (depth != null && 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.vertexUnderwaterDepth != null) { + int height = cornerSurfaceHeightAtWorldVertex(ctx, plane, worldX, worldY, 0); + int key = HDUtils.fastVertexHash(new int[] { + sceneX * LOCAL_TILE_SIZE, + sceneY * LOCAL_TILE_SIZE, + height + }); + Integer depth = ctx.vertexUnderwaterDepth.get(key); + if (depth != null && depth > 0) + return depth; + } + + return 0; + } + + private static int cornerSurfaceHeightAtWorldVertex( + ZoneSceneContext ctx, + int plane, + int worldX, + int worldY, + int fallback + ) { + if (ctx.sceneBase == null) + return fallback; + + 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 >= ctx.scene.getTileHeights()[plane].length || + tileExY >= ctx.scene.getTileHeights()[plane][tileExX].length) + return fallback; + + return ctx.scene.getTileHeights()[plane][tileExX][tileExY]; + } + + 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; + } + + /** + * @return face count if the tile should be extended with water, otherwise 0 + */ + private int shouldExtendTile(ZoneSceneContext ctx, int tileExX, int tileExY) { + if (tileExX < 0 || tileExY < 0 || tileExX >= EXTENDED_SCENE_SIZE || tileExY >= EXTENDED_SCENE_SIZE) + return 0; + + ExtendWaterSample sample = resolveSample(ctx); + if (sample == null) + return 0; + + int tileZ = sample.plane; + + if (!ExtendWaterTileUtil.shouldExtendWaterAtTile(ctx, tileExX, tileExY, tileZ)) + return 0; + + if (ctx.tileIsWater != null && + ctx.tileIsWater[tileZ][tileExX][tileExY] && + (ctx.filledTiles[tileExX][tileExY] & (1 << tileZ)) != 0) + return 0; + + Tile[][][] extendedTiles = ctx.scene.getExtendedTiles(); + Tile tile = extendedTiles[tileZ][tileExX][tileExY]; + + if (tile == null || ExtendWaterTileUtil.isHiddenGroundTile(tile)) + return 2; + + if ((ctx.filledTiles[tileExX][tileExY] & (1 << tileZ)) != 0 && hasVisibleLand(ctx, tile, tileExX, tileExY, tileZ)) + return 0; + + return 2; + } + + private boolean hasVisibleLand(SceneContext ctx, Tile tile, int tileExX, int tileExY, int tileZ) { + if (tile.getBridge() != null) + tile = tile.getBridge(); + + SceneTilePaint paint = tile.getSceneTilePaint(); + if (paint != null && paint.getNeColor() != HIDDEN_HSL) { + int[] worldPos = ctx.extendedSceneToWorld(tileExX, tileExY, tileZ); + var override = tileOverrideManager.getOverride(ctx, tile, worldPos); + if (proceduralGenerator.seasonalWaterType(override, paint.getTexture()) != WaterType.NONE) + return false; + return true; + } + + SceneTileModel model = tile.getSceneTileModel(); + if (model == null) + return false; + + int[] worldPos = ctx.extendedSceneToWorld(tileExX, tileExY, tileZ); + int overlayId = OVERLAY_FLAG | ctx.scene.getOverlayIds()[tileZ][tileExX][tileExY]; + int underlayId = ctx.scene.getUnderlayIds()[tileZ][tileExX][tileExY]; + 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; + + return true; + } + return false; + } + + private void uploadUnderwaterTile( + ZoneSceneContext ctx, + Area area, + ExtendWaterSample sample, + int[] worldPos, + int tileX, + int tileY, + int zoneBasex, + int zoneBasez, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + int tileZ = sample.plane; + AABB aabb = nearestAabb(area, worldPos[0], worldPos[1], tileZ); + if (aabb == null) + return; + + int swHeight = resolveCornerUnderwaterHeight(ctx, sample, aabb, worldPos[0], worldPos[1]); + int seHeight = resolveCornerUnderwaterHeight(ctx, sample, aabb, worldPos[0] + 1, worldPos[1]); + int nwHeight = resolveCornerUnderwaterHeight(ctx, sample, aabb, worldPos[0], worldPos[1] + 1); + int neHeight = resolveCornerUnderwaterHeight(ctx, sample, aabb, worldPos[0] + 1, worldPos[1] + 1); + + int swDepth = max(1, swHeight - resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0], worldPos[1])); + int seDepth = max(1, seHeight - resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0] + 1, worldPos[1])); + int nwDepth = max(1, nwHeight - resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0], worldPos[1] + 1)); + int neDepth = max(1, neHeight - resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0] + 1, worldPos[1] + 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(worldPos[0], worldPos[1], worldPos[2]); + seMaterial = groundMaterial.getRandomMaterial(worldPos[0] + 1, worldPos[1], worldPos[2]); + nwMaterial = groundMaterial.getRandomMaterial(worldPos[0], worldPos[1] + 1, worldPos[2]); + neMaterial = groundMaterial.getRandomMaterial(worldPos[0] + 1, worldPos[1] + 1, worldPos[2]); + } + + int swTerrainData = HDUtils.packTerrainData(true, max(1, swDepth), sample.waterType, tileZ); + int seTerrainData = HDUtils.packTerrainData(true, max(1, seDepth), sample.waterType, tileZ); + int nwTerrainData = HDUtils.packTerrainData(true, max(1, nwDepth), sample.waterType, tileZ); + int neTerrainData = HDUtils.packTerrainData(true, max(1, neDepth), sample.waterType, tileZ); + int swMaterialData = swMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); + int seMaterialData = seMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); + int nwMaterialData = nwMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); + int neMaterialData = neMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); + + float uvx = fract(worldPos[0]); + float uvy = fract(worldPos[1]); + + int baseX = tileX * LOCAL_TILE_SIZE - zoneBasex; + int baseZ = tileY * LOCAL_TILE_SIZE - zoneBasez; + + putTerrainTriangle( + vb, fb, + UNDERWATER_HSL, UNDERWATER_HSL, UNDERWATER_HSL, + neMaterialData, nwMaterialData, seMaterialData, + neTerrainData, nwTerrainData, seTerrainData, + baseX + LOCAL_TILE_SIZE, neHeight, baseZ + LOCAL_TILE_SIZE, uvx, uvy, + baseX, nwHeight, baseZ + LOCAL_TILE_SIZE, uvx - 1, uvy, + baseX + LOCAL_TILE_SIZE, seHeight, baseZ, uvx, uvy - 1, + UP_NORMAL + ); + putTerrainTriangle( + vb, fb, + UNDERWATER_HSL, UNDERWATER_HSL, UNDERWATER_HSL, + swMaterialData, seMaterialData, nwMaterialData, + swTerrainData, seTerrainData, nwTerrainData, + baseX, swHeight, baseZ, uvx - 1, uvy - 1, + baseX + LOCAL_TILE_SIZE, seHeight, baseZ, uvx, uvy - 1, + baseX, nwHeight, baseZ + LOCAL_TILE_SIZE, uvx - 1, uvy, + UP_NORMAL + ); + } + + private void uploadSurfaceTile( + ZoneSceneContext ctx, + Area area, + ExtendWaterSample sample, + int[] worldPos, + int tileX, + int tileY, + int zoneBasex, + int zoneBasez, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + int tileZ = sample.plane; + AABB aabb = nearestAabb(area, worldPos[0], worldPos[1], tileZ); + if (aabb == null) + return; + + int swHeight = resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0], worldPos[1]); + int seHeight = resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0] + 1, worldPos[1]); + int nwHeight = resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0], worldPos[1] + 1); + int neHeight = resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0] + 1, worldPos[1] + 1); + + int terrainData = HDUtils.packTerrainData(true, 0, sample.waterType, tileZ); + int packedMaterial = Material.NONE.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); + int color = 127; + + int baseX = tileX * LOCAL_TILE_SIZE - zoneBasex; + int baseZ = tileY * LOCAL_TILE_SIZE - zoneBasez; + + putTerrainTriangle( + vb, fb, + color, color, color, + packedMaterial, packedMaterial, packedMaterial, + terrainData, terrainData, terrainData, + baseX + LOCAL_TILE_SIZE, neHeight, baseZ + LOCAL_TILE_SIZE, 0, 0, + baseX, nwHeight, baseZ + LOCAL_TILE_SIZE, 0, 0, + baseX + LOCAL_TILE_SIZE, seHeight, baseZ, 0, 0, + UP_NORMAL + ); + putTerrainTriangle( + vb, fb, + color, color, color, + packedMaterial, packedMaterial, packedMaterial, + terrainData, terrainData, terrainData, + baseX, swHeight, baseZ, 0, 0, + baseX + LOCAL_TILE_SIZE, seHeight, baseZ, 0, 0, + baseX, nwHeight, baseZ + LOCAL_TILE_SIZE, 0, 0, + UP_NORMAL + ); + } + + private static void putTerrainTriangle( + GpuIntBuffer vb, + GpuIntBuffer fb, + 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, + int[] normal + ) { + int faceIdx = fb.putFace( + colorA, colorB, colorC, + packedMaterialA, packedMaterialB, packedMaterialC, + terrainDataA, terrainDataB, terrainDataC + ); + vb.putVertex(x0, y0, z0, u0, v0, 0, normal[0], normal[2], normal[1], faceIdx); + vb.putVertex(x1, y1, z1, u1, v1, 0, normal[0], normal[2], normal[1], faceIdx); + vb.putVertex(x2, y2, z2, u2, v2, 0, normal[0], normal[2], normal[1], faceIdx); + } +} diff --git a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java index 2e394138de..16a7f415b0 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.extendWaterArea == null) { var base = ctx.sceneContext.sceneBase; assert base != null; boolean inArea = ctx.sceneContext.currentArea.intersects( diff --git a/src/main/java/rs117/hd/scene/ExtendWaterSample.java b/src/main/java/rs117/hd/scene/ExtendWaterSample.java new file mode 100644 index 0000000000..78df2206c9 --- /dev/null +++ b/src/main/java/rs117/hd/scene/ExtendWaterSample.java @@ -0,0 +1,33 @@ +package rs117.hd.scene; + +import java.util.HashMap; +import rs117.hd.scene.water_types.WaterType; + +public class ExtendWaterSample { + public final WaterType waterType; + public final int plane; + public final int flatHeight; + public final int flatUnderwaterHeight; + public final HashMap boundarySurfaceHeights; + public final HashMap boundaryUnderwaterHeights; + + public ExtendWaterSample( + WaterType waterType, + int plane, + int flatHeight, + int flatUnderwaterHeight, + HashMap boundarySurfaceHeights, + HashMap boundaryUnderwaterHeights + ) { + this.waterType = waterType; + this.plane = plane; + this.flatHeight = flatHeight; + this.flatUnderwaterHeight = flatUnderwaterHeight; + this.boundarySurfaceHeights = boundarySurfaceHeights; + this.boundaryUnderwaterHeights = boundaryUnderwaterHeights; + } + + public static long packWorldVertex(int worldX, int worldY) { + return ((long) worldX << 32) | (worldY & 0xffffffffL); + } +} diff --git a/src/main/java/rs117/hd/scene/ExtendWaterTileUtil.java b/src/main/java/rs117/hd/scene/ExtendWaterTileUtil.java new file mode 100644 index 0000000000..f8bb2ea179 --- /dev/null +++ b/src/main/java/rs117/hd/scene/ExtendWaterTileUtil.java @@ -0,0 +1,134 @@ +package rs117.hd.scene; + +import javax.annotation.Nullable; +import net.runelite.api.SceneTileModel; +import net.runelite.api.SceneTilePaint; +import net.runelite.api.Tile; +import rs117.hd.scene.areas.Area; + +import static net.runelite.api.Constants.CHUNK_SIZE; +import static net.runelite.api.Constants.SCENE_SIZE; +import static rs117.hd.utils.HDUtils.HIDDEN_HSL; + +public final class ExtendWaterTileUtil { + private ExtendWaterTileUtil() {} + + @Nullable + public static Area getExtendWaterArea(SceneContext ctx) { + if (!ctx.enableExtendWater) + return null; + if (ctx.extendWaterArea != null) + return ctx.extendWaterArea; + if (ctx.currentArea != null && ctx.currentArea.extendWater) + return ctx.currentArea; + return null; + } + + public static boolean isEnabled(SceneContext ctx) { + if (!ctx.enableExtendWater) + return false; + + Area area = getExtendWaterArea(ctx); + return ctx.sceneBase != null && + area != null && + area.extendWater && + area.extendWaterReferenceTile != null && + area.extendWaterReferenceTile.length >= 2; + } + + public static int getExtendWaterPlane(SceneContext ctx) { + Area area = getExtendWaterArea(ctx); + if (area == null || area.extendWaterReferenceTile == null) + return 0; + int[] ref = area.extendWaterReferenceTile; + return ref.length > 2 ? ref[2] : 0; + } + + public static boolean isWithinExtendWaterBounds(SceneContext ctx, int tileExX, int tileExY, int plane) { + Area area = getExtendWaterArea(ctx); + if (area == null) + return false; + int[] worldPos = ctx.extendedSceneToWorld(tileExX, tileExY, plane); + return area.containsPoint(false, worldPos[0], worldPos[1], worldPos[2]); + } + + /** + * The loaded map region including extended map loading chunks. Horizon tiles must stay + * within this region — the extended scene buffer is larger, but only this area has map data. + */ + public static boolean isWithinHorizonRange(SceneContext ctx, int tileExX, int tileExY) { + if (ctx.sceneBase == null) + return false; + + int tileX = tileExX - ctx.sceneOffset; + int tileY = tileExY - ctx.sceneOffset; + int sceneMin = -ctx.expandedMapLoadingChunks * CHUNK_SIZE; + int sceneMax = SCENE_SIZE + ctx.expandedMapLoadingChunks * CHUNK_SIZE; + return tileX >= sceneMin && tileY >= sceneMin && tileX <= sceneMax && tileY <= sceneMax; + } + + public 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 computeShouldExtendWaterAtTile(SceneContext ctx, int tileExX, int tileExY, int plane) { + if (!isWithinHorizonRange(ctx, tileExX, tileExY)) + return false; + return !isWithinExtendWaterBounds(ctx, tileExX, tileExY, plane); + } + + public static void buildHorizonTileMask(SceneContext ctx) { + if (!isEnabled(ctx)) { + ctx.horizonTileMask = null; + return; + } + + int plane = getExtendWaterPlane(ctx); + boolean[][] mask = new boolean[ctx.sizeX][ctx.sizeZ]; + for (int x = 0; x < ctx.sizeX; ++x) { + for (int y = 0; y < ctx.sizeZ; ++y) { + mask[x][y] = computeShouldExtendWaterAtTile(ctx, x, y, plane); + } + } + ctx.horizonTileMask = mask; + } + + /** + * Horizon tiles fill empty space outside the area AABB up to the edge of the loaded map. + */ + public static boolean shouldExtendWaterAtTile(SceneContext ctx, int tileExX, int tileExY, int plane) { + if (!isEnabled(ctx) || plane != getExtendWaterPlane(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]; + + return computeShouldExtendWaterAtTile(ctx, tileExX, tileExY, plane); + } +} diff --git a/src/main/java/rs117/hd/scene/ProceduralGenerator.java b/src/main/java/rs117/hd/scene/ProceduralGenerator.java index 2a3969d99e..451a26b1ef 100644 --- a/src/main/java/rs117/hd/scene/ProceduralGenerator.java +++ b/src/main/java/rs117/hd/scene/ProceduralGenerator.java @@ -27,10 +27,12 @@ import java.util.Arrays; import java.util.HashMap; import javax.inject.Inject; +import javax.inject.Provider; import javax.inject.Singleton; import lombok.extern.slf4j.Slf4j; import net.runelite.api.*; import rs117.hd.renderer.legacy.LegacySceneContext; +import rs117.hd.renderer.zone.WaterExtender; import rs117.hd.scene.materials.Material; import rs117.hd.scene.model_overrides.ModelOverride; import rs117.hd.scene.model_overrides.TzHaarRecolorType; @@ -76,6 +78,9 @@ public class ProceduralGenerator { @Inject private WaterTypeManager waterTypeManager; + @Inject + private Provider waterExtender; + public void generateSceneData(SceneContext sceneContext) { long timerTotal = System.currentTimeMillis(); @@ -107,6 +112,8 @@ public void clearSceneData(SceneContext sceneContext) { sceneContext.vertexUnderwaterDepth = null; if (!(sceneContext instanceof LegacySceneContext)) sceneContext.underwaterDepthLevels = null; + sceneContext.horizonTileMask = null; + sceneContext.extendWaterSample = null; } /** @@ -534,6 +541,8 @@ else if (tile.getSceneTileModel() != null) } } + waterExtender.get().prepareSceneTerrain(sceneContext, tiles); + // Sink terrain further from shore by desired levels. for (int level = 0; level < DEPTH_LEVEL_SLOPE.length - 1; level++) { @@ -551,9 +560,13 @@ else if (tile.getSceneTileModel() != null) // If it's on the edge of the scene, reset the depth so // 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) { - sceneContext.underwaterDepthLevels[z][x][y] = 0; - continue; + if (x == 0 || y == 0 || x == sizeX || y == sizeY) { + if (!(ExtendWaterTileUtil.isEnabled(sceneContext) && + ExtendWaterTileUtil.shouldExtendWaterAtTile(sceneContext, x, y, z))) + { + sceneContext.underwaterDepthLevels[z][x][y] = 0; + continue; + } } int tileHeight = sceneContext.underwaterDepthLevels[z][x][y]; diff --git a/src/main/java/rs117/hd/scene/SceneContext.java b/src/main/java/rs117/hd/scene/SceneContext.java index 85c424d02a..d5d9f4ca81 100644 --- a/src/main/java/rs117/hd/scene/SceneContext.java +++ b/src/main/java/rs117/hd/scene/SceneContext.java @@ -34,11 +34,19 @@ public class SceneContext { public boolean enableAreaHiding; public boolean fillGaps; + public boolean enableExtendWater; + + @Nullable + public ExtendWaterSample extendWaterSample; + @Nullable + public boolean[][] horizonTileMask; public boolean isInChambersOfXeric; public boolean isInHouse; @Nullable public Area currentArea; + @Nullable + public Area extendWaterArea; public Area[] possibleAreas = new Area[0]; public final ArrayList environments = new ArrayList<>(); public byte[][] filledTiles = new byte[EXTENDED_SCENE_SIZE][EXTENDED_SCENE_SIZE]; diff --git a/src/main/java/rs117/hd/scene/areas/Area.java b/src/main/java/rs117/hd/scene/areas/Area.java index 5e32d81286..1ba0629834 100644 --- a/src/main/java/rs117/hd/scene/areas/Area.java +++ b/src/main/java/rs117/hd/scene/areas/Area.java @@ -14,6 +14,8 @@ public class Area { public String name; public boolean hideOtherAreas; public boolean fillGaps = true; + public boolean extendWater; + public int[] extendWaterReferenceTile; public String[] areas; public int[] regions; diff --git a/src/main/resources/rs117/hd/scene/areas.json b/src/main/resources/rs117/hd/scene/areas.json index ab0b40f433..cc24ac3618 100644 --- a/src/main/resources/rs117/hd/scene/areas.json +++ b/src/main/resources/rs117/hd/scene/areas.json @@ -8494,9 +8494,11 @@ { "name": "WATER_ALTAR", "aabbs": [ - [ 2688, 4800, 2750, 4862 ] + [ 2689, 4859, 2748, 4802 ] ], - "hideOtherAreas": true + "hideOtherAreas": true, + "extendWater": true, + "extendWaterReferenceTile": [ 2709, 4804 ] }, { "name": "MIND_ALTAR", @@ -8518,7 +8520,9 @@ "aabbs": [ [ 3202, 4800, 3263, 4863 ] ], - "hideOtherAreas": true + "hideOtherAreas": true, + "extendWater": true, + "extendWaterReferenceTile": [ 3216, 3437 ] }, { "name": "TRUE_BLOOD_ALTAR_CRYSTAL_POOL", From df1b51b8db33bc1ec4c8fe5ce965377e73f70571 Mon Sep 17 00:00:00 2001 From: Mark7625 <72366279+Mark7625@users.noreply.github.com> Date: Sun, 31 May 2026 04:30:52 +0100 Subject: [PATCH 2/4] Add Terrain Support --- schemas/areas.schema.json | 12 +- src/main/java/rs117/hd/HdPluginConfig.java | 2 +- .../rs117/hd/renderer/zone/GapFiller.java | 4 +- .../hd/renderer/zone/HorizonExtender.java | 1990 +++++++++++++++++ .../rs117/hd/renderer/zone/SceneManager.java | 21 +- .../rs117/hd/renderer/zone/SceneUploader.java | 9 +- .../rs117/hd/renderer/zone/WaterExtender.java | 905 -------- .../renderer/zone/WaterHorizonExtender.java | 0 .../rs117/hd/renderer/zone/ZoneRenderer.java | 2 +- .../rs117/hd/scene/ExtendWaterSample.java | 33 - .../rs117/hd/scene/ExtendWaterTileUtil.java | 134 -- .../rs117/hd/scene/ProceduralGenerator.java | 13 +- .../java/rs117/hd/scene/SceneContext.java | 11 +- src/main/java/rs117/hd/scene/areas/Area.java | 11 +- src/main/resources/rs117/hd/scene/areas.json | 10 +- 15 files changed, 2047 insertions(+), 1110 deletions(-) create mode 100644 src/main/java/rs117/hd/renderer/zone/HorizonExtender.java delete mode 100644 src/main/java/rs117/hd/renderer/zone/WaterExtender.java create mode 100644 src/main/java/rs117/hd/renderer/zone/WaterHorizonExtender.java delete mode 100644 src/main/java/rs117/hd/scene/ExtendWaterSample.java delete mode 100644 src/main/java/rs117/hd/scene/ExtendWaterTileUtil.java diff --git a/schemas/areas.schema.json b/schemas/areas.schema.json index b041482d25..7321526900 100644 --- a/schemas/areas.schema.json +++ b/schemas/areas.schema.json @@ -24,19 +24,19 @@ "type": "boolean", "description": "Whether to fill gaps in the ground. Can only be disabled if hideOtherAreas is true." }, - "extendWater": { - "type": "boolean", - "description": "Whether to fill empty scene tiles outside this area's AABBs with water matching extendWaterReferenceTile." - }, - "extendWaterReferenceTile": { + "horizonTileReference": { "type": "array", - "description": "World tile coordinates [x, y] or [x, y, plane] used to sample water type, height and material.", + "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/HdPluginConfig.java b/src/main/java/rs117/hd/HdPluginConfig.java index c568cd0d54..e3d9c961d7 100644 --- a/src/main/java/rs117/hd/HdPluginConfig.java +++ b/src/main/java/rs117/hd/HdPluginConfig.java @@ -143,7 +143,7 @@ default boolean hideUnrelatedAreas() { keyName = KEY_HORIZON_TILES, name = "Horizon tiles", description = - "Expands terrain at the edge of the loaded map outside hidden areas so scenery is not cut off abruptly.
" + + "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 diff --git a/src/main/java/rs117/hd/renderer/zone/GapFiller.java b/src/main/java/rs117/hd/renderer/zone/GapFiller.java index 0d9e5f2eed..4f8821bfb7 100644 --- a/src/main/java/rs117/hd/renderer/zone/GapFiller.java +++ b/src/main/java/rs117/hd/renderer/zone/GapFiller.java @@ -24,7 +24,7 @@ public class GapFiller { private MaterialManager materialManager; public void estimateForZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) { - if (ctx.sceneBase == null || ctx.currentArea != null && (!ctx.currentArea.fillGaps || ctx.currentArea.extendWater)) + if (ctx.sceneBase == null || ctx.currentArea != null && (!ctx.currentArea.fillGaps || ctx.currentArea.hasHorizonTiles())) return; int sceneMin = -ctx.expandedMapLoadingChunks * CHUNK_SIZE; @@ -53,7 +53,7 @@ public void uploadForZone( GpuIntBuffer vb, GpuIntBuffer fb ) { - if (ctx.sceneBase == null || ctx.currentArea != null && (!ctx.currentArea.fillGaps || ctx.currentArea.extendWater)) + if (ctx.sceneBase == null || ctx.currentArea != null && (!ctx.currentArea.fillGaps || ctx.currentArea.hasHorizonTiles())) return; int basex = (mzx - (ctx.sceneOffset >> 3)) << 10; 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..380631c4c3 --- /dev/null +++ b/src/main/java/rs117/hd/renderer/zone/HorizonExtender.java @@ -0,0 +1,1990 @@ +package rs117.hd.renderer.zone; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; +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.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.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[] UP_NORMAL = { 0, -1, 0 }; + private static final int WATER_COLOR = 127; + + @Inject + private HdPlugin plugin; + + @Inject + private TileOverrideManager tileOverrideManager; + + @Inject + private MaterialManager materialManager; + + @Inject + private ProceduralGenerator proceduralGenerator; + + 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 isWaterHorizon() { + return boundaryUnderwaterHeights != null; + } + + 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) + return false; + Area area = getArea(ctx); + return ctx.sceneBase != null && area != null && area.hasHorizonTiles(); + } + + public static boolean shouldPatchWaterAtTile(SceneContext ctx, int tileExX, int tileExY, int plane) { + Area area = getArea(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 isWithinLoadedRange(ctx, tileExX, tileExY) && + isOutsideAreaBounds(ctx, tileExX, tileExY, plane) && + isEligiblePatchTile(tile); + } + + public void logSceneLoadTiming(SceneContext ctx) { + if (!ctx.enableHorizonTiles) + return; + log.debug("horizon tile time: {} ms", TimeUnit.NANOSECONDS.toMillis(ctx.horizonTileNs)); + } + + private static boolean isFlatTerrain(SceneContext ctx) { + Area area = getArea(ctx); + return area != null && area.horizonFlatTerrain; + } + + private static void addTiming(SceneContext ctx, long start) { + ctx.horizonTileNs += System.nanoTime() - start; + } + + @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; + } + + private static int getPlane(SceneContext ctx) { + Area area = getArea(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 isWithinLoadedRange(SceneContext ctx, int tileExX, int tileExY) { + if (ctx.sceneBase == null) + return true; + int tileX = tileExX - ctx.sceneOffset; + int tileY = tileExY - ctx.sceneOffset; + int min = loadedSceneMin(ctx); + int max = loadedSceneMax(ctx); + return tileX > min && tileY > min && tileX < max && tileY < max; + } + + 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 isOutsideAreaBounds(SceneContext ctx, int tileExX, int tileExY, int plane) { + Area area = getArea(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] = isWithinLoadedRange(ctx, x, y) && + isOutsideAreaBounds(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 isOutsideAreaBounds(ctx, tileExX, tileExY, plane); + } + + private static int[] clipLocal( + ZoneSceneContext ctx, + Area area, + int mzx, + int mzz, + int plane, + int minLocalX, + int minLocalZ, + int maxLocalX, + int maxLocalZ + ) { + if (ctx.sceneBase == null) + return null; + + 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 null; + + return new int[] { (int) clipMinX, (int) clipMinZ, (int) clipMaxX, (int) clipMaxZ }; + } + + 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 putWaterSurface( + GpuIntBuffer vb, + GpuIntBuffer fb, + WaterType waterType, + int plane, + int minX, + int minZ, + int maxX, + int maxZ, + int height + ) { + int terrainData = HDUtils.packTerrainData(true, 0, waterType, plane); + int packedMaterial = Material.NONE.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); + + putTriangle( + vb, fb, + WATER_COLOR, WATER_COLOR, WATER_COLOR, + packedMaterial, packedMaterial, packedMaterial, + terrainData, terrainData, terrainData, + maxX, height, maxZ, 0, 0, + minX, height, maxZ, 0, 0, + maxX, height, minZ, 0, 0 + ); + putTriangle( + vb, fb, + WATER_COLOR, WATER_COLOR, WATER_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 putUnderwaterFlat( + GpuIntBuffer vb, + GpuIntBuffer fb, + WaterType waterType, + int plane, + int minX, + int minZ, + int maxX, + int maxZ, + int height, + int depth + ) { + int terrainData = HDUtils.packTerrainData(true, depth, waterType, plane); + int packedMaterial = Material.NONE.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); + + putTriangle( + vb, fb, + UNDERWATER_HSL, UNDERWATER_HSL, UNDERWATER_HSL, + packedMaterial, packedMaterial, packedMaterial, + terrainData, terrainData, terrainData, + maxX, height, maxZ, 0, 0, + minX, height, maxZ, 0, 0, + maxX, height, minZ, 0, 0 + ); + putTriangle( + vb, fb, + UNDERWATER_HSL, UNDERWATER_HSL, UNDERWATER_HSL, + 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( + GpuIntBuffer vb, + GpuIntBuffer fb, + 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 + ) { + int faceIdx = fb.putFace( + colorA, colorB, colorC, + packedMaterialA, packedMaterialB, packedMaterialC, + terrainDataA, terrainDataB, terrainDataC + ); + vb.putVertex(x0, y0, z0, u0, v0, 0, UP_NORMAL[0], UP_NORMAL[2], UP_NORMAL[1], faceIdx); + vb.putVertex(x1, y1, z1, u1, v1, 0, UP_NORMAL[0], UP_NORMAL[2], UP_NORMAL[1], faceIdx); + vb.putVertex(x2, y2, z2, u2, v2, 0, UP_NORMAL[0], UP_NORMAL[2], UP_NORMAL[1], faceIdx); + } + + 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 e) { + if (e == null || !e.any()) + return 0; + + int quads = 0; + if (e.west) + quads++; + if (e.east) + quads++; + if (e.north) + quads++; + if (e.south) + quads++; + if (e.west && e.north) + quads++; + if (e.east && e.north) + quads++; + if (e.west && e.south) + quads++; + if (e.east && e.south) + quads++; + return quads; + } + + private static int countShellStripTiles( + ZoneSceneContext ctx, + Area area, + int mzx, + int mzz, + int plane, + int minLocalX, + int minLocalZ, + int maxLocalX, + int maxLocalZ + ) { + int[] bounds = clipLocal(ctx, area, mzx, mzz, plane, minLocalX, minLocalZ, maxLocalX, maxLocalZ); + if (bounds == null) + return 0; + return countTiles(bounds[0], bounds[1], bounds[2], bounds[3]); + } + + private ZonePlan planZone(ZoneSceneContext ctx, int mzx, int mzz) { + ZonePlan plan = new ZonePlan(); + 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; + if (shouldExtendTile(ctx, tileExX, tileExY) > 0) { + 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; + } + + private static int baseShouldExtend(ZoneSceneContext ctx, Sample sample, int tileExX, int tileExY) { + if (!shouldRenderAtTile(ctx, tileExX, tileExY, sample.plane)) + return 0; + if (tileExX < 0 || tileExY < 0 || tileExX >= EXTENDED_SCENE_SIZE || tileExY >= EXTENDED_SCENE_SIZE) + return 2; + + Tile tile = ctx.scene.getExtendedTiles()[sample.plane][tileExX][tileExY]; + if (tile == null || isHiddenGroundTile(tile)) + return 2; + return 2; + } + + public void prepareSceneTerrain(SceneContext ctx, Tile[][][] tiles) { + Area area = getArea(ctx); + if (isFlatTerrain(ctx)) + return; + if (!isEnabled(ctx)) + return; + + long start = System.nanoTime(); + try { + buildPatchMask(ctx, tiles); + boolean[][] mask = ctx.horizonTileMask; + if (mask == 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.tileIsWater[z][x][y]) + continue; + + ctx.tileIsWater[z][x][y] = true; + 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.tileIsWater[z][nx][ny]) + continue; + + Tile neighbor = tiles[z][nx][ny]; + if (neighbor == null) + continue; + + clearLandFlagsOnWaterFaces(ctx, neighbor, nx, ny, z); + } + } + } + } + } finally { + addTiming(ctx, start); + } + } + + 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.vertexIsLand.remove(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.vertexIsLand.remove(key); + } + } + + public void estimateForZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) { + if (!isEnabled(ctx)) + return; + if (isFlatTerrain(ctx)) + estimateTerrainForZone(ctx, zone, mzx, mzz); + else + estimateWaterForZone(ctx, zone, mzx, mzz); + } + + private void estimateWaterForZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) { + if (!isEnabled(ctx)) + return; + + long start = System.nanoTime(); + try { + if (resolveSample(ctx) == null) + return; + + ZonePlan plan = planZone(ctx, mzx, mzz); + int shellQuads = countShellQuads(shellEdges(ctx, mzx, mzz)); + if (plan.tileCount == 0 && shellQuads == 0) + return; + + zone.hasWater = true; + int facesPerLayer = plan.tileCount * 2 + shellQuads * 2; + zone.sizeO += facesPerLayer * 2; + zone.sizeF += facesPerLayer * 2; + } finally { + addTiming(ctx, start); + } + } + + public void uploadUnderwaterForZone( + ZoneSceneContext ctx, + Zone zone, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + if (!isEnabled(ctx) || isFlatTerrain(ctx)) + return; + uploadWaterForZone(ctx, zone, mzx, mzz, vb, fb, true); + } + + public void uploadSurfaceForZone( + ZoneSceneContext ctx, + Zone zone, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + if (!isEnabled(ctx)) + return; + if (isFlatTerrain(ctx)) + uploadTerrainForZone(ctx, zone, mzx, mzz, vb, fb); + else + uploadWaterForZone(ctx, zone, mzx, mzz, vb, fb, false); + } + + private void uploadWaterForZone( + ZoneSceneContext ctx, + Zone zone, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb, + boolean underwater + ) { + if (!isEnabled(ctx)) + return; + + long start = System.nanoTime(); + try { + Sample sample = resolveSample(ctx); + if (sample == null) + return; + + Area area = getArea(ctx); + assert area != null; + + int posBefore = vb.position(); + ZonePlan plan = planZone(ctx, mzx, mzz); + if (plan.tileCount > 0) { + int[][] cornerHeights = buildZoneCornerHeights(ctx, area, sample, mzx, mzz, underwater); + if (plan.fullZone && isUniformHeightGrid(cornerHeights)) { + uploadZoneQuad(ctx, sample, cornerHeights, mzx, mzz, underwater, vb, fb); + } else { + uploadZoneGrid(ctx, sample, plan, cornerHeights, mzx, mzz, underwater, vb, fb); + } + } + + uploadWaterShell(ctx, area, sample, mzx, mzz, underwater, vb, fb); + + if (vb.position() > posBefore) + zone.hasWater = true; + } finally { + addTiming(ctx, start); + } + } + + private void uploadWaterShell( + ZoneSceneContext ctx, + Area area, + Sample sample, + int mzx, + int mzz, + boolean underwater, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + ShellEdges shell = shellEdges(ctx, mzx, mzz); + if (shell == null) + return; + + int x = shell.extra; + int z = shell.extent; + if (shell.west) + uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, -x, 0, 0, z); + if (shell.east) + uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, z, 0, z + x, z); + if (shell.north) + uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, 0, -x, z, 0); + if (shell.south) + uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, 0, z, z, z + x); + if (shell.west && shell.north) + uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, -x, -x, 0, 0); + if (shell.east && shell.north) + uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, z, -x, z + x, 0); + if (shell.west && shell.south) + uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, -x, z, 0, z + x); + if (shell.east && shell.south) + uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, z, z, z + x, z + x); + } + + private void uploadWaterShellStrip( + ZoneSceneContext ctx, + Area area, + Sample sample, + int mzx, + int mzz, + boolean underwater, + GpuIntBuffer vb, + GpuIntBuffer fb, + int minLocalX, + int minLocalZ, + int maxLocalX, + int maxLocalZ + ) { + int[] bounds = clipLocal(ctx, area, mzx, mzz, sample.plane, minLocalX, minLocalZ, maxLocalX, maxLocalZ); + if (bounds == null) + return; + + if (underwater) { + int depth = max(1, sample.flatUnderwaterHeight - sample.flatHeight); + putUnderwaterFlat( + vb, fb, sample.waterType, sample.plane, + bounds[0], bounds[1], bounds[2], bounds[3], + sample.flatUnderwaterHeight, depth + ); + } else { + putWaterSurface( + vb, fb, sample.waterType, sample.plane, + bounds[0], bounds[1], bounds[2], bounds[3], sample.flatHeight + ); + } + } + + private int[][] buildZoneCornerHeights( + ZoneSceneContext ctx, + Area area, + Sample sample, + int mzx, + int mzz, + boolean underwater + ) { + int[][] heights = new int[CHUNK_SIZE + 1][CHUNK_SIZE + 1]; + 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]); + } + } + return heights; + } + + private void uploadZoneQuad( + ZoneSceneContext ctx, + Sample sample, + int[][] cornerHeights, + int mzx, + int mzz, + boolean underwater, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + 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(ctx, sample, vb, fb, 0, 0, extent, extent, sw, se, nw, ne, mzx, mzz); + } else { + putWaterSurface(vb, fb, sample.waterType, sample.plane, 0, 0, extent, extent, sw); + } + } + + private void uploadZoneGrid( + ZoneSceneContext ctx, + Sample sample, + ZonePlan plan, + int[][] cornerHeights, + int mzx, + int mzz, + boolean underwater, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + 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; + + if (underwater) { + uploadUnderwaterTile(ctx, sample, vb, fb, baseX, baseZ, baseX + LOCAL_TILE_SIZE, baseZ + LOCAL_TILE_SIZE, sw, se, nw, ne, mzx, mzz); + } else { + putWaterSurface( + vb, fb, sample.waterType, sample.plane, + baseX, baseZ, baseX + LOCAL_TILE_SIZE, baseZ + LOCAL_TILE_SIZE, sw + ); + } + } + } + } + + private void uploadUnderwaterTile( + ZoneSceneContext ctx, + Sample sample, + GpuIntBuffer vb, + GpuIntBuffer fb, + 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( + vb, fb, + 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( + vb, fb, + 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 = getArea(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; + if (isFlatTerrain(ctx)) + return resolveTerrainSample(ctx); + return resolveWaterSample(ctx); + } + + @Nullable + private Sample resolveWaterSample(ZoneSceneContext ctx) { + long start = System.nanoTime(); + try { + Area area = getArea(ctx); + assert area != null; + + Reference ref = resolveReference(ctx, area); + WaterType waterType = ref.waterType; + if (waterType == WaterType.NONE) + waterType = WaterType.WATER; + + HashMap boundarySurfaceHeights = new HashMap<>(); + HashMap boundaryUnderwaterHeights = new HashMap<>(); + buildBoundaryHeightMaps(ctx, area, ref.plane, ref.flatHeight, boundarySurfaceHeights, boundaryUnderwaterHeights); + + 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; + } finally { + addTiming(ctx, start); + } + } + + @Nullable + private Sample resolveTerrainSample(ZoneSceneContext ctx) { + long start = System.nanoTime(); + try { + Area area = getArea(ctx); + assert area != null; + + Reference ref = resolveReference(ctx, area); + HashMap boundarySurfaceHeights = new HashMap<>(); + buildBoundarySurfaceHeights(ctx, area, ref.plane, ref.flatHeight, boundarySurfaceHeights); + + 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 + ); + return ctx.horizonTileSample; + } finally { + addTiming(ctx, start); + } + } + + 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, + HashMap underwaterHeights + ) { + long boundaryKey = Sample.packWorldVertex(boundaryX, boundaryY); + if (underwaterHeights.containsKey(boundaryKey)) + 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.vertexUnderwaterDepth == 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; + + int[][] cornerTiles = { + { 0, 0, 0 }, + { -1, 0, 1 }, + { 0, -1, 2 }, + { -1, -1, 3 }, + }; + + Tile[][][] tiles = ctx.scene.getExtendedTiles(); + for (int[] corner : cornerTiles) { + 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); + Integer depth = ctx.vertexUnderwaterDepth.get(keys[vertexIndex]); + if (depth != null && 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.vertexUnderwaterDepth != null) { + int height = cornerSurfaceHeightAtWorldVertex(ctx, plane, worldX, worldY, 0); + int key = HDUtils.fastVertexHash(new int[] { + sceneX * LOCAL_TILE_SIZE, + sceneY * LOCAL_TILE_SIZE, + height + }); + Integer depth = ctx.vertexUnderwaterDepth.get(key); + if (depth != null && depth > 0) + return depth; + } + + return 0; + } + + private int shouldExtendTile(ZoneSceneContext ctx, int tileExX, int tileExY) { + Sample sample = resolveSample(ctx); + if (sample == null) + return 0; + if (isFlatTerrain(ctx)) + return baseShouldExtend(ctx, sample, tileExX, tileExY); + int tileZ = sample.plane; + if (!shouldRenderAtTile(ctx, tileExX, tileExY, tileZ)) + return 0; + + if (tileExX < 0 || tileExY < 0 || tileExX >= EXTENDED_SCENE_SIZE || tileExY >= EXTENDED_SCENE_SIZE) + return 2; + + if (ctx.tileIsWater != null && + ctx.tileIsWater[tileZ][tileExX][tileExY] && + (ctx.filledTiles[tileExX][tileExY] & (1 << tileZ)) != 0) + return 0; + + Tile tile = ctx.scene.getExtendedTiles()[tileZ][tileExX][tileExY]; + if (tile == null || isHiddenGroundTile(tile)) + return 2; + + if ((ctx.filledTiles[tileExX][tileExY] & (1 << tileZ)) != 0 && hasVisibleLand(ctx, tile, tileExX, tileExY, tileZ)) + return 0; + + return 2; + } + + private boolean hasVisibleLand(SceneContext ctx, Tile tile, int tileExX, int tileExY, int tileZ) { + if (tile.getBridge() != null) + tile = tile.getBridge(); + + SceneTilePaint paint = tile.getSceneTilePaint(); + if (paint != null && paint.getNeColor() != HIDDEN_HSL) { + int[] worldPos = ctx.extendedSceneToWorld(tileExX, tileExY, tileZ); + var override = tileOverrideManager.getOverride(ctx, tile, worldPos); + if (proceduralGenerator.seasonalWaterType(override, paint.getTexture()) != WaterType.NONE) + return false; + return true; + } + + SceneTileModel model = tile.getSceneTileModel(); + if (model == null) + return false; + + int[] worldPos = ctx.extendedSceneToWorld(tileExX, tileExY, tileZ); + int overlayId = OVERLAY_FLAG | ctx.scene.getOverlayIds()[tileZ][tileExX][tileExY]; + int underlayId = ctx.scene.getUnderlayIds()[tileZ][tileExX][tileExY]; + 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; + + return true; + } + return false; + } + + private void estimateTerrainForZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) { + long start = System.nanoTime(); + try { + Sample sample = resolveSample(ctx); + if (sample == null) + return; + + Area area = getArea(ctx); + assert area != null; + + ZonePlan plan = planZone(ctx, mzx, mzz); + ShellEdges shell = shellEdges(ctx, mzx, mzz); + int shellFaces = countTerrainShellFaces(ctx, area, sample, mzx, mzz, shell); + if (plan.tileCount == 0 && shellFaces == 0) + return; + + int faces = plan.tileCount * 2 + shellFaces; + zone.sizeO += faces; + zone.sizeF += faces; + } finally { + addTiming(ctx, start); + } + } + + private int countTerrainShellFaces( + ZoneSceneContext ctx, + Area area, + Sample sample, + int mzx, + int mzz, + ShellEdges shell + ) { + if (shell == null) + return 0; + + int faces = 0; + int x = shell.extra; + int z = shell.extent; + if (shell.west) + faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, -x, 0, 0, z) * 2; + if (shell.east) + faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, z, 0, z + x, z) * 2; + if (shell.north) + faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, 0, -x, z, 0) * 2; + if (shell.south) + faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, 0, z, z, z + x) * 2; + if (shell.west && shell.north) + faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, -x, -x, 0, 0) * 2; + if (shell.east && shell.north) + faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, z, -x, z + x, 0) * 2; + if (shell.west && shell.south) + faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, -x, z, 0, z + x) * 2; + if (shell.east && shell.south) + faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, z, z, z + x, z + x) * 2; + return faces; + } + + private void uploadTerrainForZone( + ZoneSceneContext ctx, + Zone zone, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + long start = System.nanoTime(); + try { + Sample sample = resolveSample(ctx); + if (sample == null) + return; + + Area area = getArea(ctx); + assert area != null; + + int posBefore = vb.position(); + ZonePlan plan = planZone(ctx, mzx, mzz); + if (plan.tileCount > 0) { + int[][] cornerHeights = buildTerrainZoneCornerHeights(ctx, area, sample, mzx, mzz); + uploadTerrainZoneGrid(ctx, sample, plan, cornerHeights, mzx, mzz, vb, fb); + } + + uploadTerrainShell(ctx, area, sample, mzx, mzz, vb, fb); + + if (vb.position() > posBefore) + zone.hasWater = true; + } finally { + addTiming(ctx, start); + } + } + + private int[][] buildTerrainZoneCornerHeights( + ZoneSceneContext ctx, + Area area, + Sample sample, + int mzx, + int mzz + ) { + int[][] heights = new int[CHUNK_SIZE + 1][CHUNK_SIZE + 1]; + 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] = sample.flatHeight; + } else { + heights[vx][vz] = resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0], worldPos[1]); + } + } + } + return heights; + } + + private void uploadTerrainZoneGrid( + ZoneSceneContext ctx, + Sample sample, + ZonePlan plan, + int[][] cornerHeights, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + 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(ctx, sample, vb, fb, baseX, baseZ, sw, se, nw, ne, swWorld, seWorld, nwWorld, neWorld); + } + } + } + + private void uploadTerrainShell( + ZoneSceneContext ctx, + Area area, + Sample sample, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + ShellEdges shell = shellEdges(ctx, mzx, mzz); + if (shell == null) + return; + + int x = shell.extra; + int z = shell.extent; + if (shell.west) + uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, -x, 0, 0, z); + if (shell.east) + uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, z, 0, z + x, z); + if (shell.north) + uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, 0, -x, z, 0); + if (shell.south) + uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, 0, z, z, z + x); + if (shell.west && shell.north) + uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, -x, -x, 0, 0); + if (shell.east && shell.north) + uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, z, -x, z + x, 0); + if (shell.west && shell.south) + uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, -x, z, 0, z + x); + if (shell.east && shell.south) + uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, z, z, z + x, z + x); + } + + private void uploadTerrainShellStrip( + ZoneSceneContext ctx, + Area area, + Sample sample, + int mzx, + int mzz, + GpuIntBuffer vb, + GpuIntBuffer fb, + int minLocalX, + int minLocalZ, + int maxLocalX, + int maxLocalZ + ) { + int[] bounds = clipLocal(ctx, area, mzx, mzz, sample.plane, minLocalX, minLocalZ, maxLocalX, maxLocalZ); + if (bounds == null) + return; + uploadFlatTilesInBounds(ctx, sample, mzx, mzz, bounds[0], bounds[1], bounds[2], bounds[3], sample.flatHeight, vb, fb); + } + + private void uploadFlatTilesInBounds( + ZoneSceneContext ctx, + Sample sample, + int mzx, + int mzz, + int minX, + int minZ, + int maxX, + int maxZ, + int height, + GpuIntBuffer vb, + GpuIntBuffer fb + ) { + 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(ctx, sample, vb, fb, 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( + ZoneSceneContext ctx, + Sample sample, + GpuIntBuffer vb, + GpuIntBuffer fb, + int baseX, + int baseZ, + int sw, + int se, + int nw, + int ne, + int[] swWorld, + int[] seWorld, + int[] nwWorld, + int[] neWorld + ) { + if (sample.isWaterSurface()) { + putWaterSurface( + vb, fb, sample.waterType, sample.plane, + baseX, baseZ, baseX + LOCAL_TILE_SIZE, baseZ + LOCAL_TILE_SIZE, sw + ); + 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( + vb, fb, + 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( + vb, fb, + 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 + ); + } + + private static void buildBoundarySurfaceHeights( + ZoneSceneContext ctx, + Area area, + int plane, + int flatHeight, + HashMap surfaceHeights + ) { + 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++) { + sampleBoundaryVertex(ctx, plane, x, aabb.minY, flatHeight, surfaceHeights); + sampleBoundaryVertex(ctx, plane, x, aabb.maxY + 1, flatHeight, surfaceHeights); + } + for (int y = aabb.minY + 1; y <= aabb.maxY; y++) { + sampleBoundaryVertex(ctx, plane, aabb.minX, y, flatHeight, surfaceHeights); + sampleBoundaryVertex(ctx, plane, aabb.maxX + 1, y, flatHeight, surfaceHeights); + } + } + } + + private static void sampleBoundaryVertex( + ZoneSceneContext ctx, + int plane, + int boundaryX, + int boundaryY, + int flatHeight, + HashMap surfaceHeights + ) { + long key = Sample.packWorldVertex(boundaryX, boundaryY); + surfaceHeights.putIfAbsent( + key, + cornerSurfaceHeightAtWorldVertex(ctx, plane, boundaryX, boundaryY, flatHeight) + ); + } +} diff --git a/src/main/java/rs117/hd/renderer/zone/SceneManager.java b/src/main/java/rs117/hd/renderer/zone/SceneManager.java index 48415f6155..7916ebf2a9 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneManager.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneManager.java @@ -23,6 +23,7 @@ import net.runelite.client.eventbus.Subscribe; import rs117.hd.HdPlugin; import rs117.hd.HdPluginConfig; +import rs117.hd.renderer.zone.HorizonExtender; import rs117.hd.opengl.uniforms.UBOWorldViews; import rs117.hd.overlays.FrameTimer; import rs117.hd.overlays.Timer; @@ -83,6 +84,9 @@ public class SceneManager { @Inject private ProceduralGenerator proceduralGenerator; + @Inject + private HorizonExtender horizonExtender; + @Inject private NpcDisplacementCache npcDisplacementCache; @@ -352,12 +356,12 @@ public void invalidateZone(Scene scene, int zx, int zz) { } @Nullable - private Area resolveExtendWaterArea(SceneContext ctx) { + private Area resolveHorizonTileArea(SceneContext ctx) { if (ctx.sceneBase == null) return null; for (Area area : areaManager.areasWithAreaHiding) { - if (area.extendWater && ctx.intersects(area)) + if (area.hasHorizonTiles() && ctx.intersects(area)) return area; } return null; @@ -470,10 +474,11 @@ public synchronized void loadScene(WorldView worldView, Scene scene) { nextSceneContext.enableAreaHiding = nextSceneContext.sceneBase != null && config.hideUnrelatedAreas(); nextSceneContext.fillGaps = config.fillGapsInTerrain(); - nextSceneContext.enableExtendWater = - nextSceneContext.enableAreaHiding && config.horizonTiles(); - nextSceneContext.extendWaterArea = nextSceneContext.enableExtendWater ? - resolveExtendWaterArea(nextSceneContext) : null; + nextSceneContext.enableHorizonTiles = + nextSceneContext.enableAreaHiding && + config.horizonTiles(); + nextSceneContext.horizonTileArea = nextSceneContext.enableHorizonTiles ? + resolveHorizonTileArea(nextSceneContext) : null; if (nextSceneContext.intersects(areaManager.getArea("PLAYER_OWNED_HOUSE"))) { nextSceneContext.isInHouse = true; @@ -565,7 +570,7 @@ public synchronized void loadScene(WorldView worldView, Scene scene) { old.needsRoofUpdate = true; - if (old.hasWater || old.dirty || isEdgeTile(ctx.zones, ox, oz) || nextSceneContext.extendWaterArea != null) { + 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++; @@ -755,6 +760,8 @@ public void swapScene(Scene scene) { checkGLErrors(); root.sceneSwapTime = sw.elapsed(TimeUnit.NANOSECONDS); log.debug("swapScene time: {}", sw); + if (root.sceneContext != null) + horizonExtender.logSceneLoadTiming(root.sceneContext); } private void loadSubScene(WorldView worldView, Scene scene) { diff --git a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java index f5b2562778..de1a6624df 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java @@ -31,6 +31,7 @@ import net.runelite.api.*; import net.runelite.client.callback.RenderCallbackManager; import rs117.hd.HdPlugin; +import rs117.hd.renderer.zone.HorizonExtender; import rs117.hd.scene.GamevalManager; import rs117.hd.scene.MaterialManager; import rs117.hd.scene.ModelOverrideManager; @@ -113,7 +114,7 @@ public class SceneUploader implements AutoCloseable { private GapFiller gapFiller; @Inject - private WaterExtender waterExtender; + private HorizonExtender horizonExtender; @FunctionalInterface public interface OnBeforeProcessTileFunc { @@ -200,7 +201,7 @@ public void estimateZoneSize(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) if (ctx.fillGaps) gapFiller.estimateForZone(ctx, zone, mzx, mzz); - waterExtender.estimateForZone(ctx, zone, mzx, mzz); + horizonExtender.estimateForZone(ctx, zone, mzx, mzz); } public void uploadZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws InterruptedException { @@ -230,7 +231,7 @@ public void uploadZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) throws if (z == 0) { uploadZoneLevel(ctx, zone, mzx, mzz, 0, false, roofIds, vb, ab, fb); if (vb != null) - waterExtender.uploadUnderwaterForZone(ctx, zone, mzx, mzz, vb, fb); + horizonExtender.uploadUnderwaterForZone(ctx, zone, mzx, mzz, vb, fb); uploadZoneLevel(ctx, zone, mzx, mzz, 0, true, roofIds, vb, ab, fb); uploadZoneLevel(ctx, zone, mzx, mzz, 1, true, roofIds, vb, ab, fb); uploadZoneLevel(ctx, zone, mzx, mzz, 2, true, roofIds, vb, ab, fb); @@ -247,7 +248,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); - waterExtender.uploadSurfaceForZone(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) diff --git a/src/main/java/rs117/hd/renderer/zone/WaterExtender.java b/src/main/java/rs117/hd/renderer/zone/WaterExtender.java deleted file mode 100644 index ad2bbd2907..0000000000 --- a/src/main/java/rs117/hd/renderer/zone/WaterExtender.java +++ /dev/null @@ -1,905 +0,0 @@ -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.*; -import rs117.hd.HdPlugin; -import rs117.hd.scene.ExtendWaterSample; -import rs117.hd.scene.ExtendWaterTileUtil; -import rs117.hd.scene.ProceduralGenerator; -import rs117.hd.scene.SceneContext; -import rs117.hd.scene.TileOverrideManager; -import rs117.hd.scene.areas.Area; -import rs117.hd.scene.areas.AABB; -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.*; -import static net.runelite.api.Perspective.LOCAL_TILE_SIZE; -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.MathUtils.fract; - -@Slf4j -@Singleton -public class WaterExtender { - private static final int[] UP_NORMAL = { 0, -1, 0 }; - private static final int EDGE_INSET = 4; - - @Inject - private HdPlugin plugin; - - @Inject - private TileOverrideManager tileOverrideManager; - - @Inject - private ProceduralGenerator proceduralGenerator; - - /** - * Marks horizon tiles as water in procedural terrain data and clears land flags on adjacent water faces. - * Call once during underwater terrain generation before depth sinking. - */ - public void prepareSceneTerrain(SceneContext ctx, Tile[][][] tiles) { - if (!ExtendWaterTileUtil.isEnabled(ctx)) - return; - - ExtendWaterTileUtil.buildHorizonTileMask(ctx); - boolean[][] mask = ctx.horizonTileMask; - if (mask == null) - return; - - int z = ExtendWaterTileUtil.getExtendWaterPlane(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]) - continue; - - if (ctx.tileIsWater[z][x][y]) - continue; - - Tile tile = tiles[z][x][y]; - if (tile != null && hasVisibleLand(ctx, tile, x, y, z)) - continue; - - ctx.tileIsWater[z][x][y] = true; - - if (tile == null || ExtendWaterTileUtil.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 && !ExtendWaterTileUtil.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.tileIsWater[z][nx][ny]) - 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.vertexIsLand.remove(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.vertexIsLand.remove(key); - } - } - - public void estimateForZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) { - if (!isEnabled(ctx)) - return; - - 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; - if (shouldExtendTile(ctx, tileExX, tileExY) > 0) { - zone.hasWater = true; - // underwater + surface - zone.sizeO += 4; - zone.sizeF += 4; - } - } - } - } - - public void uploadUnderwaterForZone( - ZoneSceneContext ctx, - Zone zone, - int mzx, - int mzz, - GpuIntBuffer vb, - GpuIntBuffer fb - ) { - uploadForZone(ctx, zone, mzx, mzz, vb, fb, true); - } - - public void uploadSurfaceForZone( - ZoneSceneContext ctx, - Zone zone, - int mzx, - int mzz, - GpuIntBuffer vb, - GpuIntBuffer fb - ) { - uploadForZone(ctx, zone, mzx, mzz, vb, fb, false); - } - - private void uploadForZone( - ZoneSceneContext ctx, - Zone zone, - int mzx, - int mzz, - GpuIntBuffer vb, - GpuIntBuffer fb, - boolean underwater - ) { - ExtendWaterSample sample = resolveSample(ctx); - if (sample == null) - return; - - Area area = ExtendWaterTileUtil.getExtendWaterArea(ctx); - assert area != null; - - int basex = (mzx - (ctx.sceneOffset >> 3)) << 10; - int basez = (mzz - (ctx.sceneOffset >> 3)) << 10; - - int posBefore = vb.position(); - 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; - if (shouldExtendTile(ctx, tileExX, tileExY) > 0) { - int tileX = tileExX - ctx.sceneOffset; - int tileY = tileExY - ctx.sceneOffset; - int[] worldPos = ctx.sceneToWorld(tileX, tileY, sample.plane); - if (underwater) { - uploadUnderwaterTile(ctx, area, sample, worldPos, tileX, tileY, basex, basez, vb, fb); - } else { - uploadSurfaceTile(ctx, area, sample, worldPos, tileX, tileY, basex, basez, vb, fb); - } - } - } - } - - if (vb.position() > posBefore) - zone.hasWater = true; - } - - private static boolean isEnabled(ZoneSceneContext ctx) { - return ExtendWaterTileUtil.isEnabled(ctx); - } - - @Nullable - private ExtendWaterSample resolveSample(ZoneSceneContext ctx) { - Area area = ExtendWaterTileUtil.getExtendWaterArea(ctx); - if (!isEnabled(ctx)) - return null; - - if (ctx.extendWaterSample != null) - return ctx.extendWaterSample; - - assert area != null; - int[] ref = area.extendWaterReferenceTile; - int worldX = ref[0]; - int worldY = ref[1]; - int plane = ref.length > 2 ? ref[2] : 0; - - WaterType waterType = WaterType.NONE; - int textureId = -1; - int flatHeight = 0; - TileOverride override = TileOverride.NONE; - - 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); - } else { - SceneTileModel model = tile.getSceneTileModel(); - if (model != null && model.getTriangleTextureId() != null) { - for (int faceTexture : model.getTriangleTextureId()) { - if (faceTexture != -1) { - textureId = faceTexture; - break; - } - } - } - flatHeight = averageHeight(tileHeights, plane, tileExX, tileExY); - } - waterType = proceduralGenerator.seasonalWaterType(override, textureId); - } - } - } - - if (waterType == WaterType.NONE) { - int[] worldPos = { worldX, worldY, plane }; - if (override == TileOverride.NONE) - override = tileOverrideManager.getOverrideBeforeReplacements(worldPos); - waterType = proceduralGenerator.seasonalWaterType(override, textureId); - } - - if (waterType == WaterType.NONE) - waterType = WaterType.WATER; - - HashMap boundarySurfaceHeights = new HashMap<>(); - HashMap boundaryUnderwaterHeights = new HashMap<>(); - buildBoundaryHeightMaps(ctx, area, plane, flatHeight, boundarySurfaceHeights, boundaryUnderwaterHeights); - - int flatUnderwaterHeight = flatHeight + (int) (ProceduralGenerator.MAX_DEPTH * .55f); - if (!boundaryUnderwaterHeights.isEmpty()) { - int sum = 0; - for (int h : boundaryUnderwaterHeights.values()) - sum += h; - flatUnderwaterHeight = sum / boundaryUnderwaterHeights.size(); - } - - ctx.extendWaterSample = new ExtendWaterSample( - waterType, - plane, - flatHeight, - flatUnderwaterHeight, - boundarySurfaceHeights, - boundaryUnderwaterHeights - ); - log.info( - "ExtendWater enabled for {} using reference tile [{}, {}, {}]: {} at height {}, {} boundary vertices sampled", - area.name, - worldX, - worldY, - plane, - waterType, - flatHeight, - boundaryUnderwaterHeights.size() - ); - return ctx.extendWaterSample; - } - - 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 - ); - } - } - } - - /** - * Sample underwater height along an AABB edge, stepping inward when the boundary - * vertex has no water depth (common on the south shore where minY is land). - * Results are stored under the boundary vertex key so extended tiles can look them up. - */ - private static void sampleEdgeWithInwardSearch( - ZoneSceneContext ctx, - int plane, - AABB aabb, - int boundaryX, - int boundaryY, - int stepX, - int stepY, - int flatHeight, - HashMap surfaceHeights, - HashMap underwaterHeights - ) { - long boundaryKey = ExtendWaterSample.packWorldVertex(boundaryX, boundaryY); - if (underwaterHeights.containsKey(boundaryKey)) - 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) - ); - } - - @Nullable - 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 clampBoundaryVertexX(AABB aabb, int worldX) { - return Math.min(Math.max(worldX, aabb.minX), aabb.maxX + 1); - } - - private static int clampBoundaryVertexY(AABB aabb, int worldY) { - return Math.min(Math.max(worldY, aabb.minY), aabb.maxY + 1); - } - - private static int resolveCornerSurfaceHeight( - ZoneSceneContext ctx, - ExtendWaterSample sample, - AABB aabb, - int worldX, - int worldY - ) { - int boundaryX = clampBoundaryVertexX(aabb, worldX); - int boundaryY = clampBoundaryVertexY(aabb, worldY); - - Integer live = liveSurfaceHeightAtWorldVertex(ctx, sample.plane, boundaryX, boundaryY); - if (live != null) - return live; - - long key = ExtendWaterSample.packWorldVertex(boundaryX, boundaryY); - Integer height = sample.boundarySurfaceHeights.get(key); - return height != null ? height : sample.flatHeight; - } - - private static int resolveCornerUnderwaterHeight( - ZoneSceneContext ctx, - ExtendWaterSample sample, - AABB aabb, - int worldX, - int worldY - ) { - int boundaryX = clampBoundaryVertexX(aabb, worldX); - int boundaryY = clampBoundaryVertexY(aabb, worldY); - - Integer live = liveUnderwaterHeightAtWorldVertex(ctx, sample.plane, boundaryX, boundaryY); - if (live != null) - return live; - - long key = ExtendWaterSample.packWorldVertex(boundaryX, boundaryY); - Integer height = sample.boundaryUnderwaterHeights.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 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]; - } - - @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.vertexUnderwaterDepth == 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; - - int[][] cornerTiles = { - { 0, 0, 0 }, - { -1, 0, 1 }, - { 0, -1, 2 }, - { -1, -1, 3 }, - }; - - Tile[][][] tiles = ctx.scene.getExtendedTiles(); - for (int[] corner : cornerTiles) { - 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); - Integer depth = ctx.vertexUnderwaterDepth.get(keys[vertexIndex]); - if (depth != null && 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.vertexUnderwaterDepth != null) { - int height = cornerSurfaceHeightAtWorldVertex(ctx, plane, worldX, worldY, 0); - int key = HDUtils.fastVertexHash(new int[] { - sceneX * LOCAL_TILE_SIZE, - sceneY * LOCAL_TILE_SIZE, - height - }); - Integer depth = ctx.vertexUnderwaterDepth.get(key); - if (depth != null && depth > 0) - return depth; - } - - return 0; - } - - private static int cornerSurfaceHeightAtWorldVertex( - ZoneSceneContext ctx, - int plane, - int worldX, - int worldY, - int fallback - ) { - if (ctx.sceneBase == null) - return fallback; - - 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 >= ctx.scene.getTileHeights()[plane].length || - tileExY >= ctx.scene.getTileHeights()[plane][tileExX].length) - return fallback; - - return ctx.scene.getTileHeights()[plane][tileExX][tileExY]; - } - - 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; - } - - /** - * @return face count if the tile should be extended with water, otherwise 0 - */ - private int shouldExtendTile(ZoneSceneContext ctx, int tileExX, int tileExY) { - if (tileExX < 0 || tileExY < 0 || tileExX >= EXTENDED_SCENE_SIZE || tileExY >= EXTENDED_SCENE_SIZE) - return 0; - - ExtendWaterSample sample = resolveSample(ctx); - if (sample == null) - return 0; - - int tileZ = sample.plane; - - if (!ExtendWaterTileUtil.shouldExtendWaterAtTile(ctx, tileExX, tileExY, tileZ)) - return 0; - - if (ctx.tileIsWater != null && - ctx.tileIsWater[tileZ][tileExX][tileExY] && - (ctx.filledTiles[tileExX][tileExY] & (1 << tileZ)) != 0) - return 0; - - Tile[][][] extendedTiles = ctx.scene.getExtendedTiles(); - Tile tile = extendedTiles[tileZ][tileExX][tileExY]; - - if (tile == null || ExtendWaterTileUtil.isHiddenGroundTile(tile)) - return 2; - - if ((ctx.filledTiles[tileExX][tileExY] & (1 << tileZ)) != 0 && hasVisibleLand(ctx, tile, tileExX, tileExY, tileZ)) - return 0; - - return 2; - } - - private boolean hasVisibleLand(SceneContext ctx, Tile tile, int tileExX, int tileExY, int tileZ) { - if (tile.getBridge() != null) - tile = tile.getBridge(); - - SceneTilePaint paint = tile.getSceneTilePaint(); - if (paint != null && paint.getNeColor() != HIDDEN_HSL) { - int[] worldPos = ctx.extendedSceneToWorld(tileExX, tileExY, tileZ); - var override = tileOverrideManager.getOverride(ctx, tile, worldPos); - if (proceduralGenerator.seasonalWaterType(override, paint.getTexture()) != WaterType.NONE) - return false; - return true; - } - - SceneTileModel model = tile.getSceneTileModel(); - if (model == null) - return false; - - int[] worldPos = ctx.extendedSceneToWorld(tileExX, tileExY, tileZ); - int overlayId = OVERLAY_FLAG | ctx.scene.getOverlayIds()[tileZ][tileExX][tileExY]; - int underlayId = ctx.scene.getUnderlayIds()[tileZ][tileExX][tileExY]; - 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; - - return true; - } - return false; - } - - private void uploadUnderwaterTile( - ZoneSceneContext ctx, - Area area, - ExtendWaterSample sample, - int[] worldPos, - int tileX, - int tileY, - int zoneBasex, - int zoneBasez, - GpuIntBuffer vb, - GpuIntBuffer fb - ) { - int tileZ = sample.plane; - AABB aabb = nearestAabb(area, worldPos[0], worldPos[1], tileZ); - if (aabb == null) - return; - - int swHeight = resolveCornerUnderwaterHeight(ctx, sample, aabb, worldPos[0], worldPos[1]); - int seHeight = resolveCornerUnderwaterHeight(ctx, sample, aabb, worldPos[0] + 1, worldPos[1]); - int nwHeight = resolveCornerUnderwaterHeight(ctx, sample, aabb, worldPos[0], worldPos[1] + 1); - int neHeight = resolveCornerUnderwaterHeight(ctx, sample, aabb, worldPos[0] + 1, worldPos[1] + 1); - - int swDepth = max(1, swHeight - resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0], worldPos[1])); - int seDepth = max(1, seHeight - resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0] + 1, worldPos[1])); - int nwDepth = max(1, nwHeight - resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0], worldPos[1] + 1)); - int neDepth = max(1, neHeight - resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0] + 1, worldPos[1] + 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(worldPos[0], worldPos[1], worldPos[2]); - seMaterial = groundMaterial.getRandomMaterial(worldPos[0] + 1, worldPos[1], worldPos[2]); - nwMaterial = groundMaterial.getRandomMaterial(worldPos[0], worldPos[1] + 1, worldPos[2]); - neMaterial = groundMaterial.getRandomMaterial(worldPos[0] + 1, worldPos[1] + 1, worldPos[2]); - } - - int swTerrainData = HDUtils.packTerrainData(true, max(1, swDepth), sample.waterType, tileZ); - int seTerrainData = HDUtils.packTerrainData(true, max(1, seDepth), sample.waterType, tileZ); - int nwTerrainData = HDUtils.packTerrainData(true, max(1, nwDepth), sample.waterType, tileZ); - int neTerrainData = HDUtils.packTerrainData(true, max(1, neDepth), sample.waterType, tileZ); - int swMaterialData = swMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); - int seMaterialData = seMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); - int nwMaterialData = nwMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); - int neMaterialData = neMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); - - float uvx = fract(worldPos[0]); - float uvy = fract(worldPos[1]); - - int baseX = tileX * LOCAL_TILE_SIZE - zoneBasex; - int baseZ = tileY * LOCAL_TILE_SIZE - zoneBasez; - - putTerrainTriangle( - vb, fb, - UNDERWATER_HSL, UNDERWATER_HSL, UNDERWATER_HSL, - neMaterialData, nwMaterialData, seMaterialData, - neTerrainData, nwTerrainData, seTerrainData, - baseX + LOCAL_TILE_SIZE, neHeight, baseZ + LOCAL_TILE_SIZE, uvx, uvy, - baseX, nwHeight, baseZ + LOCAL_TILE_SIZE, uvx - 1, uvy, - baseX + LOCAL_TILE_SIZE, seHeight, baseZ, uvx, uvy - 1, - UP_NORMAL - ); - putTerrainTriangle( - vb, fb, - UNDERWATER_HSL, UNDERWATER_HSL, UNDERWATER_HSL, - swMaterialData, seMaterialData, nwMaterialData, - swTerrainData, seTerrainData, nwTerrainData, - baseX, swHeight, baseZ, uvx - 1, uvy - 1, - baseX + LOCAL_TILE_SIZE, seHeight, baseZ, uvx, uvy - 1, - baseX, nwHeight, baseZ + LOCAL_TILE_SIZE, uvx - 1, uvy, - UP_NORMAL - ); - } - - private void uploadSurfaceTile( - ZoneSceneContext ctx, - Area area, - ExtendWaterSample sample, - int[] worldPos, - int tileX, - int tileY, - int zoneBasex, - int zoneBasez, - GpuIntBuffer vb, - GpuIntBuffer fb - ) { - int tileZ = sample.plane; - AABB aabb = nearestAabb(area, worldPos[0], worldPos[1], tileZ); - if (aabb == null) - return; - - int swHeight = resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0], worldPos[1]); - int seHeight = resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0] + 1, worldPos[1]); - int nwHeight = resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0], worldPos[1] + 1); - int neHeight = resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0] + 1, worldPos[1] + 1); - - int terrainData = HDUtils.packTerrainData(true, 0, sample.waterType, tileZ); - int packedMaterial = Material.NONE.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); - int color = 127; - - int baseX = tileX * LOCAL_TILE_SIZE - zoneBasex; - int baseZ = tileY * LOCAL_TILE_SIZE - zoneBasez; - - putTerrainTriangle( - vb, fb, - color, color, color, - packedMaterial, packedMaterial, packedMaterial, - terrainData, terrainData, terrainData, - baseX + LOCAL_TILE_SIZE, neHeight, baseZ + LOCAL_TILE_SIZE, 0, 0, - baseX, nwHeight, baseZ + LOCAL_TILE_SIZE, 0, 0, - baseX + LOCAL_TILE_SIZE, seHeight, baseZ, 0, 0, - UP_NORMAL - ); - putTerrainTriangle( - vb, fb, - color, color, color, - packedMaterial, packedMaterial, packedMaterial, - terrainData, terrainData, terrainData, - baseX, swHeight, baseZ, 0, 0, - baseX + LOCAL_TILE_SIZE, seHeight, baseZ, 0, 0, - baseX, nwHeight, baseZ + LOCAL_TILE_SIZE, 0, 0, - UP_NORMAL - ); - } - - private static void putTerrainTriangle( - GpuIntBuffer vb, - GpuIntBuffer fb, - 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, - int[] normal - ) { - int faceIdx = fb.putFace( - colorA, colorB, colorC, - packedMaterialA, packedMaterialB, packedMaterialC, - terrainDataA, terrainDataB, terrainDataC - ); - vb.putVertex(x0, y0, z0, u0, v0, 0, normal[0], normal[2], normal[1], faceIdx); - vb.putVertex(x1, y1, z1, u1, v1, 0, normal[0], normal[2], normal[1], faceIdx); - vb.putVertex(x2, y2, z2, u2, v2, 0, normal[0], normal[2], normal[1], faceIdx); - } -} 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 16a7f415b0..6870fb2ffa 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 && ctx.sceneContext.extendWaterArea == 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/ExtendWaterSample.java b/src/main/java/rs117/hd/scene/ExtendWaterSample.java deleted file mode 100644 index 78df2206c9..0000000000 --- a/src/main/java/rs117/hd/scene/ExtendWaterSample.java +++ /dev/null @@ -1,33 +0,0 @@ -package rs117.hd.scene; - -import java.util.HashMap; -import rs117.hd.scene.water_types.WaterType; - -public class ExtendWaterSample { - public final WaterType waterType; - public final int plane; - public final int flatHeight; - public final int flatUnderwaterHeight; - public final HashMap boundarySurfaceHeights; - public final HashMap boundaryUnderwaterHeights; - - public ExtendWaterSample( - WaterType waterType, - int plane, - int flatHeight, - int flatUnderwaterHeight, - HashMap boundarySurfaceHeights, - HashMap boundaryUnderwaterHeights - ) { - this.waterType = waterType; - this.plane = plane; - this.flatHeight = flatHeight; - this.flatUnderwaterHeight = flatUnderwaterHeight; - this.boundarySurfaceHeights = boundarySurfaceHeights; - this.boundaryUnderwaterHeights = boundaryUnderwaterHeights; - } - - public static long packWorldVertex(int worldX, int worldY) { - return ((long) worldX << 32) | (worldY & 0xffffffffL); - } -} diff --git a/src/main/java/rs117/hd/scene/ExtendWaterTileUtil.java b/src/main/java/rs117/hd/scene/ExtendWaterTileUtil.java deleted file mode 100644 index f8bb2ea179..0000000000 --- a/src/main/java/rs117/hd/scene/ExtendWaterTileUtil.java +++ /dev/null @@ -1,134 +0,0 @@ -package rs117.hd.scene; - -import javax.annotation.Nullable; -import net.runelite.api.SceneTileModel; -import net.runelite.api.SceneTilePaint; -import net.runelite.api.Tile; -import rs117.hd.scene.areas.Area; - -import static net.runelite.api.Constants.CHUNK_SIZE; -import static net.runelite.api.Constants.SCENE_SIZE; -import static rs117.hd.utils.HDUtils.HIDDEN_HSL; - -public final class ExtendWaterTileUtil { - private ExtendWaterTileUtil() {} - - @Nullable - public static Area getExtendWaterArea(SceneContext ctx) { - if (!ctx.enableExtendWater) - return null; - if (ctx.extendWaterArea != null) - return ctx.extendWaterArea; - if (ctx.currentArea != null && ctx.currentArea.extendWater) - return ctx.currentArea; - return null; - } - - public static boolean isEnabled(SceneContext ctx) { - if (!ctx.enableExtendWater) - return false; - - Area area = getExtendWaterArea(ctx); - return ctx.sceneBase != null && - area != null && - area.extendWater && - area.extendWaterReferenceTile != null && - area.extendWaterReferenceTile.length >= 2; - } - - public static int getExtendWaterPlane(SceneContext ctx) { - Area area = getExtendWaterArea(ctx); - if (area == null || area.extendWaterReferenceTile == null) - return 0; - int[] ref = area.extendWaterReferenceTile; - return ref.length > 2 ? ref[2] : 0; - } - - public static boolean isWithinExtendWaterBounds(SceneContext ctx, int tileExX, int tileExY, int plane) { - Area area = getExtendWaterArea(ctx); - if (area == null) - return false; - int[] worldPos = ctx.extendedSceneToWorld(tileExX, tileExY, plane); - return area.containsPoint(false, worldPos[0], worldPos[1], worldPos[2]); - } - - /** - * The loaded map region including extended map loading chunks. Horizon tiles must stay - * within this region — the extended scene buffer is larger, but only this area has map data. - */ - public static boolean isWithinHorizonRange(SceneContext ctx, int tileExX, int tileExY) { - if (ctx.sceneBase == null) - return false; - - int tileX = tileExX - ctx.sceneOffset; - int tileY = tileExY - ctx.sceneOffset; - int sceneMin = -ctx.expandedMapLoadingChunks * CHUNK_SIZE; - int sceneMax = SCENE_SIZE + ctx.expandedMapLoadingChunks * CHUNK_SIZE; - return tileX >= sceneMin && tileY >= sceneMin && tileX <= sceneMax && tileY <= sceneMax; - } - - public 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 computeShouldExtendWaterAtTile(SceneContext ctx, int tileExX, int tileExY, int plane) { - if (!isWithinHorizonRange(ctx, tileExX, tileExY)) - return false; - return !isWithinExtendWaterBounds(ctx, tileExX, tileExY, plane); - } - - public static void buildHorizonTileMask(SceneContext ctx) { - if (!isEnabled(ctx)) { - ctx.horizonTileMask = null; - return; - } - - int plane = getExtendWaterPlane(ctx); - boolean[][] mask = new boolean[ctx.sizeX][ctx.sizeZ]; - for (int x = 0; x < ctx.sizeX; ++x) { - for (int y = 0; y < ctx.sizeZ; ++y) { - mask[x][y] = computeShouldExtendWaterAtTile(ctx, x, y, plane); - } - } - ctx.horizonTileMask = mask; - } - - /** - * Horizon tiles fill empty space outside the area AABB up to the edge of the loaded map. - */ - public static boolean shouldExtendWaterAtTile(SceneContext ctx, int tileExX, int tileExY, int plane) { - if (!isEnabled(ctx) || plane != getExtendWaterPlane(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]; - - return computeShouldExtendWaterAtTile(ctx, tileExX, tileExY, plane); - } -} diff --git a/src/main/java/rs117/hd/scene/ProceduralGenerator.java b/src/main/java/rs117/hd/scene/ProceduralGenerator.java index 451a26b1ef..be15709002 100644 --- a/src/main/java/rs117/hd/scene/ProceduralGenerator.java +++ b/src/main/java/rs117/hd/scene/ProceduralGenerator.java @@ -27,12 +27,11 @@ import java.util.Arrays; import java.util.HashMap; import javax.inject.Inject; -import javax.inject.Provider; import javax.inject.Singleton; import lombok.extern.slf4j.Slf4j; import net.runelite.api.*; import rs117.hd.renderer.legacy.LegacySceneContext; -import rs117.hd.renderer.zone.WaterExtender; +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; @@ -79,7 +78,7 @@ public class ProceduralGenerator { private WaterTypeManager waterTypeManager; @Inject - private Provider waterExtender; + private HorizonExtender horizonExtender; public void generateSceneData(SceneContext sceneContext) { @@ -113,7 +112,7 @@ public void clearSceneData(SceneContext sceneContext) { if (!(sceneContext instanceof LegacySceneContext)) sceneContext.underwaterDepthLevels = null; sceneContext.horizonTileMask = null; - sceneContext.extendWaterSample = null; + sceneContext.horizonTileSample = null; } /** @@ -541,7 +540,7 @@ else if (tile.getSceneTileModel() != null) } } - waterExtender.get().prepareSceneTerrain(sceneContext, tiles); + horizonExtender.prepareSceneTerrain(sceneContext, tiles); // Sink terrain further from shore by desired levels. for (int level = 0; level < DEPTH_LEVEL_SLOPE.length - 1; level++) @@ -561,8 +560,8 @@ else if (tile.getSceneTileModel() != null) // it creates a 'wall' to prevent fog from passing through. // Not incredibly effective, but better than nothing. if (x == 0 || y == 0 || x == sizeX || y == sizeY) { - if (!(ExtendWaterTileUtil.isEnabled(sceneContext) && - ExtendWaterTileUtil.shouldExtendWaterAtTile(sceneContext, x, y, z))) + if (!(HorizonExtender.isEnabled(sceneContext) && + HorizonExtender.shouldPatchWaterAtTile(sceneContext, x, y, z))) { sceneContext.underwaterDepthLevels[z][x][y] = 0; continue; diff --git a/src/main/java/rs117/hd/scene/SceneContext.java b/src/main/java/rs117/hd/scene/SceneContext.java index d5d9f4ca81..d240782553 100644 --- a/src/main/java/rs117/hd/scene/SceneContext.java +++ b/src/main/java/rs117/hd/scene/SceneContext.java @@ -8,6 +8,8 @@ import javax.annotation.Nullable; import net.runelite.api.*; import net.runelite.api.coords.*; +import rs117.hd.HdPluginConfig; +import rs117.hd.renderer.zone.HorizonExtender; import rs117.hd.scene.areas.AABB; import rs117.hd.scene.areas.Area; import rs117.hd.scene.environments.Environment; @@ -34,10 +36,13 @@ public class SceneContext { public boolean enableAreaHiding; public boolean fillGaps; - public boolean enableExtendWater; + public boolean enableHorizonTiles; + + /** Nanoseconds spent on horizon tile work during the current scene load. */ + public long horizonTileNs; @Nullable - public ExtendWaterSample extendWaterSample; + public HorizonExtender.Sample horizonTileSample; @Nullable public boolean[][] horizonTileMask; public boolean isInChambersOfXeric; @@ -46,7 +51,7 @@ public class SceneContext { @Nullable public Area currentArea; @Nullable - public Area extendWaterArea; + public Area horizonTileArea; public Area[] possibleAreas = new Area[0]; public final ArrayList environments = new ArrayList<>(); public byte[][] filledTiles = new byte[EXTENDED_SCENE_SIZE][EXTENDED_SCENE_SIZE]; diff --git a/src/main/java/rs117/hd/scene/areas/Area.java b/src/main/java/rs117/hd/scene/areas/Area.java index 1ba0629834..e6f57fb876 100644 --- a/src/main/java/rs117/hd/scene/areas/Area.java +++ b/src/main/java/rs117/hd/scene/areas/Area.java @@ -14,8 +14,11 @@ public class Area { public String name; public boolean hideOtherAreas; public boolean fillGaps = true; - public boolean extendWater; - public int[] extendWaterReferenceTile; + @SerializedName(value = "horizonTileReference", alternate = { "extendWaterReferenceTile" }) + public int[] horizonTileReference; + + /** When true, extend flat land instead of water (no procgen patching). */ + public boolean horizonFlatTerrain; public String[] areas; public int[] regions; @@ -39,6 +42,10 @@ public Area(String name, int x1, int y1, int x2, int y2) { aabbs = new AABB[] { new AABB(x1, y1, x2, y2) }; } + public boolean hasHorizonTiles() { + return horizonTileReference != null && horizonTileReference.length >= 2; + } + public void normalize() { if (normalized) return; diff --git a/src/main/resources/rs117/hd/scene/areas.json b/src/main/resources/rs117/hd/scene/areas.json index cc24ac3618..93b52992b5 100644 --- a/src/main/resources/rs117/hd/scene/areas.json +++ b/src/main/resources/rs117/hd/scene/areas.json @@ -8421,7 +8421,9 @@ "aabbs": [ [ 2553, 4802, 2622, 4864 ] ], - "hideOtherAreas": true + "hideOtherAreas": true, + "horizonTileReference": [ 2580, 4825 ], + "horizonFlatTerrain": true }, { "name": "FIRE_ALTAR_CULL_FIRE_LIGHTS", @@ -8497,8 +8499,7 @@ [ 2689, 4859, 2748, 4802 ] ], "hideOtherAreas": true, - "extendWater": true, - "extendWaterReferenceTile": [ 2709, 4804 ] + "horizonTileReference": [ 2709, 4804 ] }, { "name": "MIND_ALTAR", @@ -8521,8 +8522,7 @@ [ 3202, 4800, 3263, 4863 ] ], "hideOtherAreas": true, - "extendWater": true, - "extendWaterReferenceTile": [ 3216, 3437 ] + "horizonTileReference": [ 3216, 3437 ] }, { "name": "TRUE_BLOOD_ALTAR_CRYSTAL_POOL", From 1de96a073967450b7d5b092eff65ffbbd88861ac Mon Sep 17 00:00:00 2001 From: Mark7625 <72366279+Mark7625@users.noreply.github.com> Date: Sat, 6 Jun 2026 15:05:47 +0100 Subject: [PATCH 3/4] Updated for new proc gen ect --- .../hd/renderer/zone/HorizonExtender.java | 902 +++++++----------- .../rs117/hd/renderer/zone/SceneManager.java | 3 - .../rs117/hd/renderer/zone/SceneUploader.java | 17 +- .../rs117/hd/scene/ProceduralGenerator.java | 22 +- .../java/rs117/hd/scene/SceneContext.java | 16 +- src/main/java/rs117/hd/scene/areas/Area.java | 12 +- 6 files changed, 388 insertions(+), 584 deletions(-) diff --git a/src/main/java/rs117/hd/renderer/zone/HorizonExtender.java b/src/main/java/rs117/hd/renderer/zone/HorizonExtender.java index 380631c4c3..d033cb6dc6 100644 --- a/src/main/java/rs117/hd/renderer/zone/HorizonExtender.java +++ b/src/main/java/rs117/hd/renderer/zone/HorizonExtender.java @@ -1,7 +1,6 @@ package rs117.hd.renderer.zone; import java.util.HashMap; -import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; @@ -30,9 +29,11 @@ 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 @@ -40,8 +41,13 @@ public class HorizonExtender { private static final int EDGE_INSET = 4; private static final int GPU_EXTRA_CHUNKS = 4; - private static final int[] UP_NORMAL = { 0, -1, 0 }; - private static final int WATER_COLOR = 127; + 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; @@ -55,6 +61,31 @@ public class HorizonExtender { @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; @@ -130,10 +161,6 @@ public static Sample terrain( ); } - public boolean isWaterHorizon() { - return boundaryUnderwaterHeights != null; - } - public boolean isWaterSurface() { return waterType != WaterType.NONE; } @@ -232,21 +259,11 @@ public static boolean shouldPatchWaterAtTile(SceneContext ctx, int tileExX, int isEligiblePatchTile(tile); } - public void logSceneLoadTiming(SceneContext ctx) { - if (!ctx.enableHorizonTiles) - return; - log.debug("horizon tile time: {} ms", TimeUnit.NANOSECONDS.toMillis(ctx.horizonTileNs)); - } - private static boolean isFlatTerrain(SceneContext ctx) { Area area = getArea(ctx); return area != null && area.horizonFlatTerrain; } - private static void addTiming(SceneContext ctx, long start) { - ctx.horizonTileNs += System.nanoTime() - start; - } - @Nullable private static Area getArea(SceneContext ctx) { if (!ctx.enableHorizonTiles) @@ -380,7 +397,29 @@ private static boolean shouldRenderAtTile(SceneContext ctx, int tileExX, int til return isOutsideAreaBounds(ctx, tileExX, tileExY, plane); } - private static int[] clipLocal( + 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 (!isOutsideAreaBounds(ctx, tileExX, tileExY, plane)) + return false; + return isWithinGpuRange(ctx, tileExX, tileExY); + } + + if (isOutsideAreaBounds(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, @@ -389,10 +428,11 @@ private static int[] clipLocal( int minLocalX, int minLocalZ, int maxLocalX, - int maxLocalZ + int maxLocalZ, + int[] out ) { if (ctx.sceneBase == null) - return null; + return false; int zoneBaseTileX = (mzx << 3) - ctx.sceneOffset; int zoneBaseTileY = (mzz << 3) - ctx.sceneOffset; @@ -426,9 +466,13 @@ private static int[] clipLocal( } if (clipMinX >= clipMaxX || clipMinZ >= clipMaxZ) - return null; + return false; - return new int[] { (int) clipMinX, (int) clipMinZ, (int) clipMaxX, (int) clipMaxZ }; + 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) { @@ -496,43 +540,8 @@ private static int clampVertexY(AABB aabb, int worldY) { return Math.min(Math.max(worldY, aabb.minY), aabb.maxY + 1); } - private static void putWaterSurface( - GpuIntBuffer vb, - GpuIntBuffer fb, - WaterType waterType, - int plane, - int minX, - int minZ, - int maxX, - int maxZ, - int height - ) { - int terrainData = HDUtils.packTerrainData(true, 0, waterType, plane); - int packedMaterial = Material.NONE.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false); - - putTriangle( - vb, fb, - WATER_COLOR, WATER_COLOR, WATER_COLOR, - packedMaterial, packedMaterial, packedMaterial, - terrainData, terrainData, terrainData, - maxX, height, maxZ, 0, 0, - minX, height, maxZ, 0, 0, - maxX, height, minZ, 0, 0 - ); - putTriangle( - vb, fb, - WATER_COLOR, WATER_COLOR, WATER_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 putUnderwaterFlat( - GpuIntBuffer vb, - GpuIntBuffer fb, + private static void putFlatQuad( + VertexWriteCache.Collection writeCache, WaterType waterType, int plane, int minX, @@ -540,14 +549,15 @@ private static void putUnderwaterFlat( 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( - vb, fb, - UNDERWATER_HSL, UNDERWATER_HSL, UNDERWATER_HSL, + writeCache, + color, color, color, packedMaterial, packedMaterial, packedMaterial, terrainData, terrainData, terrainData, maxX, height, maxZ, 0, 0, @@ -555,8 +565,8 @@ private static void putUnderwaterFlat( maxX, height, minZ, 0, 0 ); putTriangle( - vb, fb, - UNDERWATER_HSL, UNDERWATER_HSL, UNDERWATER_HSL, + writeCache, + color, color, color, packedMaterial, packedMaterial, packedMaterial, terrainData, terrainData, terrainData, minX, height, minZ, 0, 0, @@ -566,8 +576,7 @@ private static void putUnderwaterFlat( } private static void putTriangle( - GpuIntBuffer vb, - GpuIntBuffer fb, + VertexWriteCache.Collection writeCache, int colorA, int colorB, int colorC, @@ -581,14 +590,40 @@ private static void putTriangle( int x1, int y1, int z1, float u1, float v1, int x2, int y2, int z2, float u2, float v2 ) { - int faceIdx = fb.putFace( + var vb = writeCache.getVertexBuffer(); + var tb = writeCache.getTextureBuffer(); + int faceIdx = tb.putFace( colorA, colorB, colorC, packedMaterialA, packedMaterialB, packedMaterialC, terrainDataA, terrainDataB, terrainDataC ); - vb.putVertex(x0, y0, z0, u0, v0, 0, UP_NORMAL[0], UP_NORMAL[2], UP_NORMAL[1], faceIdx); - vb.putVertex(x1, y1, z1, u1, v1, 0, UP_NORMAL[0], UP_NORMAL[2], UP_NORMAL[1], faceIdx); - vb.putVertex(x2, y2, z2, u2, v2, 0, UP_NORMAL[0], UP_NORMAL[2], UP_NORMAL[1], faceIdx); + 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) { @@ -700,28 +735,13 @@ private static ShellEdges shellEdges(ZoneSceneContext ctx, int mzx, int mzz) { return new ShellEdges(ctx, mzx, mzz); } - private static int countShellQuads(ShellEdges e) { - if (e == null || !e.any()) + private static int countShellQuads(ShellEdges shell) { + if (shell == null || !shell.any()) return 0; - int quads = 0; - if (e.west) - quads++; - if (e.east) - quads++; - if (e.north) - quads++; - if (e.south) - quads++; - if (e.west && e.north) - quads++; - if (e.east && e.north) - quads++; - if (e.west && e.south) - quads++; - if (e.east && e.south) - quads++; - return quads; + int[] quads = { 0 }; + forEachShellStrip(shell, (minX, minZ, maxX, maxZ) -> quads[0]++); + return quads[0]; } private static int countShellStripTiles( @@ -733,21 +753,25 @@ private static int countShellStripTiles( int minLocalX, int minLocalZ, int maxLocalX, - int maxLocalZ + int maxLocalZ, + int[] clipBounds ) { - int[] bounds = clipLocal(ctx, area, mzx, mzz, plane, minLocalX, minLocalZ, maxLocalX, maxLocalZ); - if (bounds == null) + if (!clipLocal(ctx, area, mzx, mzz, plane, minLocalX, minLocalZ, maxLocalX, maxLocalZ, clipBounds)) return 0; - return countTiles(bounds[0], bounds[1], bounds[2], bounds[3]); + return countTiles(clipBounds[0], clipBounds[1], clipBounds[2], clipBounds[3]); } - private ZonePlan planZone(ZoneSceneContext ctx, int mzx, int mzz) { + 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; - if (shouldExtendTile(ctx, tileExX, tileExY) > 0) { + 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++; } @@ -821,18 +845,6 @@ private static int cornerSurfaceHeightAtWorldVertex( return live != null ? live : fallback; } - private static int baseShouldExtend(ZoneSceneContext ctx, Sample sample, int tileExX, int tileExY) { - if (!shouldRenderAtTile(ctx, tileExX, tileExY, sample.plane)) - return 0; - if (tileExX < 0 || tileExY < 0 || tileExX >= EXTENDED_SCENE_SIZE || tileExY >= EXTENDED_SCENE_SIZE) - return 2; - - Tile tile = ctx.scene.getExtendedTiles()[sample.plane][tileExX][tileExY]; - if (tile == null || isHiddenGroundTile(tile)) - return 2; - return 2; - } - public void prepareSceneTerrain(SceneContext ctx, Tile[][][] tiles) { Area area = getArea(ctx); if (isFlatTerrain(ctx)) @@ -840,62 +852,57 @@ public void prepareSceneTerrain(SceneContext ctx, Tile[][][] tiles) { if (!isEnabled(ctx)) return; - long start = System.nanoTime(); - try { - buildPatchMask(ctx, tiles); - boolean[][] mask = ctx.horizonTileMask; - if (mask == null) - 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.tileIsWater[z][x][y]) - continue; - - ctx.tileIsWater[z][x][y] = true; - 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; - } + 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.tileIsWater[z][nx][ny]) - continue; - - Tile neighbor = tiles[z][nx][ny]; - if (neighbor == null) - continue; - - clearLandFlagsOnWaterFaces(ctx, neighbor, nx, ny, z); - } + 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); } } } - } finally { - addTiming(ctx, start); } } @@ -909,7 +916,7 @@ private void clearLandFlagsOnWaterFaces(SceneContext ctx, Tile tile, int x, int var override = tileOverrideManager.getOverride(ctx, tile, worldPos); if (proceduralGenerator.seasonalWaterType(override, paint.getTexture()) != WaterType.NONE) { for (int key : ProceduralGenerator.tileVertexKeys(ctx, tile)) - ctx.vertexIsLand.remove(key); + ctx.clearVertexIsLand(key); } return; } @@ -936,39 +943,37 @@ private void clearLandFlagsOnWaterFaces(SceneContext ctx, Tile tile, int x, int continue; for (int key : ProceduralGenerator.faceVertexKeys(tile, face)) - ctx.vertexIsLand.remove(key); + ctx.clearVertexIsLand(key); } } public void estimateForZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) { if (!isEnabled(ctx)) return; - if (isFlatTerrain(ctx)) - estimateTerrainForZone(ctx, zone, mzx, mzz); - else - estimateWaterForZone(ctx, zone, mzx, mzz); - } - private void estimateWaterForZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) { - if (!isEnabled(ctx)) + Sample sample = resolveSample(ctx); + if (sample == null) return; - long start = System.nanoTime(); - try { - if (resolveSample(ctx) == null) - return; - - ZonePlan plan = planZone(ctx, mzx, mzz); - int shellQuads = countShellQuads(shellEdges(ctx, mzx, mzz)); - if (plan.tileCount == 0 && shellQuads == 0) - return; + boolean terrain = isFlatTerrain(ctx); + ZonePlan plan = planZone(ctx, mzx, mzz, sample); + ShellEdges shell = shellEdges(ctx, mzx, mzz); + int shellFaces = terrain ? + countTerrainShellFaces(ctx, getArea(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; - int facesPerLayer = plan.tileCount * 2 + shellQuads * 2; - zone.sizeO += facesPerLayer * 2; - zone.sizeF += facesPerLayer * 2; - } finally { - addTiming(ctx, start); + zone.sizeO += faces * 2; + zone.sizeF += faces * 2; } } @@ -982,7 +987,7 @@ public void uploadUnderwaterForZone( ) { if (!isEnabled(ctx) || isFlatTerrain(ctx)) return; - uploadWaterForZone(ctx, zone, mzx, mzz, vb, fb, true); + uploadForZone(ctx, zone, mzx, mzz, vb, fb, ZoneUploadMode.WATER_UNDERWATER); } public void uploadSurfaceForZone( @@ -995,25 +1000,26 @@ public void uploadSurfaceForZone( ) { if (!isEnabled(ctx)) return; - if (isFlatTerrain(ctx)) - uploadTerrainForZone(ctx, zone, mzx, mzz, vb, fb); - else - uploadWaterForZone(ctx, zone, mzx, mzz, vb, fb, false); + uploadForZone( + ctx, zone, mzx, mzz, vb, fb, + isFlatTerrain(ctx) ? ZoneUploadMode.TERRAIN : ZoneUploadMode.WATER_SURFACE + ); } - private void uploadWaterForZone( + private void uploadForZone( ZoneSceneContext ctx, Zone zone, int mzx, int mzz, GpuIntBuffer vb, GpuIntBuffer fb, - boolean underwater + ZoneUploadMode mode ) { - if (!isEnabled(ctx)) + if (!isEnabled(ctx) || vb == null) return; - long start = System.nanoTime(); + var scratch = UPLOAD_SCRATCH.get(); + scratch.begin(vb, fb); try { Sample sample = resolveSample(ctx); if (sample == null) @@ -1023,101 +1029,93 @@ private void uploadWaterForZone( assert area != null; int posBefore = vb.position(); - ZonePlan plan = planZone(ctx, mzx, mzz); + ZonePlan plan = planZone(ctx, mzx, mzz, sample); if (plan.tileCount > 0) { - int[][] cornerHeights = buildZoneCornerHeights(ctx, area, sample, mzx, mzz, underwater); - if (plan.fullZone && isUniformHeightGrid(cornerHeights)) { - uploadZoneQuad(ctx, sample, cornerHeights, mzx, mzz, underwater, vb, fb); - } else { - uploadZoneGrid(ctx, sample, plan, cornerHeights, mzx, mzz, underwater, vb, fb); - } + 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); } - uploadWaterShell(ctx, area, sample, mzx, mzz, underwater, vb, fb); + uploadShell(scratch.writeCache, ctx, area, sample, mzx, mzz, mode); if (vb.position() > posBefore) zone.hasWater = true; } finally { - addTiming(ctx, start); + scratch.end(); } } - private void uploadWaterShell( + private void uploadShell( + VertexWriteCache.Collection writeCache, ZoneSceneContext ctx, Area area, Sample sample, int mzx, int mzz, - boolean underwater, - GpuIntBuffer vb, - GpuIntBuffer fb + ZoneUploadMode mode ) { - ShellEdges shell = shellEdges(ctx, mzx, mzz); - if (shell == null) - return; - - int x = shell.extra; - int z = shell.extent; - if (shell.west) - uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, -x, 0, 0, z); - if (shell.east) - uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, z, 0, z + x, z); - if (shell.north) - uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, 0, -x, z, 0); - if (shell.south) - uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, 0, z, z, z + x); - if (shell.west && shell.north) - uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, -x, -x, 0, 0); - if (shell.east && shell.north) - uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, z, -x, z + x, 0); - if (shell.west && shell.south) - uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, -x, z, 0, z + x); - if (shell.east && shell.south) - uploadWaterShellStrip(ctx, area, sample, mzx, mzz, underwater, vb, fb, z, z, z + x, z + x); + 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 uploadWaterShellStrip( + private void uploadShellStrip( + VertexWriteCache.Collection writeCache, ZoneSceneContext ctx, Area area, Sample sample, int mzx, int mzz, - boolean underwater, - GpuIntBuffer vb, - GpuIntBuffer fb, int minLocalX, int minLocalZ, int maxLocalX, - int maxLocalZ + int maxLocalZ, + ZoneUploadMode mode, + int[] clipBounds ) { - int[] bounds = clipLocal(ctx, area, mzx, mzz, sample.plane, minLocalX, minLocalZ, maxLocalX, maxLocalZ); - if (bounds == null) + if (!clipLocal(ctx, area, mzx, mzz, sample.plane, minLocalX, minLocalZ, maxLocalX, maxLocalZ, clipBounds)) return; - if (underwater) { - int depth = max(1, sample.flatUnderwaterHeight - sample.flatHeight); - putUnderwaterFlat( - vb, fb, sample.waterType, sample.plane, - bounds[0], bounds[1], bounds[2], bounds[3], - sample.flatUnderwaterHeight, depth - ); - } else { - putWaterSurface( - vb, fb, sample.waterType, sample.plane, - bounds[0], bounds[1], bounds[2], bounds[3], sample.flatHeight - ); + 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 int[][] buildZoneCornerHeights( + private static void fillZoneCornerHeights( ZoneSceneContext ctx, Area area, Sample sample, int mzx, int mzz, + int[][] heights, boolean underwater ) { - int[][] heights = new int[CHUNK_SIZE + 1][CHUNK_SIZE + 1]; int plane = sample.plane; for (int vx = 0; vx <= CHUNK_SIZE; ++vx) { for (int vz = 0; vz <= CHUNK_SIZE; ++vz) { @@ -1134,18 +1132,16 @@ private int[][] buildZoneCornerHeights( resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0], worldPos[1]); } } - return heights; } private void uploadZoneQuad( + VertexWriteCache.Collection writeCache, ZoneSceneContext ctx, Sample sample, int[][] cornerHeights, int mzx, int mzz, - boolean underwater, - GpuIntBuffer vb, - GpuIntBuffer fb + boolean underwater ) { int sw = cornerHeights[0][0]; int se = cornerHeights[CHUNK_SIZE][0]; @@ -1153,27 +1149,22 @@ private void uploadZoneQuad( int ne = cornerHeights[CHUNK_SIZE][CHUNK_SIZE]; int extent = CHUNK_SIZE * LOCAL_TILE_SIZE; - if (underwater) { - uploadUnderwaterTile(ctx, sample, vb, fb, 0, 0, extent, extent, sw, se, nw, ne, mzx, mzz); - } else { - putWaterSurface(vb, fb, sample.waterType, sample.plane, 0, 0, extent, extent, sw); - } + 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 uploadZoneGrid( + private void uploadWaterZoneTiles( + VertexWriteCache.Collection writeCache, ZoneSceneContext ctx, Sample sample, ZonePlan plan, int[][] cornerHeights, int mzx, int mzz, - boolean underwater, - GpuIntBuffer vb, - GpuIntBuffer fb + boolean underwater ) { - 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)) @@ -1186,23 +1177,18 @@ private void uploadZoneGrid( int baseX = xoff * LOCAL_TILE_SIZE; int baseZ = zoff * LOCAL_TILE_SIZE; - if (underwater) { - uploadUnderwaterTile(ctx, sample, vb, fb, baseX, baseZ, baseX + LOCAL_TILE_SIZE, baseZ + LOCAL_TILE_SIZE, sw, se, nw, ne, mzx, mzz); - } else { - putWaterSurface( - vb, fb, sample.waterType, sample.plane, - baseX, baseZ, baseX + LOCAL_TILE_SIZE, baseZ + LOCAL_TILE_SIZE, sw - ); - } + 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, - GpuIntBuffer vb, - GpuIntBuffer fb, int minX, int minZ, int maxX, @@ -1243,7 +1229,7 @@ private void uploadUnderwaterTile( float uvy = fract(swWorld[1]); putTriangle( - vb, fb, + writeCache, UNDERWATER_HSL, UNDERWATER_HSL, UNDERWATER_HSL, neMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), nwMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), @@ -1256,7 +1242,7 @@ maxX, ne, maxZ, fract(neWorld[0]), fract(neWorld[1]), maxX, se, minZ, fract(seWorld[0]), uvy - 1 ); putTriangle( - vb, fb, + writeCache, UNDERWATER_HSL, UNDERWATER_HSL, UNDERWATER_HSL, swMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), seMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), @@ -1286,27 +1272,40 @@ private Sample resolveSample(ZoneSceneContext ctx) { return null; if (ctx.horizonTileSample != null) return ctx.horizonTileSample; - if (isFlatTerrain(ctx)) - return resolveTerrainSample(ctx); - return resolveWaterSample(ctx); - } - @Nullable - private Sample resolveWaterSample(ZoneSceneContext ctx) { - long start = System.nanoTime(); - try { - Area area = getArea(ctx); - assert area != null; + Area area = getArea(ctx); + assert area != null; - Reference ref = resolveReference(ctx, area); - WaterType waterType = ref.waterType; - if (waterType == WaterType.NONE) - waterType = WaterType.WATER; + Reference ref = resolveReference(ctx, area); + HashMap boundarySurfaceHeights = new HashMap<>(); + boolean terrain = isFlatTerrain(ctx); - HashMap boundarySurfaceHeights = new HashMap<>(); + 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; @@ -1327,43 +1326,8 @@ private Sample resolveWaterSample(ZoneSceneContext ctx) { "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; - } finally { - addTiming(ctx, start); - } - } - - @Nullable - private Sample resolveTerrainSample(ZoneSceneContext ctx) { - long start = System.nanoTime(); - try { - Area area = getArea(ctx); - assert area != null; - - Reference ref = resolveReference(ctx, area); - HashMap boundarySurfaceHeights = new HashMap<>(); - buildBoundarySurfaceHeights(ctx, area, ref.plane, ref.flatHeight, boundarySurfaceHeights); - - 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 - ); - return ctx.horizonTileSample; - } finally { - addTiming(ctx, start); } + return ctx.horizonTileSample; } private static void buildBoundaryHeightMaps( @@ -1401,12 +1365,20 @@ private static void sampleEdgeWithInwardSearch( int stepY, int flatHeight, HashMap surfaceHeights, - HashMap underwaterHeights + @Nullable HashMap underwaterHeights ) { long boundaryKey = Sample.packWorldVertex(boundaryX, boundaryY); - if (underwaterHeights.containsKey(boundaryKey)) + 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; @@ -1494,7 +1466,7 @@ private static int interiorUnderwaterOffsetAtVertex( int worldX, int worldY ) { - if (ctx.sceneBase == null || ctx.vertexUnderwaterDepth == null) + if (ctx.sceneBase == null || ctx.vertexTerrainData == null) return 0; int sceneX = worldX - ctx.sceneBase[0]; @@ -1502,15 +1474,8 @@ private static int interiorUnderwaterOffsetAtVertex( int tileExX = sceneX + ctx.sceneOffset; int tileExY = sceneY + ctx.sceneOffset; - int[][] cornerTiles = { - { 0, 0, 0 }, - { -1, 0, 1 }, - { 0, -1, 2 }, - { -1, -1, 3 }, - }; - Tile[][][] tiles = ctx.scene.getExtendedTiles(); - for (int[] corner : cornerTiles) { + for (int[] corner : CORNER_TILES) { int tx = tileExX + corner[0]; int ty = tileExY + corner[1]; int vertexIndex = corner[2]; @@ -1528,9 +1493,11 @@ private static int interiorUnderwaterOffsetAtVertex( continue; int[] keys = ProceduralGenerator.tileVertexKeys(ctx, tile); - Integer depth = ctx.vertexUnderwaterDepth.get(keys[vertexIndex]); - if (depth != null && depth > 0) - return depth; + if (ctx.vertexTerrainData.containsKey(keys[vertexIndex])) { + int depth = ctx.getVertexUnderwaterDepth(keys[vertexIndex]); + if (depth > 0) + return depth; + } } return 0; @@ -1562,27 +1529,27 @@ private static int cornerUnderwaterOffsetAtWorldVertex( } } - if (ctx.vertexUnderwaterDepth != null) { + if (ctx.vertexTerrainData != null) { int height = cornerSurfaceHeightAtWorldVertex(ctx, plane, worldX, worldY, 0); - int key = HDUtils.fastVertexHash(new int[] { + int key = tileVertexHash(new int[] { sceneX * LOCAL_TILE_SIZE, - sceneY * LOCAL_TILE_SIZE, - height + height, + sceneY * LOCAL_TILE_SIZE }); - Integer depth = ctx.vertexUnderwaterDepth.get(key); - if (depth != null && depth > 0) - return depth; + if (ctx.vertexTerrainData.containsKey(key)) { + int depth = ctx.getVertexUnderwaterDepth(key); + if (depth > 0) + return depth; + } } return 0; } - private int shouldExtendTile(ZoneSceneContext ctx, int tileExX, int tileExY) { - Sample sample = resolveSample(ctx); - if (sample == null) - return 0; + private int shouldExtendTile(ZoneSceneContext ctx, Sample sample, int tileExX, int tileExY) { if (isFlatTerrain(ctx)) - return baseShouldExtend(ctx, sample, tileExX, tileExY); + return shouldExtendFlatTerrainTile(ctx, tileExX, tileExY, sample.plane) ? 2 : 0; + int tileZ = sample.plane; if (!shouldRenderAtTile(ctx, tileExX, tileExY, tileZ)) return 0; @@ -1590,8 +1557,7 @@ private int shouldExtendTile(ZoneSceneContext ctx, int tileExX, int tileExY) { if (tileExX < 0 || tileExY < 0 || tileExX >= EXTENDED_SCENE_SIZE || tileExY >= EXTENDED_SCENE_SIZE) return 2; - if (ctx.tileIsWater != null && - ctx.tileIsWater[tileZ][tileExX][tileExY] && + if (ctx.isTileFlagSet(tileZ, tileExX, tileExY, TILE_WATER_FLAG) && (ctx.filledTiles[tileExX][tileExY] & (1 << tileZ)) != 0) return 0; @@ -1645,130 +1611,29 @@ private boolean hasVisibleLand(SceneContext ctx, Tile tile, int tileExX, int til return false; } - private void estimateTerrainForZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) { - long start = System.nanoTime(); - try { - Sample sample = resolveSample(ctx); - if (sample == null) - return; - - Area area = getArea(ctx); - assert area != null; - - ZonePlan plan = planZone(ctx, mzx, mzz); - ShellEdges shell = shellEdges(ctx, mzx, mzz); - int shellFaces = countTerrainShellFaces(ctx, area, sample, mzx, mzz, shell); - if (plan.tileCount == 0 && shellFaces == 0) - return; - - int faces = plan.tileCount * 2 + shellFaces; - zone.sizeO += faces; - zone.sizeF += faces; - } finally { - addTiming(ctx, start); - } - } - private int countTerrainShellFaces( ZoneSceneContext ctx, Area area, Sample sample, int mzx, int mzz, - ShellEdges shell - ) { - if (shell == null) - return 0; - - int faces = 0; - int x = shell.extra; - int z = shell.extent; - if (shell.west) - faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, -x, 0, 0, z) * 2; - if (shell.east) - faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, z, 0, z + x, z) * 2; - if (shell.north) - faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, 0, -x, z, 0) * 2; - if (shell.south) - faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, 0, z, z, z + x) * 2; - if (shell.west && shell.north) - faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, -x, -x, 0, 0) * 2; - if (shell.east && shell.north) - faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, z, -x, z + x, 0) * 2; - if (shell.west && shell.south) - faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, -x, z, 0, z + x) * 2; - if (shell.east && shell.south) - faces += countShellStripTiles(ctx, area, mzx, mzz, sample.plane, z, z, z + x, z + x) * 2; - return faces; - } - - private void uploadTerrainForZone( - ZoneSceneContext ctx, - Zone zone, - int mzx, - int mzz, - GpuIntBuffer vb, - GpuIntBuffer fb - ) { - long start = System.nanoTime(); - try { - Sample sample = resolveSample(ctx); - if (sample == null) - return; - - Area area = getArea(ctx); - assert area != null; - - int posBefore = vb.position(); - ZonePlan plan = planZone(ctx, mzx, mzz); - if (plan.tileCount > 0) { - int[][] cornerHeights = buildTerrainZoneCornerHeights(ctx, area, sample, mzx, mzz); - uploadTerrainZoneGrid(ctx, sample, plan, cornerHeights, mzx, mzz, vb, fb); - } - - uploadTerrainShell(ctx, area, sample, mzx, mzz, vb, fb); - - if (vb.position() > posBefore) - zone.hasWater = true; - } finally { - addTiming(ctx, start); - } - } - - private int[][] buildTerrainZoneCornerHeights( - ZoneSceneContext ctx, - Area area, - Sample sample, - int mzx, - int mzz + ShellEdges shell, + int[] clipBounds ) { - int[][] heights = new int[CHUNK_SIZE + 1][CHUNK_SIZE + 1]; - 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] = sample.flatHeight; - } else { - heights[vx][vz] = resolveCornerSurfaceHeight(ctx, sample, aabb, worldPos[0], worldPos[1]); - } - } - } - return heights; + 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 uploadTerrainZoneGrid( + private void uploadTerrainZoneTiles( + VertexWriteCache.Collection writeCache, ZoneSceneContext ctx, Sample sample, ZonePlan plan, int[][] cornerHeights, int mzx, - int mzz, - GpuIntBuffer vb, - GpuIntBuffer fb + int mzz ) { int baseTileX = (mzx << 3) - ctx.sceneOffset; int baseTileY = (mzz << 3) - ctx.sceneOffset; @@ -1790,64 +1655,13 @@ private void uploadTerrainZoneGrid( int[] nwWorld = ctx.sceneToWorld(baseTileX + xoff, baseTileY + zoff + 1, sample.plane); int[] neWorld = ctx.sceneToWorld(baseTileX + xoff + 1, baseTileY + zoff + 1, sample.plane); - uploadTerrainTile(ctx, sample, vb, fb, baseX, baseZ, sw, se, nw, ne, swWorld, seWorld, nwWorld, neWorld); + uploadTerrainTile(writeCache, ctx, sample, baseX, baseZ, sw, se, nw, ne, swWorld, seWorld, nwWorld, neWorld); } } } - private void uploadTerrainShell( - ZoneSceneContext ctx, - Area area, - Sample sample, - int mzx, - int mzz, - GpuIntBuffer vb, - GpuIntBuffer fb - ) { - ShellEdges shell = shellEdges(ctx, mzx, mzz); - if (shell == null) - return; - - int x = shell.extra; - int z = shell.extent; - if (shell.west) - uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, -x, 0, 0, z); - if (shell.east) - uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, z, 0, z + x, z); - if (shell.north) - uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, 0, -x, z, 0); - if (shell.south) - uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, 0, z, z, z + x); - if (shell.west && shell.north) - uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, -x, -x, 0, 0); - if (shell.east && shell.north) - uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, z, -x, z + x, 0); - if (shell.west && shell.south) - uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, -x, z, 0, z + x); - if (shell.east && shell.south) - uploadTerrainShellStrip(ctx, area, sample, mzx, mzz, vb, fb, z, z, z + x, z + x); - } - - private void uploadTerrainShellStrip( - ZoneSceneContext ctx, - Area area, - Sample sample, - int mzx, - int mzz, - GpuIntBuffer vb, - GpuIntBuffer fb, - int minLocalX, - int minLocalZ, - int maxLocalX, - int maxLocalZ - ) { - int[] bounds = clipLocal(ctx, area, mzx, mzz, sample.plane, minLocalX, minLocalZ, maxLocalX, maxLocalZ); - if (bounds == null) - return; - uploadFlatTilesInBounds(ctx, sample, mzx, mzz, bounds[0], bounds[1], bounds[2], bounds[3], sample.flatHeight, vb, fb); - } - private void uploadFlatTilesInBounds( + VertexWriteCache.Collection writeCache, ZoneSceneContext ctx, Sample sample, int mzx, @@ -1856,9 +1670,7 @@ private void uploadFlatTilesInBounds( int minZ, int maxX, int maxZ, - int height, - GpuIntBuffer vb, - GpuIntBuffer fb + int height ) { int baseTileX = (mzx << 3) - ctx.sceneOffset; int baseTileY = (mzz << 3) - ctx.sceneOffset; @@ -1882,7 +1694,7 @@ private void uploadFlatTilesInBounds( 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(ctx, sample, vb, fb, baseX, baseZ, height, height, height, height, swWorld, seWorld, nwWorld, neWorld); + uploadTerrainTile(writeCache, ctx, sample, baseX, baseZ, height, height, height, height, swWorld, seWorld, nwWorld, neWorld); } } } @@ -1894,10 +1706,9 @@ private Material resolveMaterial(Sample sample, int[] worldPos) { } private void uploadTerrainTile( + VertexWriteCache.Collection writeCache, ZoneSceneContext ctx, Sample sample, - GpuIntBuffer vb, - GpuIntBuffer fb, int baseX, int baseZ, int sw, @@ -1910,9 +1721,11 @@ private void uploadTerrainTile( int[] neWorld ) { if (sample.isWaterSurface()) { - putWaterSurface( - vb, fb, sample.waterType, sample.plane, - baseX, baseZ, baseX + LOCAL_TILE_SIZE, baseZ + LOCAL_TILE_SIZE, sw + putFlatQuad( + writeCache, + sample.waterType, sample.plane, + baseX, baseZ, baseX + LOCAL_TILE_SIZE, baseZ + LOCAL_TILE_SIZE, + sw, 127, 0 ); return; } @@ -1926,7 +1739,7 @@ private void uploadTerrainTile( float uvy = fract(swWorld[1]); putTriangle( - vb, fb, + writeCache, sample.neColor, sample.nwColor, sample.seColor, neMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), nwMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), @@ -1937,7 +1750,7 @@ private void uploadTerrainTile( baseX + LOCAL_TILE_SIZE, se, baseZ, fract(seWorld[0]), uvy - 1 ); putTriangle( - vb, fb, + writeCache, sample.swColor, sample.seColor, sample.nwColor, swMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), seMaterial.packMaterialData(ModelOverride.NONE, UvType.GEOMETRY, false), @@ -1948,43 +1761,4 @@ private void uploadTerrainTile( baseX, nw, baseZ + LOCAL_TILE_SIZE, uvx - 1, uvy ); } - - private static void buildBoundarySurfaceHeights( - ZoneSceneContext ctx, - Area area, - int plane, - int flatHeight, - HashMap surfaceHeights - ) { - 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++) { - sampleBoundaryVertex(ctx, plane, x, aabb.minY, flatHeight, surfaceHeights); - sampleBoundaryVertex(ctx, plane, x, aabb.maxY + 1, flatHeight, surfaceHeights); - } - for (int y = aabb.minY + 1; y <= aabb.maxY; y++) { - sampleBoundaryVertex(ctx, plane, aabb.minX, y, flatHeight, surfaceHeights); - sampleBoundaryVertex(ctx, plane, aabb.maxX + 1, y, flatHeight, surfaceHeights); - } - } - } - - private static void sampleBoundaryVertex( - ZoneSceneContext ctx, - int plane, - int boundaryX, - int boundaryY, - int flatHeight, - HashMap surfaceHeights - ) { - long key = Sample.packWorldVertex(boundaryX, boundaryY); - surfaceHeights.putIfAbsent( - key, - cornerSurfaceHeightAtWorldVertex(ctx, plane, boundaryX, boundaryY, flatHeight) - ); - } } diff --git a/src/main/java/rs117/hd/renderer/zone/SceneManager.java b/src/main/java/rs117/hd/renderer/zone/SceneManager.java index bc2263349e..b6f77d7c5a 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneManager.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneManager.java @@ -21,7 +21,6 @@ import net.runelite.client.eventbus.Subscribe; import rs117.hd.HdPlugin; import rs117.hd.HdPluginConfig; -import rs117.hd.renderer.zone.HorizonExtender; import rs117.hd.opengl.uniforms.UBOWorldViews; import rs117.hd.overlays.FrameTimer; import rs117.hd.overlays.Timer; @@ -772,8 +771,6 @@ public void swapScene(Scene scene) { checkGLErrors(); root.sceneSwapTime = sw.elapsed(TimeUnit.NANOSECONDS); log.debug("swapScene time: {}", sw); - if (root.sceneContext != null) - horizonExtender.logSceneLoadTiming(root.sceneContext); } private void loadSubScene(WorldView worldView, Scene scene) { 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/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 b397051c36..72ce6721df 100644 --- a/src/main/java/rs117/hd/scene/SceneContext.java +++ b/src/main/java/rs117/hd/scene/SceneContext.java @@ -7,7 +7,6 @@ import javax.annotation.Nullable; import net.runelite.api.*; import net.runelite.api.coords.*; -import rs117.hd.HdPluginConfig; import rs117.hd.renderer.zone.HorizonExtender; import rs117.hd.scene.areas.AABB; import rs117.hd.scene.areas.Area; @@ -63,20 +62,19 @@ public class SceneContext { public boolean fillGaps; public boolean enableHorizonTiles; - /** Nanoseconds spent on horizon tile work during the current scene load. */ - public long horizonTileNs; - @Nullable public HorizonExtender.Sample horizonTileSample; @Nullable public boolean[][] horizonTileMask; + @Nullable + public byte[][][] underwaterDepthLevels; + @Nullable + public Area horizonTileArea; public boolean isInChambersOfXeric; public boolean isInHouse; @Nullable public Area currentArea; - @Nullable - public Area horizonTileArea; public Area[] possibleAreas = new Area[0]; public byte[][] filledTiles = new byte[EXTENDED_SCENE_SIZE][EXTENDED_SCENE_SIZE]; public byte[] tileFlags; @@ -107,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 79c5bcd0a3..b9c05b4b75 100644 --- a/src/main/java/rs117/hd/scene/areas/Area.java +++ b/src/main/java/rs117/hd/scene/areas/Area.java @@ -19,10 +19,8 @@ public class Area { public String name; public boolean hideOtherAreas; public boolean fillGaps = true; - @SerializedName(value = "horizonTileReference", alternate = { "extendWaterReferenceTile" }) - public int[] horizonTileReference; - /** When true, extend flat land instead of water (no procgen patching). */ + public int[] horizonTileReference; public boolean horizonFlatTerrain; public String[] areas; @@ -53,10 +51,6 @@ public Area(String name, int x1, int y1, int x2, int y2) { aabbs = new AABB[] { new AABB(x1, y1, x2, y2) }; } - public boolean hasHorizonTiles() { - return horizonTileReference != null && horizonTileReference.length >= 2; - } - public void normalize() { if (normalized) return; @@ -258,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; From 8fa710020465f6c2bced1aa2037d40fe0057f1f6 Mon Sep 17 00:00:00 2001 From: Mark7625 <72366279+Mark7625@users.noreply.github.com> Date: Sun, 7 Jun 2026 09:50:06 +0100 Subject: [PATCH 4/4] Fixes When coming from an area with horizon tile it will always invalidate now --- .../hd/renderer/zone/HorizonExtender.java | 129 ++++++------------ .../rs117/hd/renderer/zone/SceneManager.java | 20 ++- src/main/resources/rs117/hd/scene/areas.json | 7 +- 3 files changed, 64 insertions(+), 92 deletions(-) diff --git a/src/main/java/rs117/hd/renderer/zone/HorizonExtender.java b/src/main/java/rs117/hd/renderer/zone/HorizonExtender.java index d033cb6dc6..8742afad44 100644 --- a/src/main/java/rs117/hd/renderer/zone/HorizonExtender.java +++ b/src/main/java/rs117/hd/renderer/zone/HorizonExtender.java @@ -234,14 +234,20 @@ private Reference(int worldX, int worldY, int plane, int flatHeight, int texture } public static boolean isEnabled(SceneContext ctx) { - if (!ctx.enableHorizonTiles) + 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; - Area area = getArea(ctx); - return ctx.sceneBase != null && area != null && area.hasHorizonTiles(); + return getPlayArea(ctx) == null; } public static boolean shouldPatchWaterAtTile(SceneContext ctx, int tileExX, int tileExY, int plane) { - Area area = getArea(ctx); + Area area = getPlayArea(ctx); if (area != null && area.horizonFlatTerrain) return false; if (!isEnabled(ctx) || plane != getPlane(ctx)) @@ -254,13 +260,13 @@ public static boolean shouldPatchWaterAtTile(SceneContext ctx, int tileExX, int return mask[tileExX][tileExY]; Tile tile = ctx.scene.getExtendedTiles()[plane][tileExX][tileExY]; - return isWithinLoadedRange(ctx, tileExX, tileExY) && - isOutsideAreaBounds(ctx, tileExX, tileExY, plane) && + return isWithinGpuRange(ctx, tileExX, tileExY) && + isOutsidePlayAreaBounds(ctx, tileExX, tileExY, plane) && isEligiblePatchTile(tile); } private static boolean isFlatTerrain(SceneContext ctx) { - Area area = getArea(ctx); + Area area = getPlayArea(ctx); return area != null && area.horizonFlatTerrain; } @@ -275,8 +281,21 @@ private static Area getArea(SceneContext ctx) { 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 = getArea(ctx); + Area area = getReferenceArea(ctx); if (area == null || area.horizonTileReference == null) return 0; int[] ref = area.horizonTileReference; @@ -303,16 +322,6 @@ private static int gpuExtraLocalExtent(SceneContext ctx) { return GPU_EXTRA_CHUNKS * CHUNK_SIZE * LOCAL_TILE_SIZE; } - private static boolean isWithinLoadedRange(SceneContext ctx, int tileExX, int tileExY) { - if (ctx.sceneBase == null) - return true; - int tileX = tileExX - ctx.sceneOffset; - int tileY = tileExY - ctx.sceneOffset; - int min = loadedSceneMin(ctx); - int max = loadedSceneMax(ctx); - return tileX > min && tileY > min && tileX < max && tileY < max; - } - private static boolean isWithinGpuRange(SceneContext ctx, int tileExX, int tileExY) { if (ctx.sceneBase == null) return true; @@ -362,8 +371,8 @@ private static boolean isEligiblePatchTile(@Nullable Tile tile) { return tile == null || isHiddenGroundTile(tile); } - private static boolean isOutsideAreaBounds(SceneContext ctx, int tileExX, int tileExY, int plane) { - Area area = getArea(ctx); + 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); @@ -381,8 +390,8 @@ private static void buildPatchMask(SceneContext ctx, Tile[][][] tiles) { Tile[][] planeTiles = tiles[plane]; for (int x = 0; x < ctx.sizeX; ++x) { for (int y = 0; y < ctx.sizeZ; ++y) { - mask[x][y] = isWithinLoadedRange(ctx, x, y) && - isOutsideAreaBounds(ctx, x, y, plane) && + mask[x][y] = isWithinGpuRange(ctx, x, y) && + isOutsidePlayAreaBounds(ctx, x, y, plane) && isEligiblePatchTile(planeTiles[x][y]); } } @@ -394,7 +403,7 @@ private static boolean shouldRenderAtTile(SceneContext ctx, int tileExX, int til return false; if (!isWithinGpuRange(ctx, tileExX, tileExY)) return false; - return isOutsideAreaBounds(ctx, tileExX, tileExY, plane); + return isOutsidePlayAreaBounds(ctx, tileExX, tileExY, plane); } private static boolean shouldExtendFlatTerrainTile( @@ -407,12 +416,12 @@ private static boolean shouldExtendFlatTerrainTile( return false; if (tileExX < 0 || tileExY < 0 || tileExX >= EXTENDED_SCENE_SIZE || tileExY >= EXTENDED_SCENE_SIZE) { - if (!isOutsideAreaBounds(ctx, tileExX, tileExY, plane)) + if (!isOutsidePlayAreaBounds(ctx, tileExX, tileExY, plane)) return false; return isWithinGpuRange(ctx, tileExX, tileExY); } - if (isOutsideAreaBounds(ctx, tileExX, tileExY, plane)) + if (isOutsidePlayAreaBounds(ctx, tileExX, tileExY, plane)) return true; Tile tile = ctx.scene.getExtendedTiles()[plane][tileExX][tileExY]; @@ -846,7 +855,7 @@ private static int cornerSurfaceHeightAtWorldVertex( } public void prepareSceneTerrain(SceneContext ctx, Tile[][][] tiles) { - Area area = getArea(ctx); + Area area = getPlayArea(ctx); if (isFlatTerrain(ctx)) return; if (!isEnabled(ctx)) @@ -959,7 +968,7 @@ public void estimateForZone(ZoneSceneContext ctx, Zone zone, int mzx, int mzz) { ZonePlan plan = planZone(ctx, mzx, mzz, sample); ShellEdges shell = shellEdges(ctx, mzx, mzz); int shellFaces = terrain ? - countTerrainShellFaces(ctx, getArea(ctx), sample, mzx, mzz, shell, UPLOAD_SCRATCH.get().clipBounds) : + countTerrainShellFaces(ctx, getPlayArea(ctx), sample, mzx, mzz, shell, UPLOAD_SCRATCH.get().clipBounds) : countShellQuads(shell) * 2; if (plan.tileCount == 0 && shellFaces == 0) return; @@ -1025,7 +1034,7 @@ private void uploadForZone( if (sample == null) return; - Area area = getArea(ctx); + Area area = getPlayArea(ctx); assert area != null; int posBefore = vb.position(); @@ -1257,7 +1266,7 @@ maxX, se, minZ, fract(seWorld[0]), uvy - 1, } private static int resolveSurfaceAt(ZoneSceneContext ctx, Sample sample, int worldX, int worldY) { - Area area = getArea(ctx); + Area area = getPlayArea(ctx); if (area == null) return sample.flatHeight; AABB aabb = nearestAabb(area, worldX, worldY, sample.plane); @@ -1273,7 +1282,7 @@ private Sample resolveSample(ZoneSceneContext ctx) { if (ctx.horizonTileSample != null) return ctx.horizonTileSample; - Area area = getArea(ctx); + Area area = getReferenceArea(ctx); assert area != null; Reference ref = resolveReference(ctx, area); @@ -1550,65 +1559,7 @@ private int shouldExtendTile(ZoneSceneContext ctx, Sample sample, int tileExX, i if (isFlatTerrain(ctx)) return shouldExtendFlatTerrainTile(ctx, tileExX, tileExY, sample.plane) ? 2 : 0; - int tileZ = sample.plane; - if (!shouldRenderAtTile(ctx, tileExX, tileExY, tileZ)) - return 0; - - if (tileExX < 0 || tileExY < 0 || tileExX >= EXTENDED_SCENE_SIZE || tileExY >= EXTENDED_SCENE_SIZE) - return 2; - - if (ctx.isTileFlagSet(tileZ, tileExX, tileExY, TILE_WATER_FLAG) && - (ctx.filledTiles[tileExX][tileExY] & (1 << tileZ)) != 0) - return 0; - - Tile tile = ctx.scene.getExtendedTiles()[tileZ][tileExX][tileExY]; - if (tile == null || isHiddenGroundTile(tile)) - return 2; - - if ((ctx.filledTiles[tileExX][tileExY] & (1 << tileZ)) != 0 && hasVisibleLand(ctx, tile, tileExX, tileExY, tileZ)) - return 0; - - return 2; - } - - private boolean hasVisibleLand(SceneContext ctx, Tile tile, int tileExX, int tileExY, int tileZ) { - if (tile.getBridge() != null) - tile = tile.getBridge(); - - SceneTilePaint paint = tile.getSceneTilePaint(); - if (paint != null && paint.getNeColor() != HIDDEN_HSL) { - int[] worldPos = ctx.extendedSceneToWorld(tileExX, tileExY, tileZ); - var override = tileOverrideManager.getOverride(ctx, tile, worldPos); - if (proceduralGenerator.seasonalWaterType(override, paint.getTexture()) != WaterType.NONE) - return false; - return true; - } - - SceneTileModel model = tile.getSceneTileModel(); - if (model == null) - return false; - - int[] worldPos = ctx.extendedSceneToWorld(tileExX, tileExY, tileZ); - int overlayId = OVERLAY_FLAG | ctx.scene.getOverlayIds()[tileZ][tileExX][tileExY]; - int underlayId = ctx.scene.getUnderlayIds()[tileZ][tileExX][tileExY]; - 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; - - return true; - } - return false; + return shouldRenderAtTile(ctx, tileExX, tileExY, sample.plane) ? 2 : 0; } private int countTerrainShellFaces( diff --git a/src/main/java/rs117/hd/renderer/zone/SceneManager.java b/src/main/java/rs117/hd/renderer/zone/SceneManager.java index b6f77d7c5a..c3f5aee648 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneManager.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneManager.java @@ -291,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; @@ -366,6 +368,14 @@ private Area resolveHorizonTileArea(SceneContext ctx) { 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) @@ -563,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) { @@ -598,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]; @@ -623,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 @@ -631,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/resources/rs117/hd/scene/areas.json b/src/main/resources/rs117/hd/scene/areas.json index 93b52992b5..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 }, {