GTCS Game Engine:
Tutorial 6: Shadows and Parallax

Tutorial 5 <-- Tutorial 6
Main tutorial guide page


Introduction

In this tutorial, we are going to continue our understanding of lighting by working with shadow affects. To add more depth to our environment, we are going to also look at layering with parallax effects and end with parallax using camera panning.

Covered Topics: ShadowsParallaxPanning & Parallax

Demonstrations: Casting ShadowsParallax EffectCamera Panning

Complete source code for all tutorials can be downloaded from here.


Shadows

Much like lighting, in order for shadow to be rendered, we have to register each renderable to actively draw shadows. A renderable can be a shadow caster, a shadow receiver or both. We can use a LightRenderable or an IllumRenderable object as a source of shadow. To do this, we will need to use a ShadowReceiver object. The ShadowReceiver encapsulates a GameObject to provide shadow functionality. We then use this object for drawing.

The general process is as follows...

  1. We create LightRenderable or IllumRenderable objects for the caster and receiver. But also define a Z position for the caster to indicate a depth offset. This value is used to calculate shadows. As usual, we encapsulate the Renderable into GameObjects.
  2. We create our light source making sure to activate the calculation of shadows with the source using the setLightCastShadowTo() function. We apply the light source to the caster and receiver renderables.
  3. We initialize a new ShadowReceiver object with the receiver's GameObject.
  4. For every GameObject that we wish to be able to cast shadows onto the receiver, we call the receiver's addShadowCaster() function with a references to the caster GameObject.
  5. During the draw phase, we draw with the ShadowReceiver object. It will calculate all shadows for its renderable during drawing. We need to make sure that we draw the ShadowReceivers before other GameObjects.

We can view our next example here. The WASD keys control the position of the point light. Though we do not define direction for a point light, the light is emanating from a single point 10 units up the Z-axis. The virtual radial "light rays" provide direction that causes the shadow to move opposing the light source.

Figure 6-1: Minion Casting a Shadow

Declare variables and load resources.

function MyGameScene() {
	this.mCamera = null;
	this.mRenderable = null;
	this.mBgObject = null;
	this.mGameObject = null;
	this.mLight = null;
	this.mShadow = null;
	
	this.kBG = "assets/bg.png";
	this.kBGNormal = "assets/bg_normal.png";
	this.kTexture = "assets/minion_sprite.png"
};
gEngine.Core.inheritPrototype(MyGameScene, Scene);

MyGameScene.prototype.loadScene = function () {
	gEngine.Textures.loadTexture(this.kBG);
	gEngine.Textures.loadTexture(this.kBGNormal);
	gEngine.Textures.loadTexture(this.kTexture);
};

MyGameScene.prototype.unloadScene = function () {
	gEngine.Textures.unloadTexture(this.kBG);
	gEngine.Textures.unloadTexture(this.kBGNormal);
	gEngine.Textures.unloadTexture(this.kTexture);
};
Code snippet 6-1: Constructor, Load, Unload

In the initialization function, we create a background renderable. We use an IllumRenderable as we have a normal map for this background. It is going to be encapsulated in a GameObject for shadows so we have it as a local variable.

We create our light source as normal making sure to enable the calculation of shadows.

