implement full day-night cycle

This commit is contained in:
2025-11-14 22:10:28 +01:00
parent db8d78043f
commit 398f1fa26f
4 changed files with 223 additions and 15 deletions

View File

@@ -3,6 +3,7 @@ package wtf.beatrice.retrorender;
import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input; import com.badlogic.gdx.Input;
import com.badlogic.gdx.Screen; import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.PerspectiveCamera; import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.Texture; 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.environment.DirectionalShadowLight;
import com.badlogic.gdx.graphics.g3d.utils.DepthShaderProvider; import com.badlogic.gdx.graphics.g3d.utils.DepthShaderProvider;
import com.badlogic.gdx.graphics.glutils.FrameBuffer; import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.badlogic.gdx.math.Vector3;
import wtf.beatrice.retrorender.engine.*; import wtf.beatrice.retrorender.engine.*;
public class GameScreen implements Screen { public class GameScreen implements Screen {
@@ -24,6 +26,9 @@ public class GameScreen implements Screen {
private ModelBatch modelBatch; private ModelBatch modelBatch;
private Environment environment; private Environment environment;
private ColorAttribute ambientLight;
private DayNightCycle dayNightCycle;
private FpsCameraController cameraController; private FpsCameraController cameraController;
private World3D world; private World3D world;
private DebugHud hud; private DebugHud hud;
@@ -44,6 +49,7 @@ public class GameScreen implements Screen {
private TextureRegion frameRegion; private TextureRegion frameRegion;
private SpriteBatch screenBatch; private SpriteBatch screenBatch;
private static final int RETRO_WIDTH = 320; private static final int RETRO_WIDTH = 320;
private static final int RETRO_HEIGHT = 240; private static final int RETRO_HEIGHT = 240;
@@ -61,18 +67,20 @@ public class GameScreen implements Screen {
private void initEnvironment() { private void initEnvironment() {
environment = new Environment(); environment = new Environment();
// ambient color // keep a handle to ambient so we can animate it
environment.set( ambientLight = new ColorAttribute(
new ColorAttribute(ColorAttribute.AmbientLight, ColorAttribute.AmbientLight,
0.4f, 0.4f, 0.5f, 1f) 0.4f, 0.4f, 0.5f, 1f
); );
environment.set(ambientLight);
// shadow-casting directional light // shadow-casting directional light (our sun)
shadowLight = new DirectionalShadowLight( shadowLight = new DirectionalShadowLight(
1024, 1024, // shadow map resolution 1024, 1024, // shadow map resolution
60f, 60f, // viewport size 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( shadowLight.set(
1.0f, 0.85f, 0.9f, // light color 1.0f, 0.85f, 0.9f, // light color
-0.7f, -1.0f, -0.3f // direction -0.7f, -1.0f, -0.3f // direction
@@ -82,6 +90,11 @@ public class GameScreen implements Screen {
environment.shadowMap = shadowLight; environment.shadowMap = shadowLight;
shadowBatch = new ModelBatch(new DepthShaderProvider()); 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() { private void initWorld() {
@@ -89,7 +102,7 @@ public class GameScreen implements Screen {
} }
private void initMenus() { private void initMenus() {
hud = new DebugHud(); hud = new DebugHud(dayNightCycle);
gameUi = new GameUi(settings); gameUi = new GameUi(settings);
} }
@@ -149,19 +162,39 @@ public class GameScreen implements Screen {
world.update(delta); 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 --- // --- shadow pass ---
shadowLight.begin(new com.badlogic.gdx.math.Vector3(0f, 0f, 0f), camera.direction); boolean doShadows = (dayNightCycle == null) || dayNightCycle.isSunAboveHorizon();
if (doShadows) {
if (dayNightCycle != null) {
Vector3 center = new Vector3(camera.position.x, 0f, camera.position.z);
Vector3 lightDir = dayNightCycle.getSunDirection();
shadowLight.begin(center, lightDir);
} else {
shadowLight.begin(new Vector3(0f, 0f, 0f), new Vector3(-0.7f, -1f, -0.3f));
}
shadowBatch.begin(shadowLight.getCamera()); shadowBatch.begin(shadowLight.getCamera());
world.render(shadowBatch, environment); // depth-only pass world.render(shadowBatch, environment); // depth-only pass
shadowBatch.end(); shadowBatch.end();
shadowLight.end(); shadowLight.end();
}
// --- render scene into low res framebuffer --- // --- render scene into low res framebuffer ---
frameBuffer.begin(); frameBuffer.begin();
Gdx.gl.glViewport(0, 0, RETRO_WIDTH, RETRO_HEIGHT); Gdx.gl.glViewport(0, 0, RETRO_WIDTH, RETRO_HEIGHT);
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.glClearColor(0.5f, 0.6f, 1.0f, 1f);
}
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
camera.update(); camera.update();

View File

@@ -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;
/**
* 01: 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
}
}

View File

@@ -12,8 +12,11 @@ public class DebugHud {
private final SpriteBatch batch; private final SpriteBatch batch;
private final BitmapFont font; private final BitmapFont font;
private DayNightCycle dayNightCycle;
public DebugHud(DayNightCycle dayNightCycle) {
this.dayNightCycle = dayNightCycle;
public DebugHud() {
batch = new SpriteBatch(); batch = new SpriteBatch();
FreeTypeFontGenerator generator = FreeTypeFontGenerator generator =
@@ -62,6 +65,9 @@ public class DebugHud {
String angText = String.format("Yaw: %.1f Pitch: %.1f", yaw, pitch); String angText = String.format("Yaw: %.1f Pitch: %.1f", yaw, pitch);
font.draw(batch, angText, 5f, height - 25f); 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(); batch.end();

View File

@@ -54,7 +54,7 @@ public class World3D {
GL20.GL_TRIANGLES, GL20.GL_TRIANGLES,
VertexAttributes.Usage.Position VertexAttributes.Usage.Position
| VertexAttributes.Usage.Normal | VertexAttributes.Usage.Normal
| VertexAttributes.Usage.TextureCoordinates, // 👈 important | VertexAttributes.Usage.TextureCoordinates,
groundMat groundMat
); );