Gemini Update

| Comments

It has been a while since I have posted anything, so I thought I would provide a status update on Gemini. I have devoted much of my (all too rare) free time to Gemini, and I have made some real progress.

Gemini Rendering

Gemini uses a layer based rendering system. Every layer has an index which determines its position in the rendering order. Higher indexed layers are rendered on top of lower indexed ones. Additionally, the blending funciton can be specified for each layer, including not using blending at all. Gemini supports all the blending functions available to glBlendFunc(). Gemini also provides display groups, which allow mutliple rendered objects to be manipulated together by changing the group transform (rotating/scaling), position, alpha, visibility, or any of a host of other properties.

In addition to layers managed by Gemini, it is possible for the user to register a callback method as a layer. This allows the user to do custom rendering in their own code as they see fit. For instance, they user can choose to render a background in their own code to get around some limitation of Gemini. The user can register as many callback layers as they want. This makes for a very flexible system in which the user can use as much or as little of Gemini as they desire.

The Gemini coordinate system follows the the OpenGL convention of an x-axis increasing from left to right and a y-axis increasing from bottom to top. In screen space the origin (0,0) is located at the bottom left corner of the screen. Every object (including display groups) has a reference point about which rotation and scaling occurs. By default this point is the center of the object/group — the exception to this is lines, for which the first point in the line is the reference point. This reference point can be offset manually by setting xRef and yRef for the object.

Every object has a set of coordinates associated with it. The position of the center (first point for lines) of the object or group is given by xOrigin, yOrigin. The position of the reference point is given by x,y. Note that unless the reference point has been offset, the point xOrigin, yOrigin is equvalent to the point x,y. An example should help explain.

local rect = display.newRectangle(0,0,100,100)

This creates a rectangle centered on the lower left corner of the screen (the origin). Setting the (x,y) coordinates of the rectangle moves it with respect to the origin of its parent (the screen):

local rect = display.newRectangle(0,0,100,100)
rect.x = 100
rect.y = 100

Lines

As mentioned previously, Gemini provides lines as one of its diplay object types. Lines consist of one or more line segments. Each pair of segments is joined with a mitered joint. It is possilbe to set the width and the color (including alpha) of a line.

star = display.newLine( 150,30, 177,115 )
print("star initialized")
star:append( 255,115, 193,166, 215,240, 150,195, 85,240, 107,165, 45,115, 123,115, 150,30 )
star:setColor( 1.0, 1.0, 0.5, 0.75 )
star.width = 10
star.yReference = 120
star.rotation=90

Rectangles

Rectangles are created by specifying center point, width, and height. In addition to all the usual settings such as postion, rotation, etc., Gemini allows the caller to specify the color of the rectangle as well as an optional border.

rectangle = display.newRect(800,400,100,100)
rectangle:setFillColor(1.0,0,0,1.0)
rectangle:setStrokeColor(1.0,1.0,1.0,1.0)
rectangle.strokeWidth = 5.0
rectangle.rotation = 30

Sprites

Lines and rectangles are neat, but let’s face it – sprites are where the action is. Gemini provides a mechanism for creating animated sprites from a sprite sheet image identical to the systm the Corona SDK uses. This was done to enable the use of the many tools that output lua files compatible with Corona. I have spent a lot of time on the sprite subsystem and will continue to optimize it. It currently uses sprite batching (rendering all the sprites with a common sprite sheet in one draw call) to minimize texture changes during rendering.

The process of creating a sprite in Gemini begins with creating a sprite sheet object. This can be done in one of two was depending on the actual sprite sheet image used. If the sprite sheet image is composed of identically sized images, the the caller can simply specifiy the name of the sprite sheet image and the width and height of the frames.

sprite = require('sprite')
spriteSheet = sprite.newSpriteSheet("mario_sprite.png", 22, 32)

The fist line loads the sprite library and assigns it to the sprite variable. The second calls the factory method for creating the sprite sheet. This assumes that the sprite sheet cells progress from left to right, top to bottom.

If the sprite sheet image is composed of images of varying size, then the user must specify a data structure that specifies each frame in the sprite sheet. This data structure is typically provided in a separate file created by a third party spritesheet tool. This data structure is passed to the factory method for the sprite sheet like so:

horse = require('horse.lua')
spriteSheet = sprite.newSpriteSheetFromData("horses.png", horse.getSpriteSheetData())

A sprite sheet can contain images for mutliple sprites, so the next step in creating a sprite is to instanstiate a sprite set from the sprite sheet. A sprites set indicates all the subimages that belong to a given sprite. A sprite set is created by calling its factory method like so

spriteSet = sprite.newSpriteSet(spriteSheet, 1, 8)

This creates a sprite set from the given sprite sheet using frames 1 through 8. Note that it is assumed that the frames for a given sprite are contiguous, that is, there are no other sprite’s images in between the first and last frame for the sprite. Sprite sets contain all the animations (walk cycle, jump, etc.) for a given sprite. As such the user can add additional information to the sprite set to define animations that it contains. This is done as follows

