Duke Wiki  logo
Child pages
  • Creating New Croquet Objects
Skip to end of metadata
Go to start of metadata

C r e a ti n g N e w C r o q u e t O b j e c t s

Croquet is a component-based architecture that makes it extremely easy and quick for the programmer to create useful collaborative objects. The system was designed from the ground up to make it simple to create truly shared objects of virtually any sort. This section is used to give you an overview of the architecture of these objects, and how the programmer can build his own. We assume a basic understanding of how to program in the Squeak environment and some familiarity with OpenGL. Though there is some overlap between this section and the previous deconstruction section, it is worthwhile to see how we can construct new components from the ground up.

C o m p o nents

We use the term "component" to describe the basic unit of composition in the Croquet 3D environment. The suite of component level classes is built on top of the TObject base class, which is the fundamental collaboration object. TObject's functionality ensures that when you share an instance object created from a subclass of TObject with other users it maintains a consistent state between them. The TFrame object is derived from TObject and is in turn the base class of all the 3D objects that you can see and interact with in Croquet. The subclasses of TFrame act as frames in an OpenGL rendering hierarchy, as event handlers, and as time-based simulation objects.

At a high level, a component's functionality can be broken up into three main areas:

  • Graphics – This is how components express themselves visually to the user. The graphics-rendering engine is based on OpenGL.
  • Events – Events are how the user communicates his desires to the component. This includes mouse and keyboard input.
  • Simulations – This are how the component expresses itself to the user over time or when stimulated by an event.

R e n d e r i n g  E n g i n e

The philosophy behind Croquet's rendering engine is based on allowing the programmer complete access and control of the underlying graphics library, in this case OpenGL, while at the same time, providing a rich framework within which to embed these extensions with a minimal level of effort. This allows the naïve graphics programmer and 3D artist a way to easily create interesting graphic artifacts with minimal effort, yet allows the expert the ability to add fundamental extensions to the system without the need to modify the underlying architecture.

A rendering frame includes a transform matrix that defines the orientation and position of the object in a 3D space relative to its parent object in the hierarchy as well as the ability to render itself in that position in global space. A rendering message is sent to the object when its position in the hierarchy is reached. The object then calls the appropriate OpenGL library functions to render the object.

E v e n t s

An event handler can respond to user events such as keyboard and mouse/pointer events. Again, this interface is quite extensible by the programmer, but the default is that the TCamera carries a TPointer object (a kind of TRay) that tracks the objects that are underneath the current mouse position. A TPointer is a 3D analog to the mouse event object. Instead of being just a 2D position on the screen, the pointer includes vector information, in this case from the camera to the selected object in both global and local (to the selected object) frame transforms.

Keyboard events are also forwarded to the currently selected object when the pointer is over the geometry of the object. This model allows us to embed 2D objects into a scene, where the containing 3D object simply converts the TPointer vector data back into a 2D mouse position on the surface of the 2D object.

Si m u l a t i o n s

A component can exhibit behaviors on its own. These can be simple response behaviors based upon it receiving an event, or it can be a time-based change in the fundamental state of the object. This last kind of change we refer to as a simulation. There are three kinds of simulations in Croquet. The first is a simple one that determines the state of an object at render time based only upon the current TeaTime, or a difference between the current TeaTime and a set time in the past, perhaps triggered by an event.

The second kind of simulation is usually used for managing more complex behaviors, those that can't easily be captured by trying to recalculate the state of the world from some time in the past. This is done by literally having the object send a message to itself in the future.

G e tt i n g S t a r t e d : R e n d e r i n g

To get started in understanding the Croquet architecture, let's just build something. In this case, we will create a simple cube that will be floating above our little 3D world. We are assuming that you already know how to make and edit programs in Squeak. If you don't, we highly encourage you to spend some time getting familiar with it before you dive into this introduction.

The first thing we do is define a new subclass of TFrame. Let's call it TMyCube. There is already a TCube defined that is a bit more efficient and flexible than the one we are about to write. Feel free to check it out.