MyGameScene.prototype.initialize = function () {
	this.mCamera = new Camera(
		vec2.fromValues(50, 40),	// position of the camera
		100,						// width of camera
		[0, 0, 500, 400]			// viewport (orgX, orgY, width, height)
	);
	this.mCamera.setBackgroundColor([0.8, 0.8, 0.8, 1]);
	
	// create a background for the scene and put it into a GameObject
	var background;
	background = new IllumRenderable(this.kBG, this.kBGNormal);
	background.getXform().setSize(100, 80);
	background.getXform().setPosition(50, 40);
	this.mBgObject = new GameObject(background);

	// we use an object type that knows how to render with lights
	this.mRenderable = new LightRenderable(this.kTexture);
	this.mRenderable.setElementPixelPositions(130, 310, 0, 180);

	// create the light and setup the parameters we need
	this.mLight = new Light();
	this.mLight.setLightType(Light.eLightType.ePointLight);
	this.mLight.setColor([1.0, 1.0, 1.0, 1]);
	this.mLight.setXPos(35);
	this.mLight.setYPos(50);
	this.mLight.setZPos(10);
	this.mLight.setNear(18);
	this.mLight.setFar(20);
	this.mLight.setIntensity(2);
	
	// make sure to set this to true
	this.mLight.setLightCastShadowTo(true);
	
	// ... Continued in next snippet ... 

Code snippet 6-2: Creating Background and Light Source

Next, we add the light source to the renderables. We make sure to apply a Z position to the GameObject that will be casting a shadow. Then we create the ShadowReceiver object sending in the background GameObject. We call addShadowCaster() with the caster's GameObject.

	// associate the light with the renderables
	background.addLight(this.mLight);
	this.mRenderable.addLight(this.mLight);

	// create a new game object with the new renderable
	this.mGameObject = new GameObject(this.mRenderable);
	this.mGameObject.getXform().setSize(16, 16);
	this.mGameObject.getXform().setPosition(30, 50);
	
	// provide a Z coordinate for the GameObject
	this.mGameObject.getXform().setZPos(3);

	this.mShadow = new ShadowReceiver(this.mBgObject);
	this.mShadow.addShadowCaster(this.mGameObject);

	// we set the ambient light low to emphasize light affect
	gEngine.DefaultResources.setGlobalAmbientIntensity(0.4);
};
Code snippet 6-3: Initializing

Our draw and update functions are the same as previous examples except, for those GameObjects that must render shadows, we draw using the ShadowReceiver objects.

// This is the draw function, make sure to setup proper drawing environment, and more
// importantly, make sure to _NOT_ change any state.
MyGameScene.prototype.draw = function () {
	// Clear the screen
	gEngine.Core.clearCanvas([0.8, 0.8, 0.8, 1.0]);

	// Activate our camera
	this.mCamera.setupViewProjection();

	// Draw our objects
	this.mShadow.draw(this.mCamera);
	this.mGameObject.draw(this.mCamera);
};

// The Update function, updates the application state. Make sure to _NOT_ draw
// anything from this function!
MyGameScene.prototype.update = function () {
	if (gEngine.Input.isKeyPressed(gEngine.Input.keys.A)) {
		var x = this.mLight.getPosition()[0];
		this.mLight.setXPos(x - 0.5);
	}

	if (gEngine.Input.isKeyPressed(gEngine.Input.keys.D)) {
		var x = this.mLight.getPosition()[0];
		this.mLight.setXPos(x + 0.5);
	}
	if (gEngine.Input.isKeyPressed(gEngine.Input.keys.W)) {
		var y = this.mLight.getPosition()[1];
		this.mLight.setYPos(y + 0.5);
	}

	if (gEngine.Input.isKeyPressed(gEngine.Input.keys.S)) {
		var y = this.mLight.getPosition()[1];
		this.mLight.setYPos(y - 0.5);
	}
};
Code snippet 6-4: Draw and Update

Parallax

Parallax is an animation technique used to create an illusion of depth in a 2D environment. This is particularly common in side-scrolling games and is accomplished by making objects in the background scroll at a slower speed than objects in the foreground. For example, you might have cloud in the sky "in the distance" move very slowly while your characters move very quickly.

In our next example, we implement parallax as shown here. This image is created with eight layers. We will create a LightRenderable for each layer and encapsulate in a ParallaxGameObject. We set the layer number motion direction and speed. If the speed were set the same for all layers, the parallax object will automatically adjust the speed based on the layer. We update the objects and draw in order from farthest to nearest.

The images composing the layers will be automatically tiled making it appear to be ongoing (if you watch the example long enough, the sun will disappear off the left edge of the screen and eventually reappear on the right).

Figure 6-2: Background with Parallax (Graphics created by Mobile Game Graphics)

First we declare all variables and constants. We load the resources.

function MyGameScene() {
	this.mCamera = null;
	
	this.mBgL1 = null;
	this.mBgL2 = null;
	this.mBgL3 = null;
	this.mBgL4 = null;
	this.mBgL5 = null;
	this.mBgL6 = null;
	this.mBgL7 = null;
	this.mBgL8 = null;

	// graphics created by Mobile Game Graphics
	this.kLayer1 = "assets/layer_01.png";
	this.kLayer2 = "assets/layer_02.png";
	this.kLayer3 = "assets/layer_03.png";
	this.kLayer4 = "assets/layer_04.png";
	this.kLayer5 = "assets/layer_05.png";
	this.kLayer6 = "assets/layer_06.png";
	this.kLayer7 = "assets/layer_07.png";
	this.kLayer8 = "assets/layer_08.png";
}
gEngine.Core.inheritPrototype(MyGameScene, Scene);