sprite.add( spriteSet, "walk", 1, 8, 800, 0 )

This adds an animation sequence to the sprite set called walk which consists of the first eight frames and has a total duration of 800 ms. The last parameter specifies how the animation loops. Zero indicates that the animation loops forever.

When a sprite set is created, a default animation named “default” is created consisting of all the frames in the sprite set with a single frame duration of 100 msec.

THe sprite sheet and the sprite set are data structures that hold all the sprite information, but they themselves are not rendered. To actually render a sprite, it must be instiated using a factory method:

 mySprite = sprite.newSprite(spriteSet)
 mySprite.x = 100
 mySprite.y = 100

Animation of the sprite is done by Gemini in its render loop. To begin animating a sprite, the user simply calls prepare (optionally specifying an animation sequence to use) and then calls play. A full example rendering 184 sprites with the following sprite sheet is given below:

The code:

gemini = require('gemini')
display = require('display')
sprite = require('sprite')
require('math')

spriteLayer = display.newLayer(1)
spriteLayer:setBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

spriteSheet = sprite.newSpriteSheet("mario_sprite.png", 22, 32)
spriteSet = sprite.newSpriteSet(spriteSheet, 1, 8)

local ys = 40

for i=1,8 do
  local xs = 40
  for j=1,23 do
    local marioSprite = sprite.newSprite(spriteSet)
    marioSprite.x = xs
    marioSprite.y = ys
    marioSprite.xScale = 1.5
    marioSprite.yScale = 1.5
    marioSprite:prepare()
    marioSprite:play()
    spriteLayer:insert(marioSprite)
    xs = xs + 40

  end
  ys = ys + 80
end

This code creates a blended layer and adds 184 scaled sprite instances to it. Each of these is set to play. This animation runs at 60 fps on an iPhone 4.

A screen shot from running this code on an iPhone 4 is given here:

The Render Loop

As mentioned earlier, Gemini provides a render loop that automatically updates things like sprites. The user can execute their own Lua code every frame by registering an event listener for enterFrame events:

local myListener = function(event)
  rectangle.rotation = rectangle.rotation - 3.0
  rectangle2.rotation = rectangle2.rotation + 1.0
  star.rotation = star.rotation + 1.0
end 

Runtime:addEventListener("enterFrame", myListener)

In this case, a listener is registered which rotates two rectangles and a line (star).

Putting It All Together

The following Lua code uses all the functionality described here:

gemini = require('gemini')
display = require('display')
sprite = require('sprite')

local myListener = function(event)
  rectangle2.rotation = rectangle2.rotation + 1.0
  rectangle.rotation = rectangle.rotation - 3.0
  star.rotation = star.rotation + 1.0

end 

Runtime:addEventListener("enterFrame", myListener)

layer1 = display.newLayer(2)
layer1:setBlendFunc(GL_ONE, GL_ZERO)

layer2 = display.newLayer(1)
layer2:setBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

spriteSheet = sprite.newSpriteSheet("mario_sprite.png", 22, 32)
spriteSet = sprite.newSpriteSet(spriteSheet, 1, 8)


local ys = 40

for i=1,8 do
  local xs = 40
  for j=1,23 do
    local sprite = sprite.newSprite(spriteSet)
    sprite.x = xs
    sprite.y = ys
    sprite.xScale = 1.5
    sprite.yScale = 1.5
    sprite:prepare()
    sprite:play()
    layer2:insert(sprite)
    xs = xs + 40

  end
  ys = ys + 80
end

star = display.newLine( 150,30, 177,115 )
print("star initialized")
star:append( 255,115, 193,166, 215,240, 150,195, 85,240, 107,165, 45,115, 123,115, 150,30 )
star:setColor( 1.0, 1.0, 0.5, 0.75 )
star.width = 10
star.yReference = 120
star.rotation=90

local group1 = display.newGroup()
layer1:insert(group1)
group1:insert(star)
group1.xReference = star.x
group1.yReference = star.y
group1.rotation = 40

rectangle = display.newRect(800,400,100,100)
rectangle:setFillColor(1.0,0,0,1.0)
rectangle:setStrokeColor(1.0,1.0,1.0,1.0)
rectangle.strokeWidth = 5.0
rectangle.rotation = 30

rectangle2 = display.newRect(300,300,50,50)
rectangle2:setFillColor(0,0,1.0,1.0)
rectangle2:setStrokeColor(0,1.0,0,1.0)
rectangle2.strokeWidth = 2.0
rectangle2.rotation = -15

local group2 = display.newGroup()
group2:insert(rectangle)
group2:insert(rectangle2)
layer1:insert(group2)

A screen shot from executing this code is given here:

Combining the sprites with the spinning line (star) and spinning rectangles brings the frame rate down to 30 fps, so clearly I have some optimizing to do. Still, Gemini has come a long way in a relatively short time, so I am encouraged.

Comments