implement full day-night cycle
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user