Difference between revisions of "Advanced example"

From JPCT
Jump to: navigation, search
Line 5: Line 5:
  
 
* basic model loading
 
* basic model loading
 +
* animations
 
* collision detection
 
* collision detection
 
* shadows
 
* shadows
Line 214: Line 215:
  
 
GLSLShader shader = new GLSLShader(vertexShader, fragmentShader);
 
GLSLShader shader = new GLSLShader(vertexShader, fragmentShader);
shader.setShadowHelper(sh);
+
// shader.setShadowHelper(sh);
 
shader.setStaticUniform("colorMap", 0);
 
shader.setStaticUniform("colorMap", 0);
 
shader.setStaticUniform("normalMap", 1);
 
shader.setStaticUniform("normalMap", 1);
Line 759: Line 760:
  
 
In addition to polling the keys, this method also 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.
 
In addition to polling the keys, this method also 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:
 +
 +
<pre>
 +
// 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);
 +
</pre>
 +
 +
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 of different parts. 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 there are not really good for displaying a star field. We are using a method that can be found in the jPCT forum here (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:
 +
 +
<pre>
 +
        // Compile all objects for better performance
 +
 +
plane.compileAndStrip();
 +
rock.compileAndStrip();
 +
dome.compileAndStrip();
 +
 +
snork.compile(true, true, true, false, 2000);
 +
</pre>
 +
 +
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:
 +
 +
<pre>
 +
// Deform the plane
 +
 +
Mesh planeMesh = plane.getMesh();
 +
planeMesh.setVertexController(new Mod(), false);
 +
planeMesh.applyVertexController();
 +
planeMesh.removeVertexController();
 +
</pre>
 +
 +
And here's the code for the controller:
 +
 +
<pre>
 +
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;
 +
}
 +
}
 +
}
 +
</pre>
 +
 +
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:
 +
 +
<pre>
 +
        // 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);
 +
</pre>
 +
 +
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 later.
 +
 +
 +
==== 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:
 +
 +
<pre>
 +
// 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);
 +
</pre>
 +
 +
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. It 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 only available in jPCT 1.19, which hasn't been released as i'm writing this. If it has been released, you may uncomment this line again.
 +
 +
 +
====  ====

Revision as of 20:45, 26 June 2009

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 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.animateSync(ind, 2, buffer);
			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 here 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 [1], 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 you 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 parts loads the texture. 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 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);
			}
		}
	}

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_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;
		}
	}

In addition to polling the keys, this method also 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 of different parts. 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 there are not really good for displaying a star field. We are using a method that can be found in the jPCT forum here (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 later.


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. It 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 only available in jPCT 1.19, which hasn't been released as i'm writing this. If it has been released, you may uncomment this line again.