324 LOW-LEVEL MODELING IN M3G CHAPTER 14 is not very useful for most real-world meshes, but can save some space in certain special cases and procedurally generated meshes. The other alternative, explicit strips, is what you normally want to use. Here, the difference is that instead of a starting vertex, you give a list of indices: TriangleStripArray(int[] indices, int[] stripLengths) The indices array is then split into as many separate triangle strips as specified in the stripLengths array. For a simple example, let us construct an IndexBuffer contain- ing two strips, one with five and the other with three vertices; this translates into three and one triangles, respectively. static final int myStripIndices[]={0,3,1,4,5,7,6,8}; static final int myStripLengths[]={5,3}; myTriangles = new TriangleStripArray(myStripIndices, myStripLengths); Performance tip: While triangle strips in general are very efficient, there is a consider- able setup cost associated with rendering each strip. Very small strips are therefore not efficient to render at all, and it is important to try to keep your strips as long as possi- ble. It may even be beneficial to join several strips together using degener ate triangles, duplicating the end index of the first and the beginning index of the second strip so that zero-area triangles are created to join the two strips. Such degenerate triangles are detected and quickly discarded by most M3G implementations. As usual, your mileage may vary. With the large spectrum of M3G-enabled devices out there, some software-only implementations may in fact be able to render short strips fairly efficiently, whereas some other implementations may optimize the strips themselves regardless of how you specify them. Submitting already optimized strips may therefore yield little or no benefit on some devices. Note that unlike vertex arrays and buffers, the index buffers cannot be modified once created—this was seen as an unnecessary feature and left out in the name of implementa- tion complexity, but perhaps overlooked the fact that someone might still want to recycle an IndexBuffer rather than create a new one. Nevertheless, index buffers need not have a one-to-one correspondence to vertices, and you can use as many index buffers per vertex buffer as you want to. The index buffer size does not have to match the size of the vertex buffer, and you can reference any vertex from multiple index buffers, or multiple times from the same index buffer. The only restriction is that you may not index outside of the vertex buffer. As a concrete use case, you could implement a level-of-detail scheme by generating multiple index buffers for your vertex buffer, with fewer vertices used for each lower detail level, and quickly select one of them each time you render based on the distance to the camera or some other metric. SECTION 14.1 BUILDING MESHES 325 14.1.4 EXAMPLE Now we know how to build some geometry in M3G and draw it. Let us illustrate this with a more comprehensive example where we create some colored triangles and render them. We assume that you have set up your Graphics3D and Camera as described in Chapter 13; make sure your Camera sits at its default position and orientation at the origin. You will also see that we construct an Appearance object, which we have not described yet. In this example, it merely tells M3G to use the default shading parameters— we will cover Appearance in detail in the next section. First, let us define our vertex and triangle data. You could put these as static members in one of your Java classes. static final byte positions[] = { 0,100,0, 100,0,0, 0,— 100,0, — 100,0,0, 0,50,0, 45,20,0, — 45,20,0 }; static final byte colors[] = { 0,0,255,255, 0,255,0,255, 255,0,0,255, 255,255,255,255, 255,0,255,255, 255,255,0,255, 0,255,255,255 }; static final int indices[] = { 0,3,1, 1,3,2, 4,6,5 }; static final int strips[] = { 3, 3, 3 }; Note that since M3G only supports triangle strips, individual triangles must be declared as strips of three vertices each. This is not a problem when exporting from a content creation tool that creates the strips automatically, but is a small nuisance when constructing small test applications by hand. In this case, we could also easily combine the first two t riangles into a single strip like this: static final int indices[] = { 0,3,1,2, 4,6,5 }; static final int strips[] = { 4, 3 }; Once we have the vertex and index data in place, we can create the objects representing our polygon mesh. You would typically place this code in the initialization phase of your application. // Create the vertex arrays VertexArray myPositionArray = new VertexArray(7, 3, 1); VertexArray myColorArray = new VertexArray(7, 4, 1); // Set values for 7 vertices starting at vertex 0 myPositionArray.set(0, 7, positions); myColorArray.set(0, 7, colors); // Create the vertex buffer; for the vertex positions, // we set the scale to 1.0 and the bias to zero VertexBuffer myVertices = new VertexBuffer(); myVertices.setPositions(myPositionArray, 1.0f, null); myVertices.setColors(myColorArray); // Create the indices for five triangles as explicit triangle strips IndexBuffer myTriangles = new TriangleStripArray(indices, strips); 326 LOW-LEVEL MODELING IN M3G CHAPTER 14 // Use the default shading parameters Appearance myAppearance = new Appearance(); // Set up a modeling transformation Transform myModelTransform = new Transform(); myModelTransform.postTranslate(0.0f, 0.0f, — 150.0f); With all of that done, we can proceed to rendering the mesh. You will normally do this in the paint method for your MIDP Canvas. void paint(Graphics g) { Graphics3D g3d = Graphics3D.getInstance(); try { g3d.bindTarget(g); g3d.clear(null); g3d.render(myVertices, myTriangles, myAppearance, myModelTransform); } finally { g3d.releaseTarget(); } } Assuming everything went well, you should now see your geometry in the middle of the screen. You can play around with your vertex and index setup, modeling transformation, and camer a settings to see what happens. 14.2 ADDING COLOR AND LIGHT: Appearance You can now create a plain polygon mesh and draw it. To make your meshes look more interesting, we will next take a closer look at the Appearance class we met in the pre- vious section. This is one of the most powerful classes in M3G, providing a wide range of control over the rendering and compositing process of each mesh. An Appearance object is needed for everything you render in M3G, so let us begin with the simplest pos- sible example—use the default rendering parameters as we already did above. Appearance myAppearance = new Appearance(); In fact, the Appearance class in itself does very little. There is only one piece of data nativetoanAppearance object—the rendering layer—and we will not need that until we start using the M3G scene graph. Instead, the functionality of Appearance is split into five component classes. Each of the component classes wraps a logical section of the low-level rendering pipeline, so that together they cover most of the rendering state of OpenGL ES. You can then collect into an Appearance object only the state you want to control explicitly and leave the rest to null, saving you from the hassle of doing a lot of settings, and letting you share state data between different meshes. We will see how this works in practice, as we follow the rendering pipeline through the individual component classes. SECTION 14.2 ADDING COLOR AND LIGHT: Appearance 327 14.2.1 PolygonMode The PolygonMode class affects how your input geometry is interpreted and treated at a triangle level. It allows you to set your winding, culling, and shading modes, as well as control some lighting parameters and perspective correction. By default, M3G assumes that your input triangles wind counterclockwise and that only the front side of each triangle should be drawn and lit. Triangles are shaded using Gouraud shading, and local camera lighting and perspective correction are not explicitly required. You can overr ide any of these settings by creating a PolygonMode object, specify- ing the settings you want to change, and including the object into your Appearance object. For example, to render both sides of your mesh with full lighting and perspective correction, use PolygonMode as follows: PolygonMode myPolygonMode = new PolygonMode(); myAppearance.setPolygonMode(myPolygonMode); myPolygonMode.setCulling(PolygonMode.CULL_NONE); myPolygonMode.setTwoSidedLightingEnable(true); myPolygonMode.setPerspectiveCorrectionEnable(true); myPolygonMode.setLocalCameraLightingEnable(true); For the setCulling function, you can set any of CULL_BACK, CULL_FRONT, and CULL_NONE.ThesetTwoSidedLightingEnable function controls whether the vertex normals are flipped when computing lighting for the back side of triangles (should they not be culled), and setWinding controls which side of your triangles is the front side. For setWinding, you have the options WINDING_CCW and WINDING_CW. Additionally, there is setShading, where the default of SHADE_SMOOTH produces Gouraud-shaded and SHADE_FLAT flat-shaded triangles. You may wish to refer to Section 9.1 for the equivalent OpenGL ES functions. Finally, it is worth pointing out that the perspective correction and local camera lighting flags are only hints to the implementation. The very low-end implementations may not support perspective correction at all, and local camera lighting is unsupported in most implementations that we know of. If supported, both come at a cost, especially on soft- ware renderers, so you should pay attention to only using them where necessary. Do use them where necessary, though: when rendering slanted, textured surfaces made of large triangles, the possible performance gain of disabling perspective correction is not usually worth the resulting v isual artifacts. Pitfall: There is quite a lot of variety in the speed and quality of p erspective correction among different M3G implementations. What works for one implementation, may not work for others. For quality metrics you can refer to benchmark applications such as JBenchmark. 328 LOW-LEVEL MODELING IN M3G CHAPTER 14 14.2.2 Material The Material class is where you specify the lighting parameters for a mesh in M3G. Putting a non-null Material into your Appearance implicitly enables lighting for all meshes rendered using that Appearance. M3G uses the traditional OpenGL lighting model as explained in Section 3.2. If you are familiar with OpenGL lighting (see Section 8.3.1), you will find the same parameters in M3G. The setColor(int target, int argb) function lets you set each of the material parameters with target set to AMBIENT, DIFFUSE, SPECULAR, and EMISSIVE, respectively. The alpha component of the color is only used for DIFFUSE.You can also make the ambient and diffuse components track the vertex color with setVertexColorTrackingEnable(true). Additionally, you can specify the specular exponent with setShininess. If you want something resembling red plastic, you could set it up like this: redPlastic = new Material(); redPlastic.setColor(Material.AMBIENT, 0xFF0000); // red redPlastic.setColor(Material.DIFFUSE, 0xFFFF0000); // opaque red redPlastic.setColor(Material.SPECULAR, 0xFFFFFF); // white redPlastic.setColor(Material.EMISSIVE, 0x000000); // black redPlastic.setShininess(2.0f); A shinier material, something like gold, could look like this: golden = new Material(); golden.setColor(Material.AMBIENT, 0xFFDD44); // yellowish orange golden.setColor(Material.DIFFUSE, 0xFFFFDD44); // opaque yellowish orange golden.setColor(Material.SPECULAR, 0xFFDD44); // yellowish orange golden.setColor(Material.EMISSIVE, 0x000000); // black golden.setShininess(100.0f); You can also bitwise-OR the color specifiers for setColor, for example setColor (Material.AMBIENT | Material.DIFFUSE, 0xFFFFFFFF), to set multiple components to the same value. Materials need light to interact with. If you try to use Material alone, only the emissive component will produce other than black results. Light is provided through light sources, which we will discuss later in Section 14.3.2, but for a quick start, you can just create a default light source and put it into your Graphics3D like this: Light myLight = new Light(); g3d.addLight(myLight, null); Since both the light and the camera have the same transformation (now null), that light will be shining from the origin in the same direction as your camera is looking, and you should get some light on the materials. SECTION 14.2 ADDING COLOR AND LIGHT: Appearance 329 14.2.3 Texture2D Texturing lets you add detail beyond vertex positions, colors, and normals to your surfaces—look at the low-polygon bikers in Figure 14.1 for an example. After lighting, your triangles are rasterized and converted into fragments or, roughly, individual pixels. Texturing then takes an Image2D and combines that with the interpolated post-lighting color of each fragment using one of a few predefined functions. To enable texturing, add a Texture2D object into your Appearance. A valid texture image must be specified at all times, so the constructor takes a reference to an Image2D object. You can,however, change the image later with a call to setImage. Texture images must have power-of-two dimensions, and neither dimension may exceed the maximum texture dimension queriable with Graphics3D.getProperties. Assuming that we have such an image, called myTextureImage, we can proceed: Texture2D myTexture = new Texture2D(myTextureImage); myAppearance.setTexture(0, myTexture); Note the 0 in the setTexture call: that is the index of the texturing unit. At least one unit is guaranteed to exist, but multi-texturing support is optional for M3G implementa- tions. You can query the number of texturing units available in a particular implementa- tion, again via Graphics3D.getProperties. If the implementation supports two texturing units, you will also have unit 1 at your disposal, and so forth. In this case each additional texturing unit further modifies the output of the prev ious unit. Wrapping and filtering modes When sampling from the texture image, M3G takes your input texture coordinates, interpolated for each fragment, and maps them to the image. The top left-hand corner of the image is the origin, (0, 0). The bottom right corner is (1, 1). By default, the texture coordinates wrap around so that if your coordinates go from −1.0 to +3.0, for example, Figure 14.1: Texturing can add a lot of detail to low-polygon models, allowing large numbers of them on-screen without excessive geometry loads. (Images copyright c Digital Chocolate.) 330 LOW-LEVEL MODELING IN M3G CHAPTER 14 the texture image will repeat four times. You can control this behavior with setWrapping(int wrapS, int wrapT), where wrapS and wrapT can be either WRAP_REPEAT or WRAP_CLAMP. The latter will, instead of repeating, clamp that coordinate to the center of the edge pixel of the image. These are equivalent to the texture wrapping modes in OpenGL ES (Section 9.2.4). If your texture has a pattern that is only designed to tile smoothly in the hor izontal direction, for example, you may want to disable wrapping in the vertical direction with myTexture.setWrapping(Texture2D.WRAP_REPEAT, Texture2D.WRAP_CLAMP); Once the sampling point inside the texture is determined, M3G can either pick the clos- est texel or perform some combination of mipmapping and bilinear filtering, similarly to OpenGL ES (Section 9.2.3). This is controlled with setFiltering(int levelFilter, int imageFilter). You can choose between FILTER_NEAREST and FILTER_LINEAR for imageFilter, to use either point sampling or bilinear filtering within each mipmap image. For levelFilter, you can choose the same for nearest or linear filtering between mipmap levels, or FILTER_BASE_LEVEL to use just the base-level image. If you enable mipmapping, the other mipmap levels will be automatically generated from the base-level image. However, all filtering beyond point-sampling the base level is optional; you will encounter a lot of devices that do not even support mipmapping. Performance tip: Always enable mipmapping. Not only does it make your graphics look better, it also allows the underly ing renderer to save valuable memory bandwidth and spend less time drawing your better-looking graphics. In rare cases, you may want to opt for the small memory saving of not using mipmapping, but depending on the M3G implementation, this saving may not even be realized in practice. Unlike mipmapping, choosing between FILTER_NEAREST and FILTER_ LINEAR is a valid trade-off between quality and performance, especially when using a software renderer. Texture application Once the texture samples are fetched, they are combined with the input fragments accord- ing to the texture blending function you choose—blending was a somewhat unfortunate choice of name here, as it is easy to confuse with frame-buffer blending, but we shall have to live with that. The setBlending function lets you select one of FUNC_ADD, FUNC_BLEND, FUNC_DECAL, FUNC_MODULATE, and FUNC_REPLACE. These directly correspond to the texture functions described in Section 3.4.1—refer there for details on how each function works. The texture blend color used by FUNC_DECAL can be set via setBlendColor. As an example, a common case in texturing is combining a texture with per-vertex light- ing; it makes no difference whether you have M3G compute the lighting dynamically or use an off-line algorithm to bake the lighting into per-vertex colors—the texture is applied SECTION 14.2 ADDING COLOR AND LIGHT: Appearance 331 the same way. To do this, we only need to modulate (multiply) the interpolated frag ment colors with a texture. Assuming we have, say, a repeating brick pattern in an Image2D called brickImage: // Create a repeating texture image to multiply with the incoming color Texture2D myTexture = new Texture2D(brickImage); myTexture.setWrapping(Texture2D.WRAP_REPEAT, Texture2D.WRAP_REPEAT); myTexture.setBlending(Texture2D.FUNC_MODULATE); myTexture.setFiltering(Texture2D.FILTER_NEAREST, Texture2D.FILTER_NEAREST); // Set as the first texture to an Appearance object created earlier; // the other texture slots are assumed to be null myAppearance.setTexture(0, myTexture); In fact, WRAP_REPEAT and FUNC_MODULATE are the default settings for a Texture2D, so the related two lines in the example above could be skipped. Depending on your target hardware, you may also want to experiment with different filtering modes to see which one is the best compromise between performance and image quality. Multi-texturing If you are targeting an M3G implementation capable of multi-texturing, you may want to bake your static lighting into a light map texture instead—this lets you get detailed lighting without excess vertices, which can be useful if the vertex transformations would otherwise become the performance bottleneck; for example, if rasterization is hardware- accelerated but transformations are done in software. If you have your light map in an Image2D called lightmapImage, you could then implement the above example using two textures only, without any per-vertex colors or lighting: // Create the textures for the brick pattern and our light map. // We omit the default wrapping settings for the brick image; // light maps do not normally repeat, so we clamp that Texture2D myTexture = new Texture2D(brickImage); myTexture.setFiltering(Texture2D.FILTER_NEAREST, Texture2D.FILTER_NEAREST); Texture2D myLightmap = new Texture2D(lightmapImage); myLightmap.setFiltering(Texture2D.FILTER_NEAREST, Texture2D.FILTER_LINEAR); myLightmap.setWrapping(Texture2D.WRAP_CLAMP, Texture2D.WRAP_CLAMP); // Create the final fragment color by just multiplying the two textures myAppearance.setTexture(0, myLightmap); myLightmap.setBlending(Texture2D.FUNC_REPLACE); myAppearance.setTexture(1, myTexture); myTexture.setBlending(Texture2D.FUNC_MODULATE); Note that you will also need to include texture coordinates in your VertexBuffer for each texturing unit you are using. With multi-texturing, however, you may be able to share the same coordinates among many of your textures. 332 LOW-LEVEL MODELING IN M3G CHAPTER 14 Pitfall: As in OpenGL, textures in M3G are applied after lighting. If you want your lighting to modulate the texture, which is the common case when representing surface detail with textures, this only works well w ith diffuse reflection. You should then render a second, specular-only pass to get any kind of specular highlights on top of your texture, or use multi-texturing and add a specular map. Either of these will give you the effect you can see in the specular highlight in Figure 3.2—compare the images with and without a separ ate specular pass. Texture transformations Now that we have covered the basics, note that the Texture2D class is derived from Transformable. This means that you can apply the full transformation functionality to your texture coordinates prior to sampling the texture. The transformation constructed via the Transformable functions is applied to the texture coordinates in exactly the same way as the modelview matrix is to vertex coordinates. Performance tip: The scale and bias parameters of VertexBuffer are all you should need for normal texturing. To avoid an unnecessary performance penalty, especially on software-only implementations, limit the use of the texture transformation to special effects that really need it. Finally, note that you can share the Image2D object used as the texture image with as many Texture2D objects as you want. This lets you use different texture transforma- tions, or even different wrapping and filtering modes on the same image in different use cases. If the texture image is mutable, you can also render into it for dynamic effects. 14.2.4 Fog ThenextcomponentofAppearance to affect your rendering results is Fog.Itisa fairly simple simulation of atmospheric effects that gets applied to your fragments after they have been textured. Let us add some Fog into our Appearance: myFog = new Fog(); myAppearance.setFog(myfog); This creates a default black fog that obscures everything more than one unit away from the camera, so it may not be very useful as such. To get something more like atmospheric perspective, let us set some parameters: myFog.setMode(Fog.EXPONENTIAL}; myFog.setDensity(0.01f); myFog.setColor(0x6688FF); // pale blue tint We have a choice between two flavors in the setMode function: EXPONENTIAL and LINEAR fog. For the former, we just set the density of the fog using setDensity.The SECTION 14.2 ADDING COLOR AND LIGHT: Appearance 333 latter has a linear ramp from no fog to fully fogged, specified with setLinear(float near, float far). Finally, there is the fog color, set via setColor. Refer to Section 3.4.4 for the details of fog arithmetic. Pitfall: Despite the name, setLinear does not make the fog LINEAR—you must set the fog mode and parameters separately: myFog.setMode(Fog.LINEAR); myFog.setLinear(0.0, 10.0); Note that there is no EXP2 fog mode in M3G, although it is frequently used in OpenGL (see Section 3.4.4). This was, again, done to drop one code path from proprietary software implementations; today, it may seem like rather an arbitrary choice. 14.2.5 CompositingMode After fog has been applied to your fragments, they are ready to hit the frame buffer. By default, anything you render is depth-tested and, should the depth test pass, replaces the previously existing frame buffer values. The CompositingMode class lets you control what is written to the frame buffer and how it blends with the existing pixels for com- positing and multi-pass rendering effects. myCompositingMode = new CompositingMode(); myAppearance.setCompositingMode(myCompositingMode); Fragment tests The first operation done on any fragment at the compositing stage is the alpha test. M3G simplifies this down to a single threshold alpha value that your fragment must have in order to pass. The threshold is set via setAlphaThreshold, and must have a value between zero and one. Any fragment with an alpha value less than the threshold gets rejected right away. The default value of 0.0 lets all pixels pass. A common use for the alpha channel is transparency, and you usually want to reject fragments with small alpha values so that the transparent regions do not mess up the depth buffer: myCompositingMode.setAlphaThreshold(0.5f); Note that this is equivalent to enabling the alpha test in OpenGL ES and calling glAlphaFunc(GL_GEQUAL, 0.5f). See Section 9.5.2 for more details on the OpenGL ES functionality. Performance tip: The alpha test is the fastest way to discard individual fr agments, as it does not require a comparison with the depth buffer. For example, your rendering speed may improve by using alpha testing to discard transparent areas already before the blending stage. In practice, many implementations detect these discarded fragments much earlier in the rendering pipeline, providing savings from other stages as well. . contain- ing two strips, one with five and the other with three vertices; this translates into three and one triangles, respectively. static final int myStripIndices[]={0,3,1,4,5,7,6,8}; static. M3G and draw it. Let us illustrate this with a more comprehensive example where we create some colored triangles and render them. We assume that you have set up your Graphics3 D and Camera as described in. 150.0f); With all of that done, we can proceed to rendering the mesh. You will normally do this in the paint method for your MIDP Canvas. void paint (Graphics g) { Graphics3 D g3d = Graphics3 D.getInstance(); try