Advanced example

From JPCT
Jump to: navigation, search

Advanced example

This is a more advanced example. I assume that you are familiar with jPCT's basic, i.e. that you understand the Hello World example. I won't explain the basics that Hello World already covers again. In this example, you'll find...

  • basic model loading
  • animations
  • collision detection
  • shadows
  • shaders
  • vertex controller usage
  • fps-like controls
  • sky domes
  • ...


The resources

You can download the resources that this example uses here: [1]

The source code

Ready? Ok, here we go. I'll post the complete source code first and explain the interesting parts later. So here it is:

package com.threed.jpct.demos.example;

import java.awt.*;
import java.awt.event.*;

import com.threed.jpct.*;
import com.threed.jpct.util.*;

import org.lwjgl.input.*;

public class AdvancedExample implements IPaintListener {

	private static final long serialVersionUID = 1L;

	private static float PI = (float) Math.PI;

	private KeyMapper keyMapper = null;
	private MouseMapper mouseMapper = null;

	private FrameBuffer buffer = null;

	private World world = null;
	private World sky = null;

	private Object3D plane = null;
	private Object3D snork = null;
	private Object3D rock = null;
	private Object3D dome = null;

	private Light sun = null;

	private ShadowHelper sh = null;
	private Projector projector = null;

	private float xAngle = 0;

	private boolean forward = false;
	private boolean backward = false;
	private boolean up = false;
	private boolean down = false;
	private boolean left = false;
	private boolean right = false;

	private float ind = 0;
	private boolean doLoop = true;
	private int fps = 0;
	private long time = System.currentTimeMillis();
	private Ticker ticker = new Ticker(15);

	public static void main(String[] args) throws Exception {
		Config.glVerbose = true;
		AdvancedExample cd = new AdvancedExample();
		cd.init();
		cd.gameLoop();
	}

	public AdvancedExample() {
		Config.glAvoidTextureCopies = true;
		Config.maxPolysVisible = 1000;
		Config.glColorDepth = 24;
		Config.glFullscreen = false;
		Config.farPlane = 4000;
		Config.glShadowZBias = 0.8f;
		Config.lightMul = 1;
		Config.collideOffset = 500;
		Config.glTrilinear = true;
	}

	public void finishedPainting() {
		fps++;
	}

	public void startPainting() {
	}

