Inka3D

Inka3D

Overview

In this tutorial we create a simple WebGL based picture pairs game. You need Maya with Inka3D version 1.5 or higher and a mouse with three mouse buttons. See completed project

Content creation

This chapter describes the creation of the necessary content, i.e. images and a simple Maya scene.

Images

We use 8 public domain bird images from Uni Hamburg. We crop a square region and resample it to the resolution 256x256. We store them as PNG which is a lossless image format as Inka3D automatically convertes them to JPEG. As we use the Maya image sequence feature we name them image.0.png, image.1.png and so on.

Maya scene

The idea is to create a scene containing one card with a front and back side. The front side can show all eight images selectable via the index attribute of an image sequence. We also add a flip over animation that can be played by animating the time from zero to one.

First, create a polygon plane

Plane and camera

create plane

Then open the attribute editor (Window -> Attribute Editor) and edit the attributes of polyPlane1:

plane attributes

Now create a camera (Create -> Cameras -> Camera) and set some attributes:

camera attributes camera attributes

Shading network

Select the plane in the view port, then select lambert1 in the attribute editor which is a shader. Set Diffuse to 1.0. Then click on the checker symbol next to the Color attribute to add a texture:

add texture

Create a Blend Colors node:

create blend colors

The blend colors node is used to switch between front and back side of the plane. First add a texture to the front side which is Color 2:

add front texture

Create a File Texture:

create file

Then set one image of the sequence of 8 images and switch on Use Image Sequence:

texture attributes

If you move the time slider you see the images in the Texture Sample field. As we want the image index to be independent of the time, we create an extra attribute for the image index. Select the plane in the viewport and then in the menu of the Attribute Editor select Attributes -> Add Attributes..., which opens the Add Attribute dialog where you add an attribute called imageIndex:

add image index attribute

Click Ok and check in the Attribute Editor if file1 has the extra attribute:

add image index attribute

Now edit the expression of Image Number by right-clicking on the value:

edit expression

Replace time by pPlane1.imageIndex and click on the Edit button:

edit expression

Now the image changes if you move the slider of the Image Index extra attribute.

To set the back of the plane go back to the blend colors node by selecting the plane in the viewport again, select lambert1 in the attribute editor and click on the arrow symbol next to the Color attribute to follow the connection to the blend colors node:

go to blend node

Add a texture to the back side which is Color 1:

add back texture

Create a File Texture:

create file

Set the back image:

texture attributes

Then set the placement attributes for the back image:

texture attributes

Now we can look at the shading network that we have created so far by choosing Window -> Rendering Editors -> Hypershade. Right click on the shader lambert1 and select Graph Network. This should look like this:

hypershade

The last thing to do is to add a Sampler Info node that tells us if the plane is visible from the front or back. On the left side of the Hypershade, select Utilities and the Sampler Info:

create sampler info

Place the Sampler Info node next to the file nodes and press the middle mouse button on it, drag the mouse cursor over the Blend Colors node and release it there. In the popup menu select Other...

connect flipped normal

This opens the Connection Editor where you now click on flippedNormal on the left side and then blender on the right side to connect the attributes:

connect flipped normal

Flipover animation

Click on the plane in the viewport and make the attributes of pPlane1 visible in the attribute editor. Rename pPlane1 to card. Then Right-click on the rotateY attribute and select Create New Expression...:

create expression

Type in the expression (rotateY = 180 * smoothstep(0, 1, time);) and click Create:

create expression

Check if the plane rotates in the viewport when you move the time or press the play button next to the time bar. If it works, the maya scene is done.

Javascript programming

This chapter describes the JavaScript programming part of the game.

Step 1: Export to WebGL

Export the Maya scene using the Inka3D HTML/WebGL Exporter as Memory.html. The file export options should look like this:

export options

This creates Memory.html, Memory.js, Memory.dat and all used textures. Now we can start to edit Memory.html. If you change the Maya scene and export it again, take care not to overwrite Memory.html. Use the Inka3D JavaScript/WebGL Exporter to export only Memory.js, Memory.dat and the textures. Note that you have to set the export options again for Inka3D JavaScript/WebGL Exporter.

If everything works you now see the animated plane turning from the front to the back side.

See Step 1

