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
},
{