We can now add a TMyCube object to the scene, even though we have not written any code. To add it to an existing space all you need is:

tframe:= TMyCube  new.

space addChild: tframe.

The default rendering method of the base TFrame class simply displays the object's three axes. The code above places the object at the default 0,0,0 position inside of the space. We can easily move it by adding:

 tframe  translation: (1@2@3).

Now let's render one of the cube's faces. Once we do this, we can just replicate the code five more times to do the entire cube.

The Croquet rendering engine is built on OpenGL, but in fact, all of OpenGL is always available to the programmer. Croquet is a good compromise between the ease of use of a retained-mode engine and the flexibility of accessing the low-level graphics routines. In the case of our cube, the base TFrame class takes care of all of the setup and transforms we need to deal with, while the TMyCube class takes care of how to render this particular object.

This is the start of the TMyCube render method:

TMyCube>>render:  ogl
|dx dy  dz|

dx  := 1.0.
dy  := 1.0.
dz  := 1.0.


ogl glBegin: GLQuads.
ogl glVertex3f:  dx negated with:  dy  with:  dz.
ogl glVertex3f:  dx negated with:  dy  negated  with:  dz.
ogl glVertex3f:  dx with:  dy  negated  with:  dz.
ogl glVertex3f:  dx with:  dy  with:  dz. ogl glEnd.

This code generates a 2x2 square one unit away from the center of where the cube will ultimately be. This uses the traditional Smalltalk syntax for accessing OpenGL. The ogl object which is passed in as the parameter to the #render: method is actually the interface to OpenGL and in addition to having methods that give the programmer access to all of the OpenGL library functions, it also maintains some of the global state variables used in rendering.

Croquet extends Squeak syntax to allow you to access OpenGL functions in their 'positional' form. E.g., we support both:

ogl glVertex3f: x with: y with: z.

as well as:

ogl glVertex3f(x, y, z).

This allows you to use a representation of the syntax that is nearly identical to that used in the OpenGL textbooks. We can rewrite the #render: method this way:

TMyCube>>render:
ogl


dx dy
dz


dx
:= 1.0. dy
:= 1.0. dz
:= 1.0.
ogl glBegin( GLQuads ). |

ogl glVertex3f(
dx negated,
dy,
dz
).

 


 

ogl glVertex3f(
dx negated,
dy
negated,
dz
).

 

 


ogl glVertex3f(
dx,
dy
negated,
dz
).

 


 

ogl glVertex3f(
dx,
dy,
dz
).


 

 

ogl glEnd.

And of course to do all six faces we can just replicate this code with its various permutations:

TMyCube>>render:
ogl


dx dy
dz


dx
:= 1.0. dy
:= 1.0. dz
:= 1.0.
ogl glBegin( GLQuads).
ogl glVertex3f(
dx negated,
dy,
dz).
ogl glVertex3f(
dx negated,
dy
negated,
dz). ogl glVertex3f(
dx,
dy
negated,
dz).
ogl glVertex3f(
dx,
dy,
dz). |

ogl glVertex3f(
dx,
dy,
dz
negated).

 

 


 

 

 

ogl glVertex3f(
dx,
dy
negated,
dz
negated).

 

 

 

 


 

ogl glVertex3f(
dx negated,
dy
negated,
dz
negated).

 

 

 

 

 


ogl glVertex3f(
dx negated,
dy,
dz
negated).

 

 

 

 


 


 

 

 

 

 

 

ogl glVertex3f(
dx,
dy,
dz
).

 


 

 

 

 

ogl glVertex3f(
dx,
dy
negated,
dz).

 

 


 

 

 

ogl glVertex3f(
dx,
dy
negated,
dz
negated).

 

 

 

 


 

ogl glVertex3f(
dx,
dy,
dz
negated).

 

 


 

 

 


 

 

 

 

 

 

ogl glVertex3f(
dx negated,
dy,
dz
negated).

 

 

 

 


 

ogl glVertex3f(
dx negated,
dy
negated,
dz
negated).

 

 

 

 

 


ogl glVertex3f(
dx negated,
dy
negated,
dz).

 

 

 

 


 

