|
|
Geometry Array Backgrounder
© Justin Couch 1999-2000
The majority of the time that you are creating a 3D application, you will be
concerned with the use of the GeometryArray class and its
derivatives. The reason for this is that this is the only class that can
present information about polygons to the internal rendering engine. The other
classes derived from the Geometry base class cannot do this.
At first, and even second glance, GeometryArray documentation
is confusing to say the least. To start with, the constructor has all these
different numbers that need to be passed in as parameters - yet the
documentation seems to suggest that you are specifying the same value twice.
Although you will probably never directly instantiate an instance of this
class, the parameters and definitions effect every single derived class.
This introduction should smooth many of those problems out before you get
started.
Defining Geometry
As previously described, the GeometryArray class forms the
basis of all geometry. What it describes is:
- A collection of points in 3D space.
- A colour value for each of these points
- A normal for each of these points
Note that at this stage, there is nothing there that describes relationships
between points or any other surface information. Each vertex (point) that
has been added to the class is standalone in value. If you were to render
this, you would end up with a series of coloured points, one pixel in size,
on your screen. There is nothing to say that this should be a solid surface
or even a collection of lines - that comes from the higher level derived
classes.
So why do we start with just a collection of points? When you boil down
geometry, all you really have are a collection of points in 3D space that
are interconnected. Even with most modern video cards, everything is rendered
as a series of flat surfaces defined by verticies - even complex curves of
objects like cars or aircraft.
Note
|
Tom Barbelett from the Nervana Project
has apparently come up with a non-polygonal based method of representing
surfaces within a rendering engine (not to be confused with spline based
representations of surfaces like NURBS and B-Splines). Supposedly this is
faster than traditional systems by a considerable amount. Unfortunately,
nothing is really available in concrete code or demos at this point in time
to prove or disprove it.
|
After you have defined a 3D point in space, the other information becomes
useful. For each vertex you can give it a separate colour value as well as
a normal - or direction that it is pointing. While defining a normal for
a single point is not particularly useful, once you start connecting vertices
together into surfaces, then they become useful. Because they are common to
almost every type of geometry representation, it makes sense to define those
values here in the base class rather than replicating the same piece of code
through numerous derived classes. Those that don't need the information
(e.g. PointArrays ) can safely ignore it without adding any extra
overhead. We'll explain more what each of these extra pieces do later on in
the chapter.
Geometry Internals
As you pass through this chapter, you will notice that every class has
a constructor that requires arguments - there is no capability for creating
default instances of that class. There's a very good reason for this.
The underlying rendering engine is written in native code - simply to keep
the speed as high as possible. When dealing with Java code, a collection
of corresponding native data structures need to be created. If you have done
any serious programming, in any language, you will be aware that memory
allocation is an extremely expensive operation. For the sake of performance,
you cannot just go around and create new arrays every time that a piece of
geometry is about to be rendered (which could be 50 or more times a second
with the latest consumer cards). Instead, a set of fixed size arrays
are created when you first create the object. To do this accurately, the
information must be passed in through the constructor - hence the
requirement for arguments every single time. This leads to the rather
cumbersome burden of having to know exactly how many vertices each piece of
geometry is going to have before you even create it. This is especially
annoying when you want to create custom derived geometry.
Apart from speed reasons, internal native code is used to represent all
geometry. The underlying rendering API requires data structures to be
passed through. The traditional units of these are triangles that define
surfaces. Once we know the size of the data, updates through the Java side
API calls then just assign values into this array - there does not need to
be any array bounds checking/resizing etc as we update the geometry.
Version 1.2 of J3D has introduced the ability to take user referenced data
rather than making its own internal copy. Just as statically creating internal
arrays saves on memory allocation, this capability will contribute more. It
is often the case that a file loader will be creating a new array instance for
each piece of geometry information created, so there is no need to create yet
further instances (and the instances created by the loading process are
usually quickly discarded anyway shortly after use).
GeometryArray Constructor
There are two constructor arguments: vertexCount and
vertextFormat . As you will see through the chapter, these will
always be required.
The first argument is relatively simple: It is the number of vertices in
this piece of geometry. That is, the total number of points in 3D space
that will be represented in one object. Depending on the type of geometry
you are creating, this may need to be a multiple of 2,3 or 4, but as a
minimum, it must be a non-zero, positive number. At such a low level object
like this, it doesn't matter how many points you have. Later, when you come
to set values for each vertex, there cannot be any more values than this
number.
The vertex format is a little more complex. This describes the type of
data that will be supplied with this piece of geometry. It states whether
there is vertex information, colour information and normals availble. It also
defines the format that information comes in. Let's have a look at each
value:
COORDINATES Vertex values will be supplied. This is not an
optional value so it should always be included and the constructor will
barf if it isn't.
NORMALS Normals for each vertex are supplied. It also implies
that three is one normal per vertex, rather than one per face that might
be implied by higher level primitives.
COLOR_3 Colour information that has three components (RGB)
is supplied. Like normals, this indicates that there is one colour per
vertex supplied.
COLOR_4 Colour information that has four components (ARGB)
is supplied. Follows the same rules as 3 component colour.
TEXTURE_COORDINATE_2 2D texture information is supplied.
Texture coordinates in a 2D space (flat images) can be used. Per vertex
information is implied as for colour and normals.
TEXTURE_COORDINATE_3 3D texture information is supplied.
Texture coordinates are taken from a 3D volume of images (this is how
volume rendering is done in J3D). Per vertex information is implied.
BY_REFERENCE Data passed in will be used as the reference
array rather than information being copied to internal structures.
INTERLEAVED Indicates that data that is supplied is
interleaved into a single array rather than using multiple arrays. This
should only be set is BY_REFERENCE is also set.
All of these values are bitwise OR'd together to form a single value that
is passed to the constructor. Once this has been set, it cannot change. For
example, if you don't elect to use texture information, any texture
coordinates that you supply will be ignored.
For example, say you wanted to create an single square that had standard
colour information and 2D texturing, the constructor call would be like
this:
int format = GeometryArray.COORDINATE |
GeometryArray.COLOR_3 |
GeometryArray.TEXTURE_COORDINATE_2;
Geometry obj = new GeometryArray(4, format);
An alternative constructor is supplied that allows you to specify extra
texture mapping information. This provides coordination between multiple
texture maps for a single object (typically called Multi-texturing) and
sets of texture coordinates. This is a fairly complex subject, so it will be
covered in a separate section in
Chapter 4, Textures.
Variety is the Spice of Life
No, this is not some obscure reference to an all girl pommie pop group! Having
constructed the basic object, you now need to fill it with useful data. A
quick scroll through the javadoc and you'll see how bewildering the options
are. For every piece of information that you could provide, there's at least
six different methods that you could call.
Much of the following information will be modified in some form by the
derived classes, but this should give you a few ideas about where to start.
Every type of information may be supplied either as single values with a
corresponding vertex index or everything may be set at once. In variations
of this, each type may be supplied in a format that is "native" to
it's data type (more on this shortly).
When bulk setting values, you don't need to supply every single value for
vertexCount vertices. These methods provide two parameters
start and length that describe the first vertex
index to set and then the number of vertices to set from the passed in
array.
Note
|
Vertex indices start at zero. Therefore, to set the four corners of a square,
you would need to call:
setCoordinate(0, {x1, y1, z1});
setCoordinate(1, {x2, y2, z2});
setCoordinate(2, {x3, y3, z3});
setCoordinate(3, {x4, y4, z4});
|
Coordinates
Coordinate values may be supplied as either an array of floats or doubles.
For setting an individual value, the array must contain at least 3 values.
Setting a collection of values at once requires that the array is three
times the length of the number of vertices being set (x,y and z coordinates
for each vertex).
If you are obtaining coordinate data from an external source, it might be
easier to use the versions that take Point3f or
Point3d values. The values are copied from this class into the
internal array allowing you to reuse the one class instance multiple times
to save on memory allocation/GC costs.
Note
|
Should you use floats or doubles to describe values? Common sense would say
that for more accurate representations that you should use the highest
precision representation - doubles. However, as an
email
from the J3D team
states, everything is actually converted to floats. If you want the highest
speed, then supply everything in floats and there will be no conversion costs
for your application.
|
Normals
Setting normal values follows the same system as coordinates. You can specify
values in a single array or using Vector3f instances. You can
pass in values individually or a large array of values.
Normal values are required to present a unit length vector. That is the length
of all components must be equal to one. If you can't remember your
Pythagoras Theorem this means:
x^2 + y^2 + z^2 == 1.0
(note that normally you take the square root of the left side, but because the
right has a value of 1.0, there is no point as they will have the same value).
Colour
Colour values may be supplied in one of two forms:
- one byte per component. The range of values are 0 (no contribution)
- 255 (full intensity).
- one float per component. The range of values are 0 (no contribution)
- 1.0 (full intensity).
Which version you use really depends on the source that you are getting
information from. Some file formats, like VRML, use float ranges, while many
UI components will return byte values
(eg javax.swing.JColorChooser )
Once you have decided on the format of the information, there are the
typical numerous ways of providing it. There is the array form (either
3 or 4 values in length depending on what you chose in the constructor
flay), Color3f or Color4f .
Texture Coordinates
Texture coordinates are provided in either array form or using
TexCoord2f /TexCoord3f , again depending on what
you have supplied in the construct flags. (J3D 1.1 and earlier use
Point2f /Point3f to provide the same values).
For texture coordinates there is an extra argument to supply. This
is the texture coordinate set that you are applying the new value to. As it
is possible to have more than one texture to an object, you may not want to
have the same texture coordinates on every texture, so this allows you to
selectively supply values for each image.
Updating Values
Geometry information follows the same rules as all other elements in the
scene graph. In order to update values on a live or compiled piece of
geometry, you need to make sure the appropriate capability bits have been
set. How you update values in the geometry depends on whether you are copying
the data internally or using user supplied references.
Using Copies
Changing values for internal copied data is a simple matter of calling the
exact same methods that you used to set the initial values. Of course, you
also need to make sure that the capability bit for that area is set. For
example, to change colour values, you need to have
ALLOW_COLOR_WRITE set.
Using References
If you have declared that geometry information is being supplied by reference,
then this is a bit more work to be done. The first capability you must
enable is Geometry.ALLOW_REF_DATA_WRITE . Next, you need to
implement the GeometryUpdater interface in your code. The updater
has a single method void updateData(Geometry) .
Making updates with referenced datasets is an asynchronous affair.
- Collect the values that you want to change and store them somewhere
(usually in the class that implements the
GeometryUpdater
interface). Do not set these values in the target geometry object.
- Call the
updateData() method passing in your
GeometryUpdater instance.
- At some time later, when the rendering engine decides that now is a safe
time to make updates, it will call the
updateData() of your
GeometryUpdater instance.
- Call the appropriate setter methods on the geometry object with the data
that you stored in step 1.
- Exit the
updateData() method and all is completed
Note that this requires a fair amount of overhead in creating new classes and
storing the information between when it arrives and the time it is needed for
an update. This could be tricky if you are providing information from external
devices that provide realtime data (eg HMD, 3D mouse etc).
|