MyGameScene.prototype.loadScene = function () {
	gEngine.Textures.loadTexture(this.kLayer1);
	gEngine.Textures.loadTexture(this.kLayer2);
	gEngine.Textures.loadTexture(this.kLayer3);
	gEngine.Textures.loadTexture(this.kLayer4);
	gEngine.Textures.loadTexture(this.kLayer5);
	gEngine.Textures.loadTexture(this.kLayer6);
	gEngine.Textures.loadTexture(this.kLayer7);
	gEngine.Textures.loadTexture(this.kLayer8);
};

MyGameScene.prototype.unloadScene = function () {
	gEngine.Textures.unloadTexture(this.kLayer1);
	gEngine.Textures.unloadTexture(this.kLayer2);
	gEngine.Textures.unloadTexture(this.kLayer3);
	gEngine.Textures.unloadTexture(this.kLayer4);
	gEngine.Textures.unloadTexture(this.kLayer5);
	gEngine.Textures.unloadTexture(this.kLayer6);
	gEngine.Textures.unloadTexture(this.kLayer7);
	gEngine.Textures.unloadTexture(this.kLayer8);
};
Code snippet 6-4: Constructor

Now we initialize.

MyGameScene.prototype.initialize  = function () {
	this.mCamera = new Camera(
		vec2.fromValues(50, 40),		// position of the camera
		100,				// width of camera
		[0, 0, 500, 400]			// viewport (orgX, orgY, width, height)
	);
	this.mCamera.setBackgroundColor([0.8, 0.8, 0.8, 1]);
	
	var bg = new LightRenderable(this.kLayer1);
	bg.getXform().setSize(200, 100);
	bg.getXform().setPosition(50, 40);
	bg.getXform().setZPos(0); 
	this.mBgL1 = new ParallaxGameObject(bg, 1, this.mCamera);
	this.mBgL1.setCurrentFrontDir([-1, 0, 0]);
	this.mBgL1.setSpeed(0.06);
	
	bg = new LightRenderable(this.kLayer2);
	bg.getXform().setSize(200, 100);
	bg.getXform().setPosition(50, 40);
	bg.getXform().setZPos(0); 
	this.mBgL2 = new ParallaxGameObject(bg, 1, this.mCamera);
	this.mBgL2.setCurrentFrontDir([-1, 0, 0]);
	this.mBgL2.setSpeed(0.06);
	
	bg = new LightRenderable(this.kLayer3);
	bg.getXform().setSize(200, 100);
	bg.getXform().setPosition(50, 40);
	bg.getXform().setZPos(0); 
	this.mBgL3 = new ParallaxGameObject(bg, 3, this.mCamera);
	this.mBgL3.setCurrentFrontDir([-1, 0, 0]);
	this.mBgL3.setSpeed(0.06);
	
	bg = new LightRenderable(this.kLayer4);
	bg.getXform().setSize(200, 100);
	bg.getXform().setPosition(50, 40);
	bg.getXform().setZPos(0); 
	this.mBgL4 = new ParallaxGameObject(bg, 4, this.mCamera);
	this.mBgL4.setCurrentFrontDir([-1, 0, 0]);
	this.mBgL4.setSpeed(0.06);
	
	bg = new LightRenderable(this.kLayer5);
	bg.getXform().setSize(200, 100);
	bg.getXform().setPosition(50, 40);
	bg.getXform().setZPos(0); 
	this.mBgL5 = new ParallaxGameObject(bg, 5, this.mCamera);
	this.mBgL5.setCurrentFrontDir([-1, 0, 0]);
	this.mBgL5.setSpeed(0.06);
	
	bg = new LightRenderable(this.kLayer6);
	bg.getXform().setSize(200, 100);
	bg.getXform().setPosition(50, 40);
	bg.getXform().setZPos(0); 
	this.mBgL6 = new ParallaxGameObject(bg, 6, this.mCamera);
	this.mBgL6.setCurrentFrontDir([-1, 0, 0]);
	this.mBgL6.setSpeed(0.06);
	
	bg = new LightRenderable(this.kLayer7);
	bg.getXform().setSize(175, 100);
	bg.getXform().setPosition(100, 40);
	bg.getXform().setZPos(0); 
	this.mBgL7 = new ParallaxGameObject(bg, 7, this.mCamera);
	this.mBgL7.setCurrentFrontDir([-1, 0, 0]);
	this.mBgL7.setSpeed(0.06);
	
	bg = new LightRenderable(this.kLayer8);
	bg.getXform().setSize(200, 100);
	bg.getXform().setPosition(50, 40);
	bg.getXform().setZPos(0); 
	this.mBgL8 = new ParallaxGameObject(bg, 8, this.mCamera);
	this.mBgL8.setCurrentFrontDir([-1, 0, 0]);
	
	gEngine.DefaultResources.setGlobalAmbientIntensity(3);
};
Code snippet 6-5: Initialize