ogl glVertex3f(
dx negated,
dy,
dz).

 

 


 

 

 


 

 

 

 

 

 

ogl glVertex3f(
dx,
dy,
dz).


 

 

 

 

 

ogl glVertex3f(
dx,
dy,
dz
negated).

 

 


 

 

 

ogl glVertex3f(
dx negated,
dy,
dz
negated).

 

 

 


 

 

ogl glVertex3f(
dx negated,
dy,
dz).

 

 


 

 

 


 

 

 

 

 

 

ogl glVertex3f(
dx negated,
dy
negated,
dz
negated).

 

 

 

 

 


ogl glVertex3f(
dx,
dy
negated,
dz
negated).

 

 

 

 


 

ogl glVertex3f(
dx,
dy
negated,
dz).

 

 


 

 

 

ogl glVertex3f(
dx negated,
dy
negated,
dz).

 

 

 


 

 


ogl glEnd.

 

 

 

 

 

 

Of course, there are far more concise ways to write this code. Below, we see the result of rendering this object. Since we have not specified any normals for the surfaces, there is no directional light component.

We can modify the code to add the normal like so:

TMyCube>>render:
ogl


dx dy
dz


dx
:= 1.0. dy
:= 1.0. dz
:= 1.0.
ogl glBegin( GLQuads ). |

ogl glNormal3f(
0.0, 0.0, 1.0).


ogl glVertex3f(
dx negated,
dy,
dz
).
ogl glVertex3f(
dx negated,
dy
negated,
dz
). ogl glVertex3f(
dx,
dy
negated,
dz
).
ogl glVertex3f(
dx,
dy,
dz
).
....
ogl glEnd.

This would look even nicer if we added a texture to the cube. This requires a bit more setup, as we have to load the texture, and we certainly don't want to do this every time we render the image. So let's add an instance variable to the object called "txtr" and load a bitmap file into it.

TFrame
subclass:
#TMyCube

 

 

instanceVariableNames:

'txtr

'

classVariableNames:
'' poolDictionaries: ''
category: 'Croquet-Practice

The initialize method for our TMyCube class is:

TMyCube>>initialize super initialize.

txtr := TTexture new initializeWithFileName: 'rchecker.bmp' mipmap: true
shrinkFit: false.

Now we also have to update the render method to include both the texture as part of the rendering process, but determine how it will be stretched across the cube. The new render method becomes:

TMyCube>>render: ogl

 


dx dy
dz


dx
:= 1.0. dy
:= 1.0. dz
:= 1.0. | | | |

txtr
ifNotNil:\\ ogl.


 

 

ogl glBegin( GLQuads ).
ogl glNormal3f(
0.0, 0.0, 1.0).

ogl glTexCoord2f(0.0, 0.0).
"1"

 


 

ogl glVertex3f(
dx negated,
dy,
dz
).

 

 

 

ogl glTexCoord2f(
0.0, 1.0).
"2"

 

 


ogl glVertex3f(
dx negated,
dy
negated,
dz
).

 

 

 

ogl glTexCoord2f(
1.0, 1.0).
"3"

 

 


ogl glVertex3f(
dx,
dy
negated,
dz
).

 

 

 

ogl glTexCoord2f(1.0, 0.0).
"4"

 


 

ogl glVertex3f(
dx,
dy,
dz
).
...
..
ogl glEnd.

txtr
ifNotNil:\\ ogl..

 


 

And yields the following cube:

G e tt i n g S t a r t e d : E v e n t s

The next thing we will do is to enable the cube to respond to a user event. This requires a bit more work, because we have to give the system some hints as to where the cube is when the user picks it with his pointer. Thus, the first thing we need to do is add a simple bounding sphere to TMyCube.

This sphere is used for a number of things. First, it helps the renderer determine if the object is visible or not. If we don't provide a bound sphere, the renderer has to assume the object may be visible and go through all the work of trying to render it, even if it is somewhere behind the camera. By placing the object inside of a sphere that is a very rough approximation of the original object, we know that if no part of the sphere is visible, then there is no way for the object to be.