	private void init() throws Exception {

		// Load textures

		TextureManager tm = TextureManager.getInstance();
		tm.addTexture("grass", new Texture("example/GrassSample2.jpg"));
		tm.addTexture("disco", new Texture("example/disco.jpg"));
		tm.addTexture("rock", new Texture("example/rock.jpg"));
		tm.addTexture("normals", new Texture("example/normals.jpg"));
		tm.addTexture("sky", new Texture("example/sky.jpg"));

		// Initialize frame buffer

		buffer = new FrameBuffer(800, 600, FrameBuffer.SAMPLINGMODE_NORMAL);
		buffer.disableRenderer(IRenderer.RENDERER_SOFTWARE);
		buffer.enableRenderer(IRenderer.RENDERER_OPENGL, IRenderer.MODE_OPENGL);
		buffer.setPaintListener(this);

		// Initialize worlds

		world = new World();
		sky = new World();
		world.setAmbientLight(30, 30, 30);
		sky.setAmbientLight(255, 255, 255);

		world.getLights().setRGBScale(Lights.RGB_SCALE_2X);
		sky.getLights().setRGBScale(Lights.RGB_SCALE_2X);

		// Initialize mappers

		keyMapper = new KeyMapper();
		mouseMapper = new MouseMapper(buffer);
		mouseMapper.hide();

		// Load/create and setup objects

		plane = Primitives.getPlane(20, 30);
		plane.rotateX(PI / 2f);
		plane.setSpecularLighting(true);
		plane.setTexture("grass");
		plane.setCollisionMode(Object3D.COLLISION_CHECK_OTHERS);

		rock = Loader.load3DS("example/rock.3ds", 15f)[0];
		rock.translate(0, 0, -90);
		rock.rotateX(-PI / 2);
		TextureInfo stoneTex = new TextureInfo(tm.getTextureID("rock"));
		stoneTex.add(tm.getTextureID("normals"), TextureInfo.MODE_MODULATE);
		rock.setTexture(stoneTex);
		rock.setSpecularLighting(true);

		snork = Loader.loadMD2("example/snork.md2", 0.8f);
		snork.translate(0, -25, -50);
		snork.setTexture("disco");

		dome = Object3D.mergeAll(Loader.load3DS("example/dome.3ds", 2));
		dome.build();
		dome.rotateX(-PI / 2f);
		dome.setTexture("sky");
		dome.calcTextureWrap();
		tileTexture(dome, 3);
		dome.translate(plane.getTransformedCenter().calcSub(dome.getTransformedCenter()));
		dome.setLighting(Object3D.LIGHTING_NO_LIGHTS);
		dome.setAdditionalColor(Color.WHITE);

		// Add objects to the worlds

		world.addObject(plane);
		world.addObject(snork);
		world.addObject(rock);
		sky.addObject(dome);

		// Build all world's objects

		world.buildAllObjects();

		// Compile all objects for better performance

		plane.compileAndStrip();
		rock.compileAndStrip();
		dome.compileAndStrip();

		snork.compile(true, true, true, false, 2000);
		snork.setCollisionMode(Object3D.COLLISION_CHECK_SELF);

		// Deform the plane

		Mesh planeMesh = plane.getMesh();
		planeMesh.setVertexController(new Mod(), false);
		planeMesh.applyVertexController();
		planeMesh.removeVertexController();

		// Initialize shadow helper

		projector = new Projector();
		projector.setFOV(1.5f);
		projector.setYFOV(1.5f);

		sh = new ShadowHelper(world, buffer, projector, 2048);
		sh.setCullingMode(false);
		sh.setAmbientLight(new Color(30, 30, 30));
		sh.setLightMode(true);
		sh.setBorder(1);

		sh.addCaster(snork);
		sh.addCaster(rock);

		sh.addReceiver(plane);

		// Setup dynamic light source

		sun = new Light(world);
		sun.setIntensity(250, 250, 250);
		sun.setAttenuation(800);

		// Setup shaders for the rock

		String fragmentShader = Loader.loadTextFile("example/shader/fragmentshader.glsl");
		String vertexShader = Loader.loadTextFile("example/shader/vertexshader.glsl");

		GLSLShader shader = new GLSLShader(vertexShader, fragmentShader);
		// shader.setShadowHelper(sh);
		shader.setStaticUniform("colorMap", 0);
		shader.setStaticUniform("normalMap", 1);
		shader.setStaticUniform("invRadius", 0.0005f);
		rock.setRenderHook(shader);

		// Move camera

		Camera cam = world.getCamera();
		cam.moveCamera(Camera.CAMERA_MOVEOUT, 150);
		cam.moveCamera(Camera.CAMERA_MOVEUP, 100);
		cam.lookAt(plane.getTransformedCenter());
		cam.setFOV(1.5f);
	}

	private void pollControls() {

		KeyState ks = null;
		while ((ks = keyMapper.poll()) != KeyState.NONE) {
			if (ks.getKeyCode() == KeyEvent.VK_ESCAPE) {
				doLoop = false;
			}

			if (ks.getKeyCode() == KeyEvent.VK_UP) {
				forward = ks.getState();
			}

			if (ks.getKeyCode() == KeyEvent.VK_DOWN) {
				backward = ks.getState();
			}

			if (ks.getKeyCode() == KeyEvent.VK_LEFT) {
				left = ks.getState();
			}

			if (ks.getKeyCode() == KeyEvent.VK_RIGHT) {
				right = ks.getState();
			}

			if (ks.getKeyCode() == KeyEvent.VK_PAGE_UP) {
				up = ks.getState();
			}

			if (ks.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
				down = ks.getState();
			}
		}

		if (org.lwjgl.opengl.Display.isCloseRequested()) {
			doLoop = false;
		}
	}

