diff --git a/src/main/java/rs117/hd/HdPluginConfig.java b/src/main/java/rs117/hd/HdPluginConfig.java
index b24ab75201..b6593ff62f 100644
--- a/src/main/java/rs117/hd/HdPluginConfig.java
+++ b/src/main/java/rs117/hd/HdPluginConfig.java
@@ -53,6 +53,7 @@
import rs117.hd.config.TextureResolution;
import rs117.hd.config.UIScalingMode;
import rs117.hd.config.VanillaShadowMode;
+import rs117.hd.config.SkyboxTheme;
import static rs117.hd.HdPlugin.MAX_DISTANCE;
import static rs117.hd.HdPlugin.MAX_FOG_DEPTH;
@@ -797,12 +798,36 @@ default boolean characterDisplacement() {
default boolean pohThemeEnvironments() { return true; }
+ /*====== 3D Skybox settings ======*/
+
+ @ConfigSection(
+ name = "3D Skybox",
+ description = "Settings for high-resolution 3D panoramic skybox rendering templates.",
+ position = 4,
+ closedByDefault = true
+ )
+ String skyboxSettings = "skyboxSettings";
+
+ String KEY_SELECTED_SKYBOX_THEME = "selectedSkyboxTheme";
+ @ConfigItem(
+ keyName = KEY_SELECTED_SKYBOX_THEME,
+ name = "Skybox Style",
+ description = "Select the panoramic 3D skybox style.
Select 'None' to use default 117 skybox.",
+ position = 0,
+ section = skyboxSettings
+ )
+ default SkyboxTheme selectedSkyboxTheme()
+ {
+ return SkyboxTheme.PARTLY_CLOUDY;
+ }
+
+
/*====== Miscellaneous settings ======*/
@ConfigSection(
name = "Miscellaneous",
description = "Miscellaneous settings",
- position = 4,
+ position = 5,
closedByDefault = true
)
String miscellaneousSettings = "miscellaneousSettings";
@@ -956,7 +981,7 @@ default boolean windowsHdrCorrection() {
@ConfigSection(
name = "Legacy",
description = "Legacy options. If you dislike a change, you might find an option to change it back here.",
- position = 5,
+ position = 6,
closedByDefault = true
)
String legacySettings = "legacySettings";
@@ -1109,7 +1134,7 @@ default boolean legacyTzHaarReskin() {
@ConfigSection(
name = "Experimental",
description = "Experimental features - if you're experiencing issues you should consider disabling these.",
- position = 6,
+ position = 7,
closedByDefault = true
)
String experimentalSettings = "experimentalSettings";
diff --git a/src/main/java/rs117/hd/config/SkyboxTheme.java b/src/main/java/rs117/hd/config/SkyboxTheme.java
new file mode 100644
index 0000000000..4874ab1c54
--- /dev/null
+++ b/src/main/java/rs117/hd/config/SkyboxTheme.java
@@ -0,0 +1,22 @@
+package rs117.hd.config;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public enum SkyboxTheme {
+ NONE("None (Vanilla/117)", null),
+ CLEAR_DAY("Clear Day (Autumn Field)", "/rs117/hd/skybox3d/autumn_field_puresky_4k.png"),
+ CLOUDY("Cloudy Overcast (Soil)", "/rs117/hd/skybox3d/overcast_soil_puresky_4k.png"),
+ STARRY_NIGHT("Starry Night (Qwantani)", "/rs117/hd/skybox3d/qwantani_night_puresky_4k.png"),
+ PARTLY_CLOUDY("Partly Cloudy (Sunflowers)", "/rs117/hd/skybox3d/sunflowers_puresky_4k.png");
+
+ private final String name;
+ private final String resourcePath;
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java
index be35d19b5c..ffc1bea921 100644
--- a/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java
+++ b/src/main/java/rs117/hd/renderer/zone/ZoneRenderer.java
@@ -6,10 +6,10 @@
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
+ * list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
@@ -91,6 +91,13 @@ public class ZoneRenderer implements Renderer {
private static int TEXTURE_UNIT_COUNT = HdPlugin.TEXTURE_UNIT_COUNT;
public static final int TEXTURE_UNIT_TEXTURED_FACES = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++;
+ public static final int TEXTURE_UNIT_SKYBOX = GL_TEXTURE0 + TEXTURE_UNIT_COUNT++;
+ private int skyboxShaderProgramId;
+ private int customSkyboxTextureId;
+ private rs117.hd.config.SkyboxTheme loadedSkyboxTheme = rs117.hd.config.SkyboxTheme.NONE;
+ private int lastCameraYaw = -1;
+ private float continuousSkyboxYaw = 0f;
+
private static int UNIFORM_BLOCK_COUNT = HdPlugin.UNIFORM_BLOCK_COUNT;
public static final int UNIFORM_BLOCK_WORLD_VIEWS = UNIFORM_BLOCK_COUNT++;
@@ -233,6 +240,10 @@ public void initializeShaders(ShaderIncludes includes) throws ShaderException, I
sceneProgram.compile(includes);
fastShadowProgram.compile(includes);
detailedShadowProgram.compile(includes);
+
+ skyboxShaderProgramId = compileSkyboxShader();
+ // Pass the absolute resource path string to your loading method
+ customSkyboxTextureId = loadSkyboxTexture("/rs117/hd/skybox3d/sunflowers_puresky_4k.png");
}
@Override
@@ -287,7 +298,6 @@ public void preSceneDraw(
try {
WorldViewContext ctx = sceneManager.getContext(scene);
if (ctx == null || !sceneManager.isRoot(ctx) && ctx.isLoading) {
- // When triggering plugin restarts in rapid succession, it can end up in a state where no scene is loaded initially
if (scene.getWorldViewId() == WorldView.TOPLEVEL && client.getGameState() == GameState.LOGGED_IN)
clientThread.invokeLater(() -> client.setGameState(GameState.LOADING));
return;
@@ -332,7 +342,6 @@ private void preSceneDrawTopLevel(
scene.setDrawDistance(plugin.getDrawDistance());
- // Ensure that the previous frames commands have finished flushing
frameTimer.begin(Timer.DRAW_FLUSH);
glFlush();
frameTimer.end(Timer.DRAW_FLUSH);
@@ -363,7 +372,6 @@ private void preSceneDrawTopLevel(
if (plugin.orthographicProjection)
zoom *= ORTHOGRAPHIC_ZOOM;
- // Calculate the viewport dimensions before scaling in order to include the extra padding
sceneCamera.setOrthographic(plugin.orthographicProjection);
sceneCamera.setPosition(plugin.cameraPosition);
sceneCamera.setOrientation(plugin.cameraOrientation);
@@ -374,7 +382,6 @@ private void preSceneDrawTopLevel(
sceneCamera.setNearPlane(plugin.orthographicProjection ? -40000 : NEAR_PLANE);
sceneCamera.setZoom(zoom);
- // Calculate view matrix, view proj & inv matrix
boolean hasSceneCameraChanged = sceneCamera.isViewDirty() || sceneCamera.isProjDirty();
sceneCamera.getViewMatrix(plugin.viewMatrix);
sceneCamera.getViewProjMatrix(plugin.viewProjMatrix);
@@ -404,8 +411,8 @@ private void preSceneDrawTopLevel(
boolean hasDirectionalCameraChanged = directionalCamera.isViewDirty() || directionalCamera.isProjDirty();
if (plugin.configShadowsEnabled &&
- (hasSceneCameraChanged || hasDirectionalCameraChanged) &&
- !sceneCamera.isOrthographic()
+ (hasSceneCameraChanged || hasDirectionalCameraChanged) &&
+ !sceneCamera.isOrthographic()
) {
int shadowDrawDistance = 90 * LOCAL_TILE_SIZE;
@@ -417,7 +424,6 @@ private void preSceneDrawTopLevel(
add(sceneCenter, sceneCenter, corner);
divide(sceneCenter, sceneCenter, (float) volumeCorners.length);
- // Reset position before transforming points
directionalCamera.setPosition(0, 0, 0);
float minX = Float.POSITIVE_INFINITY, maxX = Float.NEGATIVE_INFINITY;
@@ -439,24 +445,19 @@ private void preSceneDrawTopLevel(
maxZ = max(maxZ, corner[2]);
}
- // Offset the Directional Camera by the radius of the scene
float[] directionalFwd = directionalCamera.getForwardDirection();
multiply(directionalFwd, directionalFwd, radius);
add(sceneCenter, sceneCenter, directionalFwd);
- // Calculate directional size from the AABB of the scene frustum corners
- // Then snap to the nearest multiple of `LOCAL_HALF_TILE_SIZE` to prevent shimmering
int directionalSize = (int) max(abs(maxY - minY), abs(maxX - minX), abs(maxZ - minZ));
directionalSize = Math.round(directionalSize / (float) LOCAL_HALF_TILE_SIZE) * LOCAL_HALF_TILE_SIZE;
- directionalSize = max(8000, directionalSize); // Clamp the size to prevent going too small at reduced draw distances
+ directionalSize = max(8000, directionalSize);
- // Ignore directional size changes below the change threshold to avoid inducing shimmering
int previousDirectionalSize = directionalCamera.getViewportWidth();
- float changeThreshold = previousDirectionalSize * 0.05f; // 10% of the previous directional size
+ float changeThreshold = previousDirectionalSize * 0.05f;
if (abs(directionalSize - previousDirectionalSize) < changeThreshold)
directionalSize = previousDirectionalSize;
- // Snap Position to Shadow Texel Grid to prevent shimmering
directionalCamera.transformPoint(sceneCenter, sceneCenter);
float texelSize = (float) directionalSize / plugin.shadowMapResolution;
@@ -485,7 +486,6 @@ private void preSceneDrawTopLevel(
plugin.uboGlobal.invProjectionMatrix.set(plugin.invViewProjMatrix);
if (plugin.configDynamicLights != DynamicLights.NONE) {
- // Update lights UBO
assert ctx.sceneContext.numVisibleLights <= UBOLights.MAX_LIGHTS;
frameTimer.begin(Timer.UPDATE_LIGHTS);
@@ -507,10 +507,9 @@ private void preSceneDrawTopLevel(
plugin.uboLights.setLight(i, lightPosition, lightColor);
if (plugin.configTiledLighting) {
- // Pre-calculate the view space position of the light, to save having to do the multiplication in the culling shader
lightPosition[3] = 1.0f;
Mat4.mulVec(lightPosition, plugin.viewMatrix, lightPosition);
- lightPosition[3] = lightRadiusSq; // Restore lightRadiusSq
+ lightPosition[3] = lightRadiusSq;
plugin.uboLightsCulling.setLight(i, lightPosition, lightColor);
}
}
@@ -522,7 +521,6 @@ private void preSceneDrawTopLevel(
}
}
- // Upon logging in, the client will draw some frames with zero geometry before it hides the login screen
if (client.getGameState().getState() >= GameState.LOGGED_IN.getState())
plugin.hasLoggedIn = true;
@@ -589,7 +587,6 @@ private void preSceneDrawTopLevel(
environmentManager.currentGroundFogOpacity :
0);
- // Lights & lightning
plugin.uboGlobal.lightningBrightness.set(environmentManager.getLightningBrightness());
plugin.uboGlobal.saturation.set(config.saturation() / 100f);
@@ -609,7 +606,6 @@ private void preSceneDrawTopLevel(
plugin.uboGlobal.upload();
- // Reset buffers for the next frame
indirectDrawCmdsStaging.clear();
sceneCmd.reset();
directionalCmd.reset();
@@ -649,13 +645,11 @@ private void postDrawTopLevel() {
sceneFboValid = true;
- // Upload world views before rendering
uboWorldViews.upload();
if (eboAlphaWriter != null)
eboAlphaWriter.flush();
- // Scene draw state to apply before all recorded commands
if (indirectDrawCmdsStaging.position() > 0) {
indirectDrawCmdsStaging.flip();
indirectDrawCmds.orphan();
@@ -666,7 +660,6 @@ private void postDrawTopLevel() {
frameTimer.begin(Timer.RENDER_FRAME);
shouldRenderScene = true;
- // TODO: Add proper support for stat tracking to the FrameTimer or elsewhere
plugin.drawnDynamicRenderableCount += modelStreamingManager.getDrawnDynamicRenderableCount();
checkGLErrors();
@@ -713,7 +706,6 @@ private void directionalShadowPass() {
environmentManager.currentDirectionalStrength > 0;
if (shouldRenderShadows || shouldClearShadowFbo) {
- // Render to the shadow depth map
renderState.framebuffer.set(GL_FRAMEBUFFER, plugin.fboShadowMap);
renderState.viewport.set(0, 0, plugin.shadowMapResolution, plugin.shadowMapResolution);
renderState.apply();
@@ -747,8 +739,8 @@ private void directionalShadowPass() {
private void scenePass() {
sceneProgram.use();
-
frameTimer.begin(Timer.DRAW_SCENE);
+
renderState.framebuffer.set(GL_DRAW_FRAMEBUFFER, plugin.fboScene);
if (plugin.msaaSamples > 1) {
renderState.enable.set(GL_MULTISAMPLE);
@@ -759,43 +751,100 @@ private void scenePass() {
renderState.ido.set(indirectDrawCmds.id);
renderState.apply();
- // Clear scene
frameTimer.begin(Timer.CLEAR_SCENE);
-
float[] fogColor = ColorUtils.linearToSrgb(environmentManager.currentFogColor);
float[] gammaCorrectedFogColor = pow(fogColor, plugin.getGammaCorrection());
- glClearColor(
- gammaCorrectedFogColor[0],
- gammaCorrectedFogColor[1],
- gammaCorrectedFogColor[2],
- 1f
- );
+ glClearColor(gammaCorrectedFogColor[0], gammaCorrectedFogColor[1], gammaCorrectedFogColor[2], 1f);
glClearDepth(0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
frameTimer.end(Timer.CLEAR_SCENE);
- frameTimer.begin(Timer.RENDER_SCENE);
+ // --- DYNAMIC SKYBOX THEME CONFIGURATION TRACKER ---
+ rs117.hd.config.SkyboxTheme currentTheme = config.selectedSkyboxTheme();
+ boolean hasActiveSkyboxTheme = currentTheme != rs117.hd.config.SkyboxTheme.NONE;
+
+ if (hasActiveSkyboxTheme && environmentManager.isOverworld()) {
+
+ // INTERCEPT: Check if the user changed the dropdown selection mid-game
+ if (currentTheme != loadedSkyboxTheme) {
+ // Free the old texture handle from GPU memory if it exists
+ if (customSkyboxTextureId != 0) {
+ glDeleteTextures(customSkyboxTextureId);
+ customSkyboxTextureId = 0;
+ }
+
+ // Stream the new asset path specified by the active enum selection
+ customSkyboxTextureId = loadSkyboxTexture(currentTheme.getResourcePath());
+ loadedSkyboxTheme = currentTheme;
+ }
+
+ // Only run drawing commands if the selected asset file loaded successfully
+ if (customSkyboxTextureId != 0) {
+ glDisable(GL_DEPTH_TEST);
+ glDepthMask(false);
+ glUseProgram(skyboxShaderProgramId);
+
+ // [ Your original texture coordinate math stays exactly here ]
+ float horizontalSpeed = 1.0f;
+ float verticalSpeed = 1.0f;
+ int currentYaw = client.getCameraYaw();
+ if (lastCameraYaw == -1) {
+ lastCameraYaw = currentYaw;
+ continuousSkyboxYaw = currentYaw;
+ }
+
+ int deltaYaw = currentYaw - lastCameraYaw;
+ if (deltaYaw > 1024) {
+ deltaYaw -= 2048;
+ } else if (deltaYaw < -1024) {
+ deltaYaw += 2048;
+ }
+
+ continuousSkyboxYaw += (deltaYaw * horizontalSpeed);
+ continuousSkyboxYaw = (continuousSkyboxYaw % 2048.0f + 2048.0f) % 2048.0f;
+ lastCameraYaw = currentYaw;
+
+ float yawRadians = (float) ((continuousSkyboxYaw * 2.0 * Math.PI / 2048.0));
+ float pitchRadians = (float) ((client.getCameraPitch() * 2.0 * Math.PI / 2048.0) * verticalSpeed);
+
+ float[] projMatrix = sceneCamera.getProjectionMatrix();
+
+ glUniform1f(glGetUniformLocation(skyboxShaderProgramId, "cameraYaw"), yawRadians);
+ glUniform1f(glGetUniformLocation(skyboxShaderProgramId, "cameraPitch"), pitchRadians);
+ glUniformMatrix4fv(glGetUniformLocation(skyboxShaderProgramId, "projectionMatrix"), false, projMatrix);
+
+ glActiveTexture(GL_TEXTURE8);
+ glBindTexture(GL_TEXTURE_2D, customSkyboxTextureId);
+ glUniform1i(glGetUniformLocation(skyboxShaderProgramId, "skyboxTexture"), 8);
+
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+
+ glEnable(GL_DEPTH_TEST);
+ glDepthMask(true);
+ sceneProgram.use();
+ } else {
+ sceneProgram.use();
+ }
+ } else {
+ sceneProgram.use();
+ }
+
+ frameTimer.begin(Timer.RENDER_SCENE);
renderState.enable.set(GL_BLEND);
renderState.enable.set(GL_CULL_FACE);
renderState.enable.set(GL_DEPTH_TEST);
renderState.depthFunc.set(GL_GEQUAL);
renderState.blendFunc.set(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE);
- // Render the scene
sceneCmd.execute();
-
- // TODO: Filler tiles
frameTimer.end(Timer.RENDER_SCENE);
glBindVertexArray(0);
-
- // Done rendering the scene
renderState.disable.set(GL_BLEND);
renderState.disable.set(GL_CULL_FACE);
renderState.disable.set(GL_DEPTH_TEST);
renderState.apply();
-
frameTimer.end(Timer.DRAW_SCENE);
}
@@ -926,7 +975,6 @@ public void drawZoneAlpha(Projection entityProjection, Scene scene, int level, i
final boolean hasAlpha = z.sizeA != 0 || !z.alphaModels.isEmpty();
if (hasAlpha) {
final int offset = ctx.sceneContext.sceneOffset >> 3;
- // Only sort if the alpha will be directly visible, since shadows don't require sorting
if (level == 0 && (!sceneManager.isRoot(ctx) || z.inSceneFrustum))
z.alphaSort(zx - offset, zz - offset, sceneCamera);
@@ -978,20 +1026,16 @@ public void drawPass(Projection projection, Scene scene, int pass) {
if (sceneManager.isRoot(ctx))
frameTimer.end(Timer.UNMAP_ROOT_CTX);
- // Draw opaque
ctx.drawAll(VAO_OPAQUE, ctx.vaoSceneCmd);
ctx.drawAll(VAO_OPAQUE, ctx.vaoDirectionalCmd);
ctx.drawAll(VAO_PLAYER, ctx.vaoDirectionalCmd);
- // Draw shadow-only models
ctx.drawAll(VAO_SHADOW, ctx.vaoDirectionalCmd);
- // Draw players with sorted alpha, without writing depth
ctx.vaoSceneCmd.DepthMask(false);
ctx.drawAll(VAO_PLAYER, ctx.vaoSceneCmd);
ctx.vaoSceneCmd.DepthMask(true);
- // Redraw players, this time only writing depth, for correct ordering with the background
ctx.vaoSceneCmd.ColorMask(false, false, false, false);
ctx.drawAll(VAO_PLAYER, ctx.vaoSceneCmd);
ctx.vaoSceneCmd.ColorMask(true, true, true, true);
@@ -1066,8 +1110,6 @@ public void draw(int overlayColor) {
try {
plugin.prepareInterfaceTexture();
} catch (Exception ex) {
- // Fixes: https://github.com/runelite/runelite/issues/12930
- // Gracefully Handle loss of opengl buffers and context
log.warn("prepareInterfaceTexture exception", ex);
plugin.restartPlugin();
return;
@@ -1083,7 +1125,6 @@ public void draw(int overlayColor) {
if (sceneFboValid && plugin.sceneResolution != null && plugin.sceneViewport != null) {
glBindFramebuffer(GL_READ_FRAMEBUFFER, plugin.fboScene);
if (plugin.fboSceneResolve != 0) {
- // Blit from the scene FBO to the multisample resolve FBO
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, plugin.fboSceneResolve);
glBlitFramebuffer(
0, 0, plugin.sceneResolution[0], plugin.sceneResolution[1],
@@ -1093,7 +1134,6 @@ public void draw(int overlayColor) {
glBindFramebuffer(GL_READ_FRAMEBUFFER, plugin.fboSceneResolve);
}
- // Blit from the resolved FBO to the default FBO
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, plugin.awtContext.getFramebuffer(false));
glBlitFramebuffer(
0,
@@ -1127,12 +1167,9 @@ public void draw(int overlayColor) {
frameTimer.end(Timer.SWAP_BUFFERS);
drawManager.processDrawComplete(plugin::screenshot);
} catch (RuntimeException ex) {
- // this is always fatal
if (!plugin.canvas.isValid()) {
- // this might be AWT shutting down on VM shutdown, ignore it
return;
}
-
log.error("Unable to swap buffers:", ex);
}
@@ -1152,8 +1189,6 @@ public void draw(int overlayColor) {
public void onGameStateChanged(GameStateChanged gameStateChanged) {
GameState state = gameStateChanged.getGameState();
if (state.getState() < GameState.LOADING.getState()) {
- // this is to avoid scene fbo blit when going from =loading,
- // but keep it when doing >loading to loading
sceneFboValid = false;
}
}
@@ -1218,4 +1253,126 @@ public void swapScene(Scene scene) {
plugin.stopPlugin();
}
}
-}
+
+ private int compileSkyboxShader() {
+ int vShader = glCreateShader(GL_VERTEX_SHADER);
+ int fShader = glCreateShader(GL_FRAGMENT_SHADER);
+
+ String vSrc = "#version 330\n" +
+ "out vec2 screenUV;\n" +
+ "void main() {\n" +
+ " vec2 pos[3] = vec2[3](vec2(-1, -1), vec2(3, -1), vec2(-1, 3));\n" +
+ " gl_Position = vec4(pos[gl_VertexID], 1.0, 1.0);\n" +
+ " screenUV = pos[gl_VertexID] * 0.5 + 0.5;\n" +
+ "}";
+
+ String fSrc = "#version 330\n" +
+ "in vec2 screenUV;\n" +
+ "out vec4 FragColor;\n" +
+ "uniform sampler2D skyboxTexture;\n" +
+ "uniform float cameraYaw;\n" +
+ "uniform float cameraPitch;\n" +
+ "uniform mat4 projectionMatrix;\n" +
+ "void main() {\n" +
+ " float xClip = screenUV.x * 2.0 - 1.0;\n" +
+ " float yClip = screenUV.y * 2.0 - 1.0;\n" +
+ " \n" +
+ " vec3 viewDir = vec3(\n" +
+ " xClip / projectionMatrix[0][0],\n" +
+ " yClip / projectionMatrix[1][1],\n" +
+ " -1.0\n" +
+ " );\n" +
+ " \n" +
+ " float cosP = cos(cameraPitch);\n" +
+ " float sinP = sin(cameraPitch);\n" +
+ " vec3 pitchedDir = vec3(\n" +
+ " viewDir.x,\n" +
+ " viewDir.y * cosP - viewDir.z * sinP,\n" +
+ " viewDir.y * sinP + viewDir.z * cosP\n" +
+ " );\n" +
+ " \n" +
+ " float cosY = cos(cameraYaw);\n" +
+ " float sinY = sin(cameraYaw);\n" +
+ " vec3 finalDir = vec3(\n" +
+ " pitchedDir.x * cosY + pitchedDir.z * sinY,\n" +
+ " pitchedDir.y,\n" +
+ " -pitchedDir.x * sinY + pitchedDir.z * cosY\n" +
+ " );\n" +
+ " \n" +
+ " vec3 dir = normalize(finalDir);\n" +
+ " \n" +
+ " // --- APPLY VERTICAL SHIFT HERE ---\n" +
+ " // Slightly offsetting the Y lookup pushes the panorama horizon downwards\n" +
+ " float shiftedY = dir.y - 0.12;\n" +
+ " \n" +
+ " // Map UV coordinates using our shifted position variable\n" +
+ " vec2 uv = vec2(atan(dir.x, -dir.z) / (2.0 * 3.14159265) + 0.5, 1.0 - (acos(clamp(shiftedY, -1.0, 1.0)) / 3.14159265));\n" +
+ " \n" +
+ " FragColor = texture(skyboxTexture, uv);\n" +
+ "}";
+
+ glShaderSource(vShader, vSrc);
+ glCompileShader(vShader);
+ if (glGetShaderi(vShader, GL_COMPILE_STATUS) == GL_FALSE) {
+ log.error("Vertex Shader Error: " + glGetShaderInfoLog(vShader));
+ }
+
+ glShaderSource(fShader, fSrc);
+ glCompileShader(fShader);
+ if (glGetShaderi(fShader, GL_COMPILE_STATUS) == GL_FALSE) {
+ log.error("Fragment Shader Error: " + glGetShaderInfoLog(fShader));
+ }
+
+ int program = glCreateProgram();
+ glAttachShader(program, vShader);
+ glAttachShader(program, fShader);
+ glLinkProgram(program);
+ return program;
+ }
+
+ private int loadSkyboxTexture(String resourcePath) {
+ int texId = 0;
+ try {
+ java.io.InputStream is = getClass().getResourceAsStream(resourcePath);
+ if (is == null) {
+ log.error("IMAGE IS NULL: Could not find resource at: " + resourcePath);
+ return 0;
+ }
+
+ // Fully qualified variable initialization:
+ java.awt.image.BufferedImage img = javax.imageio.ImageIO.read(is);
+ is.close();
+
+ int width = img.getWidth();
+ int height = img.getHeight();
+ int[] pixels = new int[width * height];
+ img.getRGB(0, 0, width, height, pixels, 0, width);
+
+ java.nio.ByteBuffer buffer = org.lwjgl.BufferUtils.createByteBuffer(width * height * 4);
+ for(int y = 0; y < height; y++) {
+ for(int x = 0; x < width; x++) {
+ int pixel = pixels[y * width + x];
+ buffer.put((byte) ((pixel >> 16) & 0xFF));
+ buffer.put((byte) ((pixel >> 8) & 0xFF));
+ buffer.put((byte) (pixel & 0xFF));
+ buffer.put((byte) ((pixel >> 24) & 0xFF));
+ }
+ }
+ buffer.flip();
+
+ texId = glGenTextures();
+ glBindTexture(GL_TEXTURE_2D, texId);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
+ return texId;
+ } catch (Exception e) {
+ log.error("Failed to load custom skybox texture from resources.", e);
+ return 0;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/rs117/hd/scene/EnvironmentManager.java b/src/main/java/rs117/hd/scene/EnvironmentManager.java
index b19eb18f64..cf6ff3e402 100644
--- a/src/main/java/rs117/hd/scene/EnvironmentManager.java
+++ b/src/main/java/rs117/hd/scene/EnvironmentManager.java
@@ -500,4 +500,6 @@ public boolean isUnderwater() {
public boolean allowRoofShadows() {
return currentEnvironment.allowRoofShadows;
}
+
+ public boolean isOverworld() { return currentEnvironment.isOverworld; }
}
diff --git a/src/main/resources/rs117/hd/skybox3d/autumn_field_puresky_4k.png b/src/main/resources/rs117/hd/skybox3d/autumn_field_puresky_4k.png
new file mode 100644
index 0000000000..2d0d22edbb
Binary files /dev/null and b/src/main/resources/rs117/hd/skybox3d/autumn_field_puresky_4k.png differ
diff --git a/src/main/resources/rs117/hd/skybox3d/overcast_soil_puresky_4k.png b/src/main/resources/rs117/hd/skybox3d/overcast_soil_puresky_4k.png
new file mode 100644
index 0000000000..2dc7f171fb
Binary files /dev/null and b/src/main/resources/rs117/hd/skybox3d/overcast_soil_puresky_4k.png differ
diff --git a/src/main/resources/rs117/hd/skybox3d/qwantani_night_puresky_4k.png b/src/main/resources/rs117/hd/skybox3d/qwantani_night_puresky_4k.png
new file mode 100644
index 0000000000..888c632136
Binary files /dev/null and b/src/main/resources/rs117/hd/skybox3d/qwantani_night_puresky_4k.png differ
diff --git a/src/main/resources/rs117/hd/skybox3d/sunflowers_puresky_4k.png b/src/main/resources/rs117/hd/skybox3d/sunflowers_puresky_4k.png
new file mode 100644
index 0000000000..1f1cb76287
Binary files /dev/null and b/src/main/resources/rs117/hd/skybox3d/sunflowers_puresky_4k.png differ