Second, the sphere is a first approximation test to determine if the pointer, or any other ray, might intersect with the enclosed object. Again, if the ray does not intersect with the sphere, there is no way that it can intersect with the object enclosed inside. Since our cube is a constant size, we can initialize a new boundSphere instance variable at start up.

TFrame
subclass:
#TMyCube

 

 

instanceVariableNames:
'txtr

boundSphere'


classVariableNames:
'' poolDictionaries: ''
category: 'Croquet-Practice

We now initialize the object this way:

TMyCube>>initialize super initialize.
txtr
:= TTexture
new initializeWithFileName:
'rchecker.bmp' mipmap:
true
shrinkFit: false.

boundSphere
:= TBoundSphere
localPosition:
(0@0 (mailto:@0)@ (mailto:@0)0)

(00 (mailto:@0)@ (mailto:@0)

(00 (mailto:@0)@ (mailto:@0)


radius:
(3
sqrt).

 


 

boundSphere
frame:
self.


 

 


 

 

 

We add a reference to the current object to the boundSphere, which allows us to just add the boundSphere itself to the tests of visibility or intersection. If these are positive results, the object can be easily accessed from the boundSphere for further tests.

We need to add a method that allows external access to the boundSphere as well:

TMyCube>>boundSphere
^ boundSphere.

The next thing TMyCube must be able to do is determine if the pointer is intersecting with the cube. Keyboard and mouse events are usually vectored to the object that the pointer is over, so we need to know when this happens. The
#pick: method is used by the object to determine if the ray is intersecting with it. The #pick: method is not called unless the boundSphere test returns a positive result, so we can actually forgo more precise calculations of intersection and just have #pick: always return true.

TMyCube>>pick: aTRay
^ true.

The downside of this is that we will get some false positives, because we are now actually picking the sphere that surrounds the cube, and not the cube itself. More complex and precise picking models are available for study in the Croquet source code. See the 'picking' method category of TRay.

Let's have the cube move when we click on it with the pointer. To do this, we first need to let the system know that we are particularly interested in pointer events, and then we need to write a #pointerDown: method. The #eventMask: message is used to let the system know that this object cares about the user clicking the pointer on the object.

TMyCube>>eventMask
^ EventPointerDown.

Let's have the #pointerDown: move the cube up a bit every time we click on it.

TMyCube>>pointerDown: aCroquetEvent self translation: (self translation + (0@0.1 (mailto:@0)@ (mailto:@0)0)).

G e tt i n g S t a r t e d : S i m u l a t i o n s

Finally, we get to the point where our cube will start doing something more interesting than just sitting there, waiting for us to do something. We will now have it perform some tricks for us. The simplest simulation we can perform is to base the state of the object on the current island time. Let's add another instance variable called 'move'. We will initialize with the Boolean value of "false".

TFrame
subclass:
#TMyCube

 

 

instanceVariableNames:
'txtr boundSphere

move'


classVariableNames:
'' poolDictionaries: ''
category: 'Croquet-Practice

TMyCube>>initialize super initialize.

txtr := TTexture new initializeWithFileName: 'rchecker.bmp' mipmap: true
shrinkFit: false.

boundSphere := TBoundSphere localPosition: (0@0 (mailto:@0)@ (mailto:@0)0)
radius: (3 sqrt). boundSphere frame: self.

move := false.

Next, let's modify the #pointerDown: method to flip the state of move.

TMyCube>>pointerDown: pointer move := move not.
self update.

Finally, let's add an update method that moves the cube around in a complex circle based upon the island time.

TMyCube>>update
| t delta |
move ifFalse: self.

t := self island time.
delta := 3 * (t sin @ t cos @ (t sin * t cos)). self translation: delta.
(self future: 50) update.

This is clearly a simple kind of simulation, where the only information required to determine the render state is the current time. More complex simulations are often based upon previously calculated states and would require significant overhead to recreate this information on every update. We can store previous state in the object, and rely on the repeated #future: messages to keep us from falling too far behind.

  • No labels