Step 2: Create 16 instances

Our next step is to create 16 instances for the 16 playing cards. For this we create a global array variable for the cards:

// array of 16 cards
var cards = [];
		
In the calback function of loadEmbedded() we initialize the cards by creating 16 instances of the scene. The camera parameters are obtained from the first instance:
// create cards
for (var y = 0; y < 4; ++y)
{
	for (var x = 0; x < 4; ++x)
	{
		// create instance of scene
		var scene = container.createScene("Memory", group);
		
		// get time
		var time = scene.getFloatVector("time", 1);

		// store card in global array
		cards[x + 4 * y] = {
			scene: scene,
			time: time};

		// place this card
		var translate = scene.getFloatVector("card.translate", 3);
		translate[0] = 1.2 * (x - 1.5);
		translate[1] = 1.2 * (y - 1.5);
	}
}

// get reference to camera matrix from scene
cameraMatrix = cards[0].scene.getFloatVector("cameraShape1.worldMatrix[0]", 16);

// get reference to projection parameters from scene
cameraProjection = cards[0].scene.getFloatArray("cameraShape1.projection");
		
The two for loops iterate over the 4x4 cards. For each card an instance of scene Memory is created in the render group. Then the time and image index parameters are retrieved which are of type Float32Array of length 1. Also the cards are placed by retrieving the translate attribute (type Float32Array of length 3) and setting the x and y component which are in translate[0] and translate[1]. Then the info for the current card is stored in the global cards array for later use. In the drawScene function we use the time to animate all 16 cards:
// animate cards
for (var i = 0; i < 16; ++i)
{
	cards[i].time[0] = time;
}
		
Now we have 4x4 cards.

See Step 2

Step 3: Shuffle cards

Now we assign each of the 8 images to two cards and shuffle them. For this we first get the image index in the card creation loops and store it for each card:

// get image index
var imageIndex = scene.getIntVector("card.imageIndex", 1);

// store card in global array
cards[x + 4 * y] = {
	scene: scene,
	time: time,
	imageIndex: imageIndex};
		
The imageIndex variable is of type Int32Array of length 1. An additional function called shuffleCards assigns and shuffles the images:
function shuffleCards()
{
	// initialize cards
	for (var i = 0; i < 16; ++i)
	{
		cards[i].imageIndex[0] = i / 2;
	}

	// shuffle cards
	for (var i = 0; i < 20; ++i)
	{
		// select two cards
		var a = Math.floor(Math.random() * 16);
		var b = Math.floor(Math.random() * 16);
		
		// swap two cards
		var tmp = cards[a].imageIndex[0];
		cards[a].imageIndex[0] = cards[b].imageIndex[0];
		cards[b].imageIndex[0] = tmp;
	}
}
		
This function is called after the card creation loops. Now we have shuffled cards with each image occuring exactly twice.

See Step 3

Step 4: Turning cards with mouse click

To turn cards over from back side to front side on mouse click, we set the time to 1 to show the back side and get the id of each card shape for picking. We also add a side flag that indicates if the card is on the front or back side. This is the internal state and does not represent the visible state. When a card is clicked, the internal state changes immediately while the visible state is smoothly animated. We add the following to the card creation loops:

time[0] = 1;

// get id of card for picking
var id = scene.getObjectId("cardShape[0]");

// store card in global array
cards[x + 4 * y] = {
	scene: scene,
	time: time,
	imageIndex: imageIndex,
	id: id,
	side: false}; // false = back side visible, true = front side visible
		
Then we add a global variable for the mouse/touch input handler:
// mouse/touch input handler
var input;
		
In WebGLStart we create the mouse/touch input handler just after the webgl context:
// create mouse/touch input handler
input = new control.Input(canvas);
		
Before we enter the render loop, we install an input event listener
// install event listeners
input.moveStart = function()
{
	// pick card in group using current camera and mouse position
	// note: mouse position is in device space, i.e. in the range -1 to 1
	var id = group.pick(viewMatrix, projectionMatrix, this.positionX, this.positionY);
	for (var i = 0; i < 16; ++i)
	{
		var card = cards[i];
		if (card.id == id)
		{
			// turn internal state of card to front side
			card.side = true;
		}
	}
};
		
