Night and Day (done not stupidly)

In my game I have “sunlight” and “source light”, sunlight provided by the sun, remarkably, and source light provided by light blocks. However, in blockworlds light tends to be “prebaked” that is calculated once then each vertex knows how bright it is, this is great for efficiency! However in this prebaking I ended up combining the sunlight and sourcelight into one value; the bigger of the sunlight or the sourcelight, so a vertex (corner) is say 0.7 in brightness because its near a light block. Great!

Now, try to implement night and day, if you want to turn down the sun from 1 to 0.95 you have to regenerate everything to do the combination of sourcelights (still full power) and sunlight (now at 0.95 power). And you can’t rerender everything at once, its just not fast enough. So that mountain over there; its still day over there! But over here its twilight. Not to mention the fact that the whole game is now lagging under the strain. Turns out this is a really stupid way to do it.

So how do you do it not stupidly? You still prebake the light, but you prebake sunlight at full intensity and sourcelight separately for each vertex. Then you let the graphics card sort it out. Or more precisely; you write shaders to have the graphics card combine those prebaked values every frame and get the best of both worlds; you can do millisecond by millisecond adjustments to the sunlight level without any regenerating of geometry at all.

Link

So that’s a nice video of a timelapsed day/night cycle, but how did I do it in practice, well we’ll need:

  • A custom mesh, where we’ll set TWO colour buffers, one for sunlight and one for sourcelight
  • A custom jMonkey material with custom fragment and vertex shaders (the fragment shader is where the real work will be). Btw a vertex is a corner of your geometry and a fragment is sort-of kind-of a pixel  and the shaders allow you to manipulate them on the graphics card, and graphics cards are really really good at that sort of thing.

Now; code!

(Incidentally this is fairly advanced, you can get a long way with jMonkey without worrying about custom meshes at all)

The main java class:

 


public class Main extends SimpleApplication {

private Material mat;
 private float dayNightPeriod = 4;
 private float time;

 public static void main(String[] args) {
 Main app = new Main();
 app.start();
 }

@Override
 public void simpleInitApp() {

 //the points in 3d space where the geometry will be
 Vector3f [] vertices = new Vector3f[8];
 //one square
 vertices[0] = new Vector3f(0,0,0);
 vertices[1] = new Vector3f(3,0,0);
 vertices[2] = new Vector3f(0,3,0);
 vertices[3] = new Vector3f(3,3,0);
 //a second square
 vertices[4] = new Vector3f(3,0,0);
 vertices[5] = new Vector3f(6,0,0);
 vertices[6] = new Vector3f(3,3,0);
 vertices[7] = new Vector3f(6,3,0);

 //combine those vetexes into triangles 
 int [] indexes = { 
 //first square
 2,0,1,1,3,2, 
 //second square 
 6,4,5,5,7,6 
 };

 //we're not using a texture but if we were this would define whtich parts of the image are where 
 Vector2f[] texCoord = new Vector2f[8];
 texCoord[0] = new Vector2f(0,0);
 texCoord[1] = new Vector2f(1,0);
 texCoord[2] = new Vector2f(0,1);
 texCoord[3] = new Vector2f(1,1);
 texCoord[4] = new Vector2f(0,0);
 texCoord[5] = new Vector2f(1,0);
 texCoord[6] = new Vector2f(0,1);
 texCoord[7] = new Vector2f(1,1);

 //Set the first colour buffer, this will be the sunlight color
 Vector4f[] sunlightColour = new Vector4f[8]; //these are Vector4f because we have a red, green, blue and transparency per vertex
 //both the squares at full brightness in sunlight
 sunlightColour[0] = new Vector4f(1,1,1,1);
 sunlightColour[1] = new Vector4f(1,1,1,1);
 sunlightColour[2] = new Vector4f(1,1,1,1);
 sunlightColour[3] = new Vector4f(1,1,1,1);

 sunlightColour[4] = new Vector4f(1,1,1,1);
 sunlightColour[5] = new Vector4f(1,1,1,1);
 sunlightColour[6] = new Vector4f(1,1,1,1);
 sunlightColour[7] = new Vector4f(1,1,1,1);

 Vector4f[] sourceLightColour = new Vector4f[8];
 sourceLightColour[0] = new Vector4f(0.9f,0.9f,0.9f,1); //first square at 90% brightness in sourcelight
 sourceLightColour[1] = new Vector4f(0.9f,0.9f,0.9f,1);
 sourceLightColour[2] = new Vector4f(0.9f,0.9f,0.9f,1);
 sourceLightColour[3] = new Vector4f(0.9f,0.9f,0.9f,1);

 sourceLightColour[4] = new Vector4f(0.3f,0.3f,0.3f,1);//second square at 30% brightness in sourcelight
 sourceLightColour[5] = new Vector4f(0.3f,0.3f,0.3f,1);
 sourceLightColour[6] = new Vector4f(0.3f,0.3f,0.3f,1);
 sourceLightColour[7] = new Vector4f(0.3f,0.3f,0.3f,1);

 //now we have all the data we create the mesh
 //for more details https://jmonkeyengine.github.io/wiki/jme3/advanced/custom_meshes.html
 Mesh mesh = new Mesh();
 mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(vertices));
 mesh.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(indexes));
 mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoord));
 mesh.setBuffer(VertexBuffer.Type.Color, 4, BufferUtils.createFloatBuffer(sunlightColour));
 //we're using the conventional Color buffer for sunlight and using one of the (usually) unused TexCoord5 to hold the source light
 mesh.setBuffer(VertexBuffer.Type.TexCoord5, 4, BufferUtils.createFloatBuffer(sourceLightColour));
 mesh.updateBound();

 Geometry geom = new Geometry("mesh", mesh);