Now we draw and update. We draw the layers with furthest (and slowest) first. Calling update() on each layer handles the animation.

// This is the draw function, make sure to setup proper drawing environment, and more
// importantly, make sure to _NOT_ change any state.
MyGameScene.prototype.draw = function () {
	// Clear the screen
	gEngine.Core.clearCanvas([0.8, 0.8, 0.8, 1.0]);
	
	// Activate our camera
	this.mCamera.setupViewProjection();
	
	// Draw our backgrounds
	this.mBgL8.draw(this.mCamera);
	this.mBgL7.draw(this.mCamera);
	this.mBgL6.draw(this.mCamera);
	this.mBgL5.draw(this.mCamera);
	this.mBgL4.draw(this.mCamera);
	this.mBgL3.draw(this.mCamera);
	this.mBgL2.draw(this.mCamera);
	this.mBgL1.draw(this.mCamera);
};

// The Update function, updates the application state. Make sure to _NOT_ draw
// anything from this function!
MyGameScene.prototype.update = function () {
	this.mBgL1.update();
	this.mBgL2.update();
	this.mBgL3.update();
	this.mBgL4.update();
	this.mBgL5.update();
	this.mBgL6.update();
	this.mBgL7.update();
	this.mBgL8.update();
};
Code snippet 6-6: Draw and Update

Camera Panning with Parallax

Scrolling layers with parallax gives a sense of depth but we can take this further by making the effect more interactive. If we remove the calls to setCurrentFrontDir() and setSpeed() for all of the layers in the initialize() function, the layers will not move. We can then have our parallax affect work with the movement of our GameObjects. We can pan the camera within the update() function. Using the camera object's panWith() function will move the camera with the GameObject. We pass in a "subject" GameObject for the camera to focus on and a tolerance. A tolerance of 0.9 will start the pan affect when the subject gets within 90% of the view bounds. The parallax affect will work as expected during the panning.

Use 'WASD' to move the minion. When the minion gets near the edges of the view, the camera will pan to follow the minion. The background will tile with parallax. See here for a live view.

Figure 6-3: Parallax with Camera Panning

In the sample , the panning call is found in the last lines of the update() function. We also use a directional light and completely disable the ambient light to enhance the images using normal maps for added depth. All other functionality has been covered in the tutorials and should be familiar. We show the full code below.

function MyGameScene() {
	this.mCamera = null;
	this.mLight = null;
	
	this.kTexture = "assets/minion_spritesheet.png"
	this.kBgTexture = "assets/bg.png"
	this.kBgTextureNormal = "assets/bg_normal.png"
	this.kBg2Texture = "assets/bgLayer.png"
	this.kBg2TextureNormal = "assets/bgLayer_normal.png"
}
gEngine.Core.inheritPrototype(MyGameScene, Scene);

MyGameScene.prototype.loadScene = function () {
	gEngine.Textures.loadTexture(this.kTexture);
	gEngine.Textures.loadTexture(this.kBgTexture);
	gEngine.Textures.loadTexture(this.kBgTextureNormal);
	gEngine.Textures.loadTexture(this.kBg2Texture);
	gEngine.Textures.loadTexture(this.kBg2TextureNormal);
};