	private void move(long ticks) {

		if (ticks == 0) {
			return;
		}

		// Key controls

		SimpleVector ellipsoid = new SimpleVector(5, 5, 5);

		if (forward) {
			world.checkCameraCollisionEllipsoid(Camera.CAMERA_MOVEIN,
					ellipsoid, ticks, 5);
		}

		if (backward) {
			world.checkCameraCollisionEllipsoid(Camera.CAMERA_MOVEOUT,
					ellipsoid, ticks, 5);
		}

		if (left) {
			world.checkCameraCollisionEllipsoid(Camera.CAMERA_MOVELEFT,
					ellipsoid, ticks, 5);
		}

		if (right) {
			world.checkCameraCollisionEllipsoid(Camera.CAMERA_MOVERIGHT,
					ellipsoid, ticks, 5);
		}

		if (up) {
			world.checkCameraCollisionEllipsoid(Camera.CAMERA_MOVEUP,
					ellipsoid, ticks, 5);
		}

		if (down) {
			world.checkCameraCollisionEllipsoid(Camera.CAMERA_MOVEDOWN,
					ellipsoid, ticks, 5);
		}

		// mouse rotation

		Matrix rot = world.getCamera().getBack();
		int dx = mouseMapper.getDeltaX();
		int dy = mouseMapper.getDeltaY();

		float ts = 0.2f * ticks;
		float tsy = ts;

		if (dx != 0) {
			ts = dx / 500f;
		}
		if (dy != 0) {
			tsy = dy / 500f;
		}

		if (dx != 0) {
			rot.rotateAxis(rot.getYAxis(), ts);
		}

		if ((dy > 0 && xAngle < Math.PI / 4.2)
				|| (dy < 0 && xAngle > -Math.PI / 4.2)) {
			rot.rotateX(tsy);
			xAngle += tsy;
		}

		// Update the skydome

		sky.getCamera().setBack(world.getCamera().getBack().cloneMatrix());
		dome.rotateY(0.00005f * ticks);
	}

	private void gameLoop() throws Exception {

		SimpleVector pos = snork.getTransformedCenter();
		SimpleVector offset = new SimpleVector(1, 0, -1).normalize();

		long ticks = 0;

		while (doLoop) {

			ticks = ticker.getTicks();
			if (ticks > 0) {
				// animate the snork and the dome

				animate(ticks);
				offset.rotateY(0.007f * ticks);

				// move the camera

				pollControls();
				move(ticks);
			}

			// update the projector for the shadow map

			projector.lookAt(plane.getTransformedCenter());
			projector.setPosition(pos);
			projector.moveCamera(new SimpleVector(0, -1, 0), 200);
			projector.moveCamera(offset, 215);
			sun.setPosition(projector.getPosition());

			// update the shadow map

			sh.updateShadowMap();

			// render the scene

			buffer.clear();

			buffer.setPaintListenerState(false);
			sky.renderScene(buffer);
			sky.draw(buffer);
			buffer.setPaintListenerState(true);
			sh.drawScene();
			buffer.update();
			buffer.displayGLOnly();

			// print out the fps to the console

			if (System.currentTimeMillis() - time >= 1000) {
				System.out.println(fps);
				fps = 0;
				time = System.currentTimeMillis();
			}
		}

		// exit...

		System.exit(0);
	}

	private void animate(long ticks) {
		if (ticks > 0) {
			float ft = (float) ticks;
			ind += 0.02f * ft;
			if (ind > 1) {
				ind -= 1;
			}
			snork.animate(ind, 2);
			snork.rotateY(-0.02f * ft);
			snork.translate(0, -50, 0);
			SimpleVector dir = snork.getXAxis();
			dir.scalarMul(ft);
			dir = snork.checkForCollisionEllipsoid(dir, new SimpleVector(5, 20,	5), 5);
			snork.translate(dir);
			dir = snork.checkForCollisionEllipsoid(new SimpleVector(0, 100, 0),	new SimpleVector(5, 20, 5), 1);
			snork.translate(dir);
		}
	}

	private void tileTexture(Object3D obj, float tileFactor) {
		PolygonManager pm = obj.getPolygonManager();

		int end = pm.getMaxPolygonID();
		for (int i = 0; i < end; i++) {
			SimpleVector uv0 = pm.getTextureUV(i, 0);
			SimpleVector uv1 = pm.getTextureUV(i, 1);
			SimpleVector uv2 = pm.getTextureUV(i, 2);

			uv0.scalarMul(tileFactor);
			uv1.scalarMul(tileFactor);
			uv2.scalarMul(tileFactor);

			int id = pm.getPolygonTexture(i);

			TextureInfo ti = new TextureInfo(id, uv0.x, uv0.y, uv1.x, uv1.y,
					uv2.x, uv2.y);
			pm.setPolygonTexture(i, ti);
		}
	}