mat = new Material(assetManager, "MatDefs/RepeatingUnshaded.j3md");
 //mat = new Material(assetManager,"Common/MatDefs/Misc/Unshaded.j3md"); 
 mat.setBoolean("VertexColor", true);
 geom.setMaterial(mat);

rootNode.attachChild(geom);
 }

@Override
 public void simpleUpdate(float tpf) {
 time += tpf;
 if (time>dayNightPeriod){
 time = 0; 
 }
 mat.setParam("SunlightIntensity", VarType.Float,time/dayNightPeriod); 
 }
}

 

Now if I posted the whole of the shaders they’d look crazy complicated, but I’ve mostly stolen them from the built in Common/MatDefs/Misc/Unshaded.j3md and just edited what I needed to combine the colour rather than just pass it though. So to avoid a code dump I’ll just pull out the important bits and here’s a repository version for the full project https://bitbucket.org/RichardTingle/sunsourcelightwithshaders

A jMonkey Material def contains all the uniforms (things that are common to the whole material) and references to the shaders to use. The only thing we need to add here is the SunlightIntensity uniform. We can vary this to tell the shader how much to use the sunlight prebaked light


MaterialDef Unshaded {
MaterialParameters {
Float SunlightIntensity // This is send to the shaders to tell them how combine light levels.
.........
}
........
}

The vertex shader can affect the verticies of the mesh, we don’t really want to do much here so we just recieve the extra color data and pass it on to the fragment shader, where the real work will be

attribute vec4 inTexCoord5; // this is the source light that is passed to us
varying vec4 texCoord5; //and this is how we'll pass it through to the fragment shader

void main(){
......
texCoord5 = inTexCoord5; //its the fragment shader that wants this so we just pass it on
}

The fragment shader is where the real work happens, we’ll declare that we want the uniform m_SunlightIntensity and use that to combine the sun and source light. Each fragment (soft-of pixel) will combine the sunlight and sourcelight according to the current sunlight level.

uniform float m_SunlightIntensity; //just by declaring the uniform we'll receive it automatically
varying vec4 texCoord5; //this is the source color passed through to us
void main(){
.......
vec4 color = vec4(1.0);
color.x *= max(vertColor.x * m_SunlightIntensity,texCoord5.x);
color.y *= max(vertColor.y * m_SunlightIntensity,texCoord5.y);
color.z *= max(vertColor.z * m_SunlightIntensity,texCoord5.z);
color.a *= vertColor.a;
......
}

So with all that together you get this:

 

Personally I thought the forest was prettier but nevermind.

Anyway, that’s light done non stupidly (or at least less stupidly). A few thoughts you may immediately have; what about machines? Sourcelight coming from the main world affecting moving machines would need to regenerate geometry each tick to truely track the light through it (slow!) so my plan is to have the light sampled by the machines at a single point and used as the sunlight level; that will give me not quite right but hopefully good enough lighting for the machines.

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s