From 398f1fa26f3b43be65208dd54e2524a2e6ffc4bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beatrice=20Dellac=C3=A0?= Date: Fri, 14 Nov 2025 22:10:28 +0100 Subject: [PATCH] implement full day-night cycle --- .../wtf/beatrice/retrorender/GameScreen.java | 59 ++++-- .../retrorender/engine/DayNightCycle.java | 169 ++++++++++++++++++ .../beatrice/retrorender/engine/DebugHud.java | 8 +- .../beatrice/retrorender/engine/World3D.java | 2 +- 4 files changed, 223 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/wtf/beatrice/retrorender/engine/DayNightCycle.java diff --git a/core/src/main/java/wtf/beatrice/retrorender/GameScreen.java b/core/src/main/java/wtf/beatrice/retrorender/GameScreen.java index 5f688e7..996b296 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/GameScreen.java +++ b/core/src/main/java/wtf/beatrice/retrorender/GameScreen.java @@ -3,6 +3,7 @@ package wtf.beatrice.retrorender; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; import com.badlogic.gdx.Screen; +import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.PerspectiveCamera; import com.badlogic.gdx.graphics.Texture; @@ -14,6 +15,7 @@ import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute; import com.badlogic.gdx.graphics.g3d.environment.DirectionalShadowLight; import com.badlogic.gdx.graphics.g3d.utils.DepthShaderProvider; import com.badlogic.gdx.graphics.glutils.FrameBuffer; +import com.badlogic.gdx.math.Vector3; import wtf.beatrice.retrorender.engine.*; public class GameScreen implements Screen { @@ -24,6 +26,9 @@ public class GameScreen implements Screen { private ModelBatch modelBatch; private Environment environment; + private ColorAttribute ambientLight; + private DayNightCycle dayNightCycle; + private FpsCameraController cameraController; private World3D world; private DebugHud hud; @@ -44,6 +49,7 @@ public class GameScreen implements Screen { private TextureRegion frameRegion; private SpriteBatch screenBatch; + private static final int RETRO_WIDTH = 320; private static final int RETRO_HEIGHT = 240; @@ -61,18 +67,20 @@ public class GameScreen implements Screen { private void initEnvironment() { environment = new Environment(); - // ambient color - environment.set( - new ColorAttribute(ColorAttribute.AmbientLight, - 0.4f, 0.4f, 0.5f, 1f) + // keep a handle to ambient so we can animate it + ambientLight = new ColorAttribute( + ColorAttribute.AmbientLight, + 0.4f, 0.4f, 0.5f, 1f ); + environment.set(ambientLight); - // shadow-casting directional light + // shadow-casting directional light (our sun) shadowLight = new DirectionalShadowLight( 1024, 1024, // shadow map resolution 60f, 60f, // viewport size - 1f, 50f // near/far for the light camera + 1f, 100f // near/far for the light camera ); + // initial values; DayNightCycle will start animating these shadowLight.set( 1.0f, 0.85f, 0.9f, // light color -0.7f, -1.0f, -0.3f // direction @@ -82,6 +90,11 @@ public class GameScreen implements Screen { environment.shadowMap = shadowLight; shadowBatch = new ModelBatch(new DepthShaderProvider()); + + // create the cycle controller + dayNightCycle = new DayNightCycle(shadowLight, ambientLight); + // optional: start at morning/noon/etc + dayNightCycle.setTimeOfDay(0.25f); // sunrise } private void initWorld() { @@ -89,7 +102,7 @@ public class GameScreen implements Screen { } private void initMenus() { - hud = new DebugHud(); + hud = new DebugHud(dayNightCycle); gameUi = new GameUi(settings); } @@ -149,19 +162,39 @@ public class GameScreen implements Screen { world.update(delta); } + // update day/night (you can choose to run even when paused if you prefer) + if (dayNightCycle != null && gameplay) { + dayNightCycle.update(delta); + } + // --- shadow pass --- - shadowLight.begin(new com.badlogic.gdx.math.Vector3(0f, 0f, 0f), camera.direction); + boolean doShadows = (dayNightCycle == null) || dayNightCycle.isSunAboveHorizon(); - shadowBatch.begin(shadowLight.getCamera()); - world.render(shadowBatch, environment); // depth-only pass - shadowBatch.end(); + if (doShadows) { + if (dayNightCycle != null) { + Vector3 center = new Vector3(camera.position.x, 0f, camera.position.z); + Vector3 lightDir = dayNightCycle.getSunDirection(); - shadowLight.end(); + shadowLight.begin(center, lightDir); + } else { + shadowLight.begin(new Vector3(0f, 0f, 0f), new Vector3(-0.7f, -1f, -0.3f)); + } + + shadowBatch.begin(shadowLight.getCamera()); + world.render(shadowBatch, environment); // depth-only pass + shadowBatch.end(); + shadowLight.end(); + } // --- render scene into low res framebuffer --- frameBuffer.begin(); Gdx.gl.glViewport(0, 0, RETRO_WIDTH, RETRO_HEIGHT); - Gdx.gl.glClearColor(0.5f, 0.6f, 1.0f, 1f); + if (dayNightCycle != null) { + Color sky = dayNightCycle.getSkyColor(); + Gdx.gl.glClearColor(sky.r, sky.g, sky.b, 1f); + } else { + Gdx.gl.glClearColor(0.5f, 0.6f, 1.0f, 1f); + } Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); camera.update(); diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/DayNightCycle.java b/core/src/main/java/wtf/beatrice/retrorender/engine/DayNightCycle.java new file mode 100644 index 0000000..53ea659 --- /dev/null +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/DayNightCycle.java @@ -0,0 +1,169 @@ +package wtf.beatrice.retrorender.engine; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute; +import com.badlogic.gdx.graphics.g3d.environment.DirectionalShadowLight; +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.math.Vector3; + +public class DayNightCycle { + + private final DirectionalShadowLight sun; + private final ColorAttribute ambientLight; + + // base colors + private final Color dayAmbient = new Color(0.5f, 0.5f, 0.5f, 1f); + private final Color nightAmbient = new Color(0.05f, 0.05f, 0.1f, 1f); + + private final Color daySunColor = new Color(1.0f, 0.85f, 0.9f, 1f); + private final Color nightSunColor = new Color(0.4f, 0.4f, 0.6f, 1f); + + private final Color daySkyColor = new Color(0.5f, 0.6f, 1.0f, 1f); + private final Color nightSkyColor = new Color(0.02f, 0.02f, 0.06f, 1f); + + private final Color currentSkyColor = new Color(); + private final Color tmpSunColor = new Color(); + private final Vector3 tmpDir = new Vector3(); + private final Vector3 sunDirection = new Vector3(); + + // Rotate around Z so the sun path is tilted relative to world up. + private final Vector3 tiltAxis = new Vector3(0f, 0f, 1f).nor(); + public float axialTiltDeg = 30f; // tweak to taste + + /** + * If false (default), simple equator-style model: + * timeOfDay=0 -> midnight + * timeOfDay=0.25-> sunrise + * timeOfDay=0.5 -> noon + * timeOfDay=0.75-> sunset + * + * Brightness is driven purely by this ideal curve; Z-tilt only affects + * visual direction (shadow slant), not timing of day/night. + * + * If true, brightness and day length depend on the final tilted direction, + * so axialTiltDeg affects how long days are and how high the sun gets. + */ + public boolean seasonal = false; + + /** + * 0–1: 0 = midnight, 0.25 = sunrise, 0.5 = noon, 0.75 = sunset. + */ + private float timeOfDay = 0.25f; + + /** Seconds for one full 24h cycle. */ + public float dayLengthSeconds = 60 * 5f; + + /** + * Daylight / altitude factor in [0, 1]. + * 0 = "sun below horizon / no direct light" + * 1 = "sun as high/bright as it gets for current configuration". + */ + private float sunAltitude = 0f; + + public DayNightCycle(DirectionalShadowLight sun, ColorAttribute ambientLight) { + this.sun = sun; + this.ambientLight = ambientLight; + } + + public void update(float delta) { + if (dayLengthSeconds <= 0f) return; + + // Advance time of day + timeOfDay = (timeOfDay + delta / dayLengthSeconds) % 1f; + + // 0..2π over a day + float angle = timeOfDay * MathUtils.PI2; + + // Ideal equator curve: + // t=0 -> midnight (height=-1) + // t=0.25-> sunrise (height=0) + // t=0.5 -> noon (height=1) + // t=0.75-> sunset (height=0) + float orbitAngle = angle - MathUtils.PI / 2f; + float height = MathUtils.sin(orbitAngle); // -1..1 + float horizontal = MathUtils.cos(orbitAngle); // -1..1 + + // Base direction before Z-tilt, as if on equator. + float baseX = -horizontal * 0.7f; + float baseY = -height; // negative when sun is above + float baseZ = -horizontal * 0.3f; + + if (!seasonal) { + // === Simple "equator-style" mode === + + float sunFactor = MathUtils.clamp(height, 0f, 1f); // 0..1 + sunAltitude = sunFactor; // 👈 always 0..1 + + float ambientFactor = 0.25f + 0.75f * sunFactor; + + // Visual direction with axial tilt + tmpDir.set(baseX, baseY, baseZ).nor(); + tmpDir.rotate(tiltAxis, axialTiltDeg); + sunDirection.set(tmpDir); + + // Sun color / intensity + tmpSunColor.set(nightSunColor).lerp(daySunColor, sunFactor); + tmpSunColor.mul(sunFactor); + + sun.set(tmpSunColor.r, tmpSunColor.g, tmpSunColor.b, + tmpDir.x, tmpDir.y, tmpDir.z); + + // Ambient + sky + ambientLight.color.set(nightAmbient).lerp(dayAmbient, ambientFactor); + currentSkyColor.set(nightSkyColor).lerp(daySkyColor, ambientFactor); + + } else { + // === Seasonal / physical mode === + + tmpDir.set(baseX, baseY, baseZ).nor(); + tmpDir.rotate(tiltAxis, axialTiltDeg); + sunDirection.set(tmpDir); + + // True altitude from final direction: 0..1 + float altitude = Math.max(-tmpDir.y, 0f); + sunAltitude = altitude; // 👈 also 0..1 + + float sunFactor = altitude; + float ambientFactor = 0.25f + 0.75f * altitude; + + tmpSunColor.set(nightSunColor).lerp(daySunColor, sunFactor); + tmpSunColor.mul(sunFactor); + + sun.set(tmpSunColor.r, tmpSunColor.g, tmpSunColor.b, + tmpDir.x, tmpDir.y, tmpDir.z); + + ambientLight.color.set(nightAmbient).lerp(dayAmbient, ambientFactor); + currentSkyColor.set(nightSkyColor).lerp(daySkyColor, ambientFactor); + } + } + + public Color getSkyColor() { + return currentSkyColor; + } + + public float getTimeOfDay() { + return timeOfDay; + } + + /** + * Daylight / altitude factor in [0, 1]. + * 0 = no direct sunlight, 1 = max noon brightness. + */ + public float getSunAltitude() { + return sunAltitude; + } + + public void setTimeOfDay(float t) { + timeOfDay = MathUtils.clamp(t, 0f, 1f); + } + + /** World-space direction the sun is shining *from*. */ + public Vector3 getSunDirection() { + return sunDirection; + } + + /** True if we consider the sun above the horizon (i.e. there should be direct light / shadows). */ + public boolean isSunAboveHorizon() { + return sunAltitude > 0f; // strictly > 0; you can use >= 0.01f if you want a tiny cutoff + } +} diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/DebugHud.java b/core/src/main/java/wtf/beatrice/retrorender/engine/DebugHud.java index 08a82fe..76a6c84 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/engine/DebugHud.java +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/DebugHud.java @@ -12,8 +12,11 @@ public class DebugHud { private final SpriteBatch batch; private final BitmapFont font; + private DayNightCycle dayNightCycle; + + public DebugHud(DayNightCycle dayNightCycle) { + this.dayNightCycle = dayNightCycle; - public DebugHud() { batch = new SpriteBatch(); FreeTypeFontGenerator generator = @@ -62,6 +65,9 @@ public class DebugHud { String angText = String.format("Yaw: %.1f Pitch: %.1f", yaw, pitch); font.draw(batch, angText, 5f, height - 25f); + + String dayText = String.format("Time of day: %.2f Sun height: %.2f", dayNightCycle.getTimeOfDay(), dayNightCycle.getSunAltitude()); + font.draw(batch, dayText, 5f, height - 35f); } batch.end(); diff --git a/core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java b/core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java index 0a39ef6..acc3eae 100644 --- a/core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java +++ b/core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java @@ -54,7 +54,7 @@ public class World3D { GL20.GL_TRIANGLES, VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal - | VertexAttributes.Usage.TextureCoordinates, // 👈 important + | VertexAttributes.Usage.TextureCoordinates, groundMat );