	private static class MouseMapper {

		private boolean hidden = false;

		private int height = 0;

		public MouseMapper(FrameBuffer buffer) {
			height = buffer.getOutputHeight();
			init();
		}

		public void hide() {
			if (!hidden) {
				Mouse.setGrabbed(true);
				hidden = true;
			}
		}

		public void show() {
			if (hidden) {
				Mouse.setGrabbed(false);
				hidden = false;
			}
		}

		public boolean isVisible() {
			return !hidden;
		}

		public void destroy() {
			show();
			if (Mouse.isCreated()) {
				Mouse.destroy();
			}
		}

		public boolean buttonDown(int button) {
			return Mouse.isButtonDown(button);
		}

		public int getMouseX() {
			return Mouse.getX();
		}

		public int getMouseY() {
			return height - Mouse.getY();
		}

		public int getDeltaX() {
			if (Mouse.isGrabbed()) {
				return Mouse.getDX();
			} else {
				return 0;
			}
		}

		public int getDeltaY() {
			if (Mouse.isGrabbed()) {
				return Mouse.getDY();
			} else {
				return 0;
			}
		}

		private void init() {
			try {
				if (!Mouse.isCreated()) {
					Mouse.create();
				}

			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	private static class Mod extends GenericVertexController {
		private static final long serialVersionUID = 1L;

		public void apply() {
			SimpleVector[] s = getSourceMesh();
			SimpleVector[] d = getDestinationMesh();
			for (int i = 0; i < s.length; i++) {
				d[i].z = s[i].z
						- (10f * ((float) Math.sin(s[i].x / 50f) + (float) Math.cos(s[i].y / 50f)));
				d[i].x = s[i].x;
				d[i].y = s[i].y;
			}
		}
	}

	private static class Ticker {

		private int rate;
		private long s2;

		public static long getTime() {
			return System.currentTimeMillis();
		}

		public Ticker(int tickrateMS) {
			rate = tickrateMS;
			s2 = Ticker.getTime();
		}

		public int getTicks() {
			long i = Ticker.getTime();
			if (i - s2 > rate) {
				int ticks = (int) ((i - s2) / (long) rate);
				s2 += (long) rate * ticks;
				return ticks;
			}
			return 0;
		}
	}
}


The configuration section

We'll deal with the configuration of the engine that this example uses here. Here's the code snippet:

	Config.glAvoidTextureCopies = true;
	Config.maxPolysVisible = 1000;
	Config.glColorDepth = 24;
	Config.glFullscreen = false;
	Config.farPlane = 4000;
	Config.glShadowZBias = 0.8f;
	Config.lightMul = 1;
	Config.collideOffset = 500;
	Config.glTrilinear = true;

The first two lines (glAvoid... and maxPolys...) are mainly to save some memory. We don't need copies of your textures in main memory once uploaded to the graphics card and we are using Compiled objects, which is why our VisList won't get very huge, so 1000 polygons are more than enough here. The next few lines are basic stuff. You can read about them in the docs at [2], if you need more information. But i'll cover the last two lines: collideOffset is needed to make the Collision detection work with the plane. It's a common problem when using collision detection, that your collision sources are passing through the obstacles, if this value is too low. 500 is a reasonable value for this example. The last line (glTrilinear) makes jPCT use trilinear filtering. It simply looks better that way.

Textures and the frame buffer

Both sections are pretty simple. Code:

	// Load textures

	TextureManager tm = TextureManager.getInstance();
	tm.addTexture("grass", new Texture("example/GrassSample2.jpg"));
	tm.addTexture("disco", new Texture("example/disco.jpg"));
	tm.addTexture("rock", new Texture("example/rock.jpg"));
	tm.addTexture("normals", new Texture("example/normals.jpg"));
	tm.addTexture("sky", new Texture("example/sky.jpg"));

	// Initialize frame buffer

	buffer = new FrameBuffer(800, 600, FrameBuffer.SAMPLINGMODE_NORMAL);
	buffer.disableRenderer(IRenderer.RENDERER_SOFTWARE);
	buffer.enableRenderer(IRenderer.RENDERER_OPENGL, IRenderer.MODE_OPENGL);
        buffer.setPaintListener(this);

The first part loads the textures. Nothing new here. In the second section, the call to setPaintListener() is interesting, because it shows you how to use an IPaintListener implementation. Pretty simple, but what's the point? Well, the point is, that you have two methods that will be called before and after a frame is being drawn. In this example, we are using this to count frames per second. This example doesn't really need to do it this way, it would have been fine in the game loop itself too, but it's more flexible to do it this way right from the start. Once you decide to switch to AWTGLRenderer or to JOGL for example, this makes the transition easier.

Setting up the worlds

Setting up a world is easy, but this example uses two of them!? Code:

	// Initialize worlds

	world = new World();
	sky = new World();
	world.setAmbientLight(30, 30, 30);
	sky.setAmbientLight(255, 255, 255);

	world.getLights().setRGBScale(Lights.RGB_SCALE_2X);
	sky.getLights().setRGBScale(Lights.RGB_SCALE_2X);

Why two? Because one is for the scene itself and one is for the sky dome. This makes it much easier to handle the dome, but you don't have to do it this way. Adding the dome to the scene itself would work too.


Key- and mouse mapping

The example uses helper classes to access the keyboard and the mouse. The keyboard helper comes with jPCT in form of the KeyMapper. The mouse mapper is similar, just for the mouse. It's part of the source code:

        // Initialize mappers

	keyMapper = new KeyMapper();
	mouseMapper = new MouseMapper(buffer);
	mouseMapper.hide();

And the mapper itself:

	private static class MouseMapper {

	        private boolean hidden = false;

		private int height = 0;

		public MouseMapper(FrameBuffer buffer) {
			height = buffer.getOutputHeight();
			init();
		}

		public void hide() {
			if (!hidden) {
				Mouse.setGrabbed(true);
				hidden = true;
			}
		}

		.....

		public int getDeltaY() {
			if (Mouse.isGrabbed()) {
				return Mouse.getDY();
			} else {
				return 0;
			}
		}

		private void init() {
			try {
				if (!Mouse.isCreated()) {
					Mouse.create();
				}

			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

While we are at it, i'll post the code for the keyboard access here too. It's nothing special, but it shows the basic concept that you should always remember when using the keyboard in games: Don't work with the events but just set/clear a flag for a key pressed/released.

	private void pollControls() {

		KeyState ks = null;
		while ((ks = keyMapper.poll()) != KeyState.NONE) {
			if (ks.getKeyCode() == KeyEvent.VK_ESCAPE) {
				doLoop = false;
			}

			if (ks.getKeyCode() == KeyEvent.VK_UP) {
				forward = ks.getState();
			}

			....

			if (ks.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
				down = ks.getState();
			}
		}

		if (org.lwjgl.opengl.Display.isCloseRequested()) {
			doLoop = false;
		}
	}

In addition to poll the keys, this method asks the Display if it has been closed. This is a direct call to an LWJGL method, because jPCT doesn't provide this information by itself.

Object loading and creation

Without some meshes, the scene would be empty. We are using two static 3DS meshs, an animated MD2 and a self created plane in this example. All three are initialized in this piece of code:

	// Load/create and setup objects

	plane = Primitives.getPlane(20, 30);
	plane.rotateX(PI / 2f);
	plane.setSpecularLighting(true);
	plane.setTexture("grass");
	plane.setCollisionMode(Object3D.COLLISION_CHECK_OTHERS);

	rock = Loader.load3DS("example/rock.3ds", 15f)[0];
	rock.translate(0, 0, -90);
	rock.rotateX(-PI / 2);
	TextureInfo stoneTex = new TextureInfo(tm.getTextureID("rock"));
	stoneTex.add(tm.getTextureID("normals"), TextureInfo.MODE_MODULATE);
	rock.setTexture(stoneTex);
	rock.setSpecularLighting(true);

	snork = Loader.loadMD2("example/snork.md2", 0.8f);
	snork.translate(0, -25, -50);
	snork.setTexture("disco");

	dome = Object3D.mergeAll(Loader.load3DS("example/dome.3ds", 2));
	dome.build();
	dome.rotateX(-PI / 2f);
	dome.setTexture("sky");
	dome.calcTextureWrap();
	tileTexture(dome, 3);
	dome.translate(plane.getTransformedCenter().calcSub(dome.getTransformedCenter()));
	dome.setLighting(Object3D.LIGHTING_NO_LIGHTS);
	dome.setAdditionalColor(Color.WHITE);

There are three interesting things in here. At first, the loading of the 3DS files (more/additional information on loading models, can be found on Loading models page). A 3DS file may contains different parts/meshes. Each part will be loaded as a separate Object3D in jPCT, which is why you get an array as return value from the loader. However, an array of objects isn't needed in this case. The rock consists of a single mesh and the dome...to be honest, i don't know...but regardless of its numbers of meshes, we only need one object for it. Therefore, we only use the first instance from the rock-array and use the mergeAll-method from Object3D to merge all dome parts (if any...) into one. The dome already has texture coordinates, but they are not really suitable for displaying a star field. We are using a method that can be found in the jPCT forum (tileTexture) to improve this. And finally, the plane is a primitive. So it's flat right now. Later, we'll take care of this.

Compiling the objects

We want maximum performance, so we are compiling the objects. This happens in this code snippet:

        // Compile all objects for better performance

	plane.compileAndStrip();
	rock.compileAndStrip();
	dome.compileAndStrip();

	snork.compile(true, true, true, false, 2000);

Not much to comment on this. Just read the Compiled objects page.


Deforming the plane

As said above, the plane is flat...which is boring. We want some flat hills. We could model something like that in a modeler, we could create a vertex shader that does it at runtime (which is pretty bad, because it makes collision detection with the deformed plane very difficult), or we can use an IVertexController. This is what we do here:

	// Deform the plane

	Mesh planeMesh = plane.getMesh();
	planeMesh.setVertexController(new Mod(), false);
	planeMesh.applyVertexController();
	planeMesh.removeVertexController();

And here's the code for the controller:

	private static class Mod extends GenericVertexController {
		private static final long serialVersionUID = 1L;

		public void apply() {
			SimpleVector[] s = getSourceMesh();
			SimpleVector[] d = getDestinationMesh();
			for (int i = 0; i < s.length; i++) {
				d[i].z = s[i].z	- (10f * ((float) Math.sin(s[i].x / 50f) + (float) Math.cos(s[i].y / 50f)));
				d[i].x = s[i].x;
				d[i].y = s[i].y;
			}
		}
	}

It's a pretty simple controller. All it does, is to modify the plane's mesh data based on a simple sin/cos function. We only apply this controller once, which is why we remove it right afterwards. This isn't needed. As long as you don't apply it again, it doesn't change anything no matter if it's still attached or not.


Setting up the shadow mapping

This example uses shadow mapping on hardware that supports it. To ease things, we are using the ShadowHelper that comes with jPCT. Some code:

        // Initialize shadow helper

	projector = new Projector();
	projector.setFOV(1.5f);
	projector.setYFOV(1.5f);

	sh = new ShadowHelper(world, buffer, projector, 2048);
	sh.setCullingMode(false);
	sh.setAmbientLight(new Color(30, 30, 30));
	sh.setLightMode(true);
	sh.setBorder(1);

	sh.addCaster(snork);
	sh.addCaster(rock);

	sh.addReceiver(plane);

We need a projector (which is basically the sun) and set up the shadow helper with that. In this example, the plane is a receiver (i.e. shadows will be cast to it) and the animated model (the snork) and the rock are casters. Rock and snork are not receivers, because self shadowing tends to create artifacts and the rock uses shaders, but more about that in the next section.


Shaders for the rock

The rock uses a simple shader. The shader is exactly the same as the one on the Shaders page. Here's the code:

	// Setup shaders for the rock

	String fragmentShader = Loader.loadTextFile("example/shader/fragmentshader.glsl");
	String vertexShader = Loader.loadTextFile("example/shader/vertexshader.glsl");

	GLSLShader shader = new GLSLShader(vertexShader, fragmentShader);
	// shader.setShadowHelper(sh);
	shader.setStaticUniform("colorMap", 0);
	shader.setStaticUniform("normalMap", 1);
	shader.setStaticUniform("invRadius", 0.0005f);
	rock.setRenderHook(shader);

When using a shader, the fixed function pipeline is disabled completely for this object. This is why an object that uses shaders can't be a receiver of shadows in the way the ShadowHelper does shadow mapping. If you want shadows in a shader object, you would have to do this in the shader. In this example, we won't to this, because it's the advanced example, not the expert one... There's one line commented out in the code. This method is available in jPCT 1.19+ only, which hasn't been released as i'm writing this. If it has been released, you may uncomment this line again.


The game loop

The game loop is where the action happens. To prevent modern cpus from throttling, which makes timing calculations too difficult for this example, this example renders more frames than it actually has to. However, for the game logic (animations, controls,...), we are using a ticks based approach. The ticks are provided by a simple Ticker class:

	private static class Ticker {

		private int rate;
		private long s2;

		public static long getTime() {
			return System.currentTimeMillis();
		}

		public Ticker(int tickrateMS) {
			rate = tickrateMS;
			s2 = Ticker.getTime();
		}

		public int getTicks() {
			long i = Ticker.getTime();
			if (i - s2 > rate) {
				int ticks = (int) ((i - s2) / (long) rate);
				s2 += (long) rate * ticks;
				return ticks;
			}
			return 0;
		}
	}

This implementation uses System.currentTimeMillis(), because it's simple. But it's not very accurate. For a better timing, consider to use System.nanoTime() (which it prone to problems with cpu throttling) or the LWJGL timer (which has some problems with running at double the speed on some VMs). Anyway, correct timing isn't an easy task and for sure not something that we'll cover here in detail.


The game logic

In this block, the game logic happens:

        ticks = ticker.getTicks();
	if (ticks > 0) {
		// animate the snork and the dome

		animate(ticks);
		offset.rotateY(0.007f * ticks);

		// move the camera

		pollControls();
		move(ticks);
	}

If at least one tick has passed, the conditional branch will be entered. Here, the model will be animated, the sun rotated, the keys polled and the camera moved.

This is the animation code:

	private void animate(long ticks) {
		if (ticks > 0) {
			float ft = (float) ticks;
			ind += 0.02f * ft;
			if (ind > 1) {
				ind -= 1;
			}
			snork.animate(ind, 2);
			snork.rotateY(-0.02f * ft);
			snork.translate(0, -50, 0);
			SimpleVector dir = snork.getXAxis();
			dir.scalarMul(ft);
			dir = snork.checkForCollisionEllipsoid(dir, new SimpleVector(5, 20,	5), 5);
			snork.translate(dir);
			dir = snork.checkForCollisionEllipsoid(new SimpleVector(0, 100, 0),	new SimpleVector(5, 20, 5), 1);
			snork.translate(dir);
		}
	}

It simply animates the snork based on its key frames and make it walk around in circles in the scene. The interesting part here is, where the snork gets translated 50 units up (more info: Coordinate system) and then dropped back onto the plane using the collision detection methods that jPCT provides. This way, it follows the terrain quite nicely.


The shadows

Because the projector and the casters move each tick, the shadow map has to be recalculated at least each tick. For reasons mentioned above, we are doing this every frame instead to keep the cpu busy:

        // update the projector for the shadow map

	projector.lookAt(plane.getTransformedCenter());
	projector.setPosition(pos);
	projector.moveCamera(new SimpleVector(0, -1, 0), 200);
	projector.moveCamera(offset, 215);
	sun.setPosition(projector.getPosition());

	// update the shadow map

	sh.updateShadowMap();

This code rotates the projector/sun around the planes center and renders the depth map for the shadow mapping.


Rendering the scene

The last part of the loop renders the scene. Code comes here:

	// render the scene

	buffer.clear();

	buffer.setPaintListenerState(false);
	sky.renderScene(buffer);
	sky.draw(buffer);
	buffer.setPaintListenerState(true);
	sh.drawScene();
	buffer.update();
	buffer.displayGLOnly();

This is pretty basic code except that it renders two worlds into one buffer. The world with the sky dome and the one with the scene. To make the IPaintListener not be called twice (and count double the fps that we actually have), it has to be disabled for one of these calls. We are doing this for sky dome, we could have done it for the scene as well. Please note that rendering the scene doesn't require the usual render...draw...update-sequence in this case, because it uses the shadow helper and its implementation already takes care of this. A simple call to ShadowHelper.drawScene() is sufficient here.


Screen shot

If all goes well, this is how it looks like:

Advanced example.jpg


Final words

That's it. This tutorial doesn't cover each and every detail of the code and the code may not be optimal. However, it should help to get a better understanding for some aspects. Have fun!