The moveStart handler does a pick on the group of 16 cards. The returned id is compared with the ids of all cards to see if one was hit. The one that was clicked on gets turned to the front side. This is indicated by setting card.side to true. This does not change the graphical representation yet. This gets changed by smoothly animating the time parameter of the card from 1 to 0 to let it rotate from the back to the front side. For this we calculate a time delta and subtract it from the time parameter of all cards that are on the front side and clamp it at zero:
// get time
var time = new Date().getTime() / 1000.0;
var timeDelta = time - startTime;
startTime = time;

// animate cards
for (var i = 0; i < 16; ++i)
{
	var card = cards[i];
	if (card.side)
	{
		// rotate card to front side by animating time of card from 0 to 1
		card.time[0] = Math.max(0, card.time[0] - timeDelta);
	}
}
		
Now we can turn all cards to the front side by clicking on them.

See Step 4

Step 5: Adding game rules

The last step is do add some game rules. On each turn you can turn two cards from the back to the front side. If they are equal, they stay up and you get another turn. Otherwise they are reverted to the back side after a short time and you have to try again. For this delay we add a config variable:

// delay in seconds for two mismatching cards to be turned back
var turnDelay = 2;
		
We also need two global variables for the two cards we can turn over and a time that indicates when the cards have to be turned back if they didn't match:
// first and second card turned over in a turn
var firstCard = null;
var secondCard = null;

// time at which the two cards are turned back
var turnTime;
		
The mouseDown event handler now gets a bit more complicated:
function mouseDown(e)
{
	// actions allowed only if at most one card is already turned over
	if (secondCard == null)
	{
		// pick card in group using current camera and mouse position
		// note: mouse position is in device space, i.e. in the range -1 to 1
		var id = group.pick(viewMatrix, projectionMatrix, this.positionX, this.positionY);
		for (var i = 0; i < 16; ++i)
		{
			var card = cards[i];
			if (card.id == id)
			{
				// check if this card is still on the back side
				if (!card.side)
				{
					// turn internal state of card to front side
					card.side = true;
					
					if (firstCard == null)
					{
						// first card: store it
						firstCard = card;
					}
					else
					{
						// second card: decide if matching or not
						if (card.imageIndex[0] == firstCard.imageIndex[0])
						{
							// good brain: you get a second turn ;-)
							firstCard = null;
						}
						else
						{
							// oh no: store second card to turn it back again after turnDelay
							secondCard = card;							
							turnTime = new Date().getTime() / 1000.0 + turnDelay;
						}
					}
				}
			}
		}
	}
}
		
At first we check if the second card has turned over. If yes, no more actions are possible. If a card has been clicked that is still on the back side, we now check if it is the first or the second one. If it is the first one we simply store it, if it is the second one we check if the images match or not. If they match we clear the first card as two new cards can be turned over. If it does not match we store it and set the time when the cards should turn back. In the drawScene function we check for this timeout and set the cards to back side if it has been reached:
// check if two mismatching cards are turned over and the time to turn them back has been reached
if (secondCard != null && time > turnTime)
{
	firstCard.side = false;
	secondCard.side = false;
	firstCard = null;
	secondCard = null;
}

// animate cards
for (var i = 0; i < 16; ++i)
{
	var card = cards[i];
	if (card.side)
	{
		// rotate card to front side by animating time of card from 1 to 0
		card.time[0] = Math.max(0, card.time[0] - timeDelta);
	}
	else
	{
		// rotate card to back side by animating time of card from 0 to 1
		card.time[0] = Math.min(1, card.time[0] + timeDelta);		
	}
}
		
Also the time of cards now gets animated that are on the back side that were previously on the front side.

Now we can play the game!

See Step 5

Step 6: Optimization

Memory.js, the JavaScript file for the exported Maya scene, has a size of about 60k bytes. This can be optimized by reducing the number of attributes that can be controlled from JavaScript. Therefore we export the scene from Maya again using Inka3D JavaScript Exporter and only make the attributes card.imageIndex and card.translate available:

export options

Note that worldMatrix and projection attributes of the camera of the active viewport get exported automatically. Therefore make sure that camera1 is active and not persp. Now Memory.js has only 29k bytes and still works for our purpose.