MyGameScene.prototype.unloadScene = function () {
	gEngine.Textures.unloadTexture(this.kTexture);
	gEngine.Textures.unloadTexture(this.kBgTexture);
	gEngine.Textures.unloadTexture(this.kBgTextureNormal);
	gEngine.Textures.unloadTexture(this.kBg2Texture);
	gEngine.Textures.unloadTexture(this.kBg2TextureNormal);
};

MyGameScene.prototype.initialize = function () {
	this.mCamera = new Camera(
			vec2.fromValues(50, 40), // position of the camera
			100, 				// width of camera
			[0, 0, 500, 400]	// viewport (orgX, orgY, width, height)
			);
	this.mCamera.setBackgroundColor([0.8, 0.8, 0.8, 1]);
	
	// We use a directional light so the normals will give depth
	this.mLight = new Light();
	this.mLight.setLightType(Light.eLightType.eDirectionalLight);
	this.mLight.setColor([1.0, 1.0, 1.0, 1]);
	this.mLight.setDirection([0.3, 0, -1]);
	this.mLight.setIntensity(1);
	
	// we now use a sprite renderable object
	this.mRenderable = new LightRenderable(this.kTexture);
	this.mRenderable.setElementPixelPositions(130, 310, 0, 180);
	this.mRenderable.addLight(this.mLight);
	
	// create a new game object with the new renderable
	this.mGameObject = new GameObject(this.mRenderable);
	this.mGameObject.getXform().setSize(16, 16);
	this.mGameObject.getXform().setPosition(30, 50);

	var bg = new IllumRenderable(this.kBgTexture,this.kBgTextureNormal);
	bg.getXform().setSize(100, 100);
	bg.getXform().setPosition(50, 40);
	bg.getXform().setZPos(0);
	this.mBgL1 = new ParallaxGameObject(bg, 2, this.mCamera);
	bg.addLight(this.mLight);
	
	bg = new IllumRenderable(this.kBg2Texture,this.kBg2TextureNormal);
	bg.getXform().setSize(100, 100);
	bg.getXform().setPosition(50, 40);
	bg.getXform().setZPos(0);
	this.mBgL2 = new ParallaxGameObject(bg, 1, this.mCamera);
	bg.addLight(this.mLight);
	
	// We turn off the ambient light entirely
	gEngine.DefaultResources.setGlobalAmbientIntensity(0);
};

MyGameScene.prototype.draw = function () {
	// Clear the screen
	gEngine.Core.clearCanvas([0.8, 0.8, 0.8, 1.0]);

	// Activate our camera
	this.mCamera.setupViewProjection();

	// Draw our background objects
	this.mBgL1.draw(this.mCamera);
	this.mBgL2.draw(this.mCamera);

	// Draw out game object
	this.mGameObject.draw(this.mCamera);
};

MyGameScene.prototype.update = function () {
	// Update for the parallax affect to work
	this.mBgL1.update();
	this.mBgL2.update();

	// Use WASD keys to control the game object
	if (gEngine.Input.isKeyPressed(gEngine.Input.keys.A)) {
		this.mRenderable.setElementPixelPositions(130, 310, 0, 180);
		this.mGameObject.getXform().incXPosBy(-0.5);
	}

	if (gEngine.Input.isKeyPressed(gEngine.Input.keys.D)) {
		this.mRenderable.setElementPixelPositions(720, 900, 0, 180);
		this.mGameObject.getXform().incXPosBy(0.5);
	}

	if (gEngine.Input.isKeyPressed(gEngine.Input.keys.S)) {
		this.mGameObject.getXform().incYPosBy(-0.5);
	}

	if (gEngine.Input.isKeyPressed(gEngine.Input.keys.W)) {
		this.mGameObject.getXform().incYPosBy(0.5);
	}
	
	this.mGameObject.update();
	// Make the camera pan so the game object is always in view
	this.mCamera.panWith(this.mGameObject.getXform(), 0.9);
	this.mCamera.update();
};
Code snippet 6-7: Parallax with Camera Panning

Conclusion

Over the course of this tutorial, we have taken a peek at many aspects of the GTCS Game Engine. For a more in-depth look at the commands, be sure to reference the API documentation.


Tutorial 5 <-- Tutorial 6
Main tutorial guide page

2/16/2016 - David Watson, Proofread by Adedayo Odesile