Advanced example

From JPCT
Revision as of 20:15, 26 June 2009 by Admin (Talk | contribs)

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
  • 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.