OpenGL ES 1.1 doesn't come equipped with Sphere Mapping. This is in contrast to its desktop non-ES counterpart. Sphere mapping is a fairly important part of any rendering engine, so having support for it is pretty much a requirement. Thankfully it is easy to reproduce in OpenGL ES. In fact, if you can tolerate 'fake' Sphere Mapping, where you only need the impression of the effect (and not absolutely accurate reflection) you can push the entire calculation to the GPU. Noel Llopis has written about the latter ('Normal Environment Mapping') in a chapter of the recently released iPhone Advanced Projects. However, if you need accurate Sphere Mapping, you can still achieve the effect on the CPU. Here's how I went about reproducing it in UtopiaGL.
Sphere Mapping
Sphere mapping is one of the easiest ways to implement Environment Mapping. It uses a single, 2D texture, which contains a warped capture of the environment (it looks a little like what you'd expect a chrome sphere would reflect, although that isn't entirely accurate).
The front part of the sphere is essentially what you're seeing when you look at a Sphere map, but the back side is also captured, and heavily compressed into the circular boundary of the image. Given a Reflection vector, you can calculate a texture index (s,t) into this Sphere Map to pull out the correct texel to achieve perfect Environment Reflection at a particular vertex.
Reflection
The mathematics of Sphere Mapping are straight forward - it's a simple reflection with an additional indexing step. You can find a full description of how it works in Desktop OpenGL here. Essentially, the process is this.
Then for each Vertex:
- Calculate the Normalized Vector from the Object Space Eye to the Object Space Vertex.
-
Calculate the Reflection Vector given the Object Space Vertex Normal
-
Transform the Reflection Vector to World Space using the Inverse Transpose of the Object to World matrix.
-
Given the World Space Reflection Vector, use it to build (s,t) coordinates to index the Sphere Map.
utMat4 m = _pRefFrame->GetInvTranspose();
// pV, pN and pTC point to the XYZ, Normal and Texture Coordinate attributes of a single vertex.
for( int i=0; i<numVerts; i++, pV+=vstride, pN+=nstride, pTC+=tstride )
{
// Calculate the vector from Object Space Eye to the Vertex
viewVec = _refFrameEye - *(utVec3*)pV;
viewVec.NormalizeFast();
// Reflect it
float d = viewVec.Dot( *(utVec3*)pN );
reflectedVec = *(utVec3*)pN * (2.f*d) - viewVec;
// Transform it to World Space
m.Multiply3( reflectedVec, reflectedVec );
// Index into Sphere Map. Optimization: this code takes the Reciprocal Square Root instead of the 1 / sqrt()
float p = utMath::RSqrt(
reflectedVec.v[0] * reflectedVec.v[0] +
reflectedVec.v[1] * reflectedVec.v[1] +
(reflectedVec.v[2]+1) * (reflectedVec.v[2]+1) ) * .5f;
((float*)pTC)[0] = .5f + reflectedVec.v[0] * p;
((float*)pTC)[1] = .5f + reflectedVec.v[1] * p;
}
Transforming Normals
~ 3D Bookshelf ~
Robin Hood Edition
Free Download!
3D Bookshelf - Robin Hood Edition uses the world's first fully 3D eBook engine.
Download it for free now!
If you are new to OpenGL, something that can trip you up is transformation of normals. Unlike vertices, normals don't have a location in space, just an orientation. They are also not subject to scaling, as vertices are. The MODELVIEW matrix can incorporate scaling, translation, sheering etc. on top of simple rotation, so how can you make use of it to transform normals, in such a way to preserve their direction and length? The answer is to do what OpenGL does under the hood. It multiplies normals by the Inverse Transpose of the MODELVIEW matrix. This is actually very easy to understand. When a matrix is orthogonal (its basis vectors are all at 90 degrees to each other), normalized (no scaling going on), and has no translation (so it just encodes rotation), you can calculate the inverse with a single trivial operation: the Transpose. What the Inverse Transpose does, is take the inverse of the MODELVIEW (which brings you from World Space to the Identity Reference Frame) and then take the Transpose of that Matrix (to bring us back to World Space, except without any Scale, Sheer, translation etc). This is perfect for transforming Normals.
Optimizations
The above code makes use of the Reciprocal Square Root function, which you can read about here. If you are doing multipass rendering you can of course cache the results of the above and reuse the texture coordinates on subsequent passes. UtopiaGL allows caching of this data in its Shader pipeline.
Uses
The primary reason to use accurate Sphere Mapping is when you need to provide a very clean (and controllable) reflection of the environment. I needed this in a recent contract for a high profile British artist. The work involved drawing a highly detailed, reflective (and refractive) object where we needed to have good control over Specular and other Diffuse environmental contributions to the lighting of the object. While it is slower than the alternatives, it is not that slow, particularly if you are clever about how you model the objects in a scene.
Conclusion
OpenGL ES 1.1 may not support Sphere Mapping but nothing prevents you from implementing it yourself. There are many ways to go about it, each with performance and quality tradeoffs. The above method perfectly reproduces the type of Sphere mapping Desktop OpenGL uses.
This is part 2 in a series of posts describing how to extend Blender to fit with your own content production process, specifically with regard to producing content for iPhone.
The 2.5 release of Blender is just around the corner. From the looks of the feature improvements already described on the Blender site we can expect some substantial changes to almost every aspect of the application. One significant area is the UI, in particular scripting updates to the UI.
What I'll describe here is relevant to scripting a UI for an exporter using the 2.49.2 release of Blender. Below is a snapshot of the UtopiaGL exporter in Blender.
One of the things that bugged me about setting up a UI for the UtopiaGL exporter was that Blender exposes a pretty low-level API for this purpose. This means that you end up dealing with absolute coordinates when positioning textboxes and buttons and so on. From looking at some of the existing Blender exporter plugins, the following type of UI code is not uncommon.
def draw_gui():
... # globals removed to save space!
# Title
glClear(GL_COLOR_BUFFER_BIT)
glRasterPos2d(10, 290)
Text("UtopiaGL .model Export")
# VNormals / UVs / VColors / VWeights
Label( "Properties To Export", 430, 190, 120, 20 )
g_toggle_outputvnormals = Toggle("Vertex Normals", EVENT_NOEVENT, 430, 160, 100, 20, g_toggle_outputvnormals.val, "Output Vertex Normals" )
g_toggle_outputuvs = Toggle("Vertex UVs", EVENT_NOEVENT, 540, 160, 100, 20, g_toggle_outputuvs.val, "Output Vertex UV Coordinates" )
g_toggle_outputvcolors = Toggle("Vertex Colors", EVENT_NOEVENT, 430, 130, 100, 20, g_toggle_outputvcolors.val, "Output Vertex Colors" )
g_toggle_outputvweights = Toggle("Vertex Weights", EVENT_NOEVENT, 540, 130, 100, 20, g_toggle_outputvweights.val, "Output Vertex Weights" )
Label( "Faces", 430, 90, 80, 20 )
g_menu_facewinding = Menu("Face Winding %t|Counter-Clockwise %x1|Clockwise %x2|", EVENT_NOEVENT, 430, 60, 150, 20, g_menu_facewinding.val, "Face winding to use" )
# Content Root / Model File
Label( "Content Root Path", 10, 240, 80, 20 )
g_content_root = String("", EVENT_NOEVENT, 10, 210, 300, 20, g_content_root.val, 255, "Content root path")
Button( "Browse", EVENT_CHOOSE_CONTENT_ROOT, 310, 210, 80, 20 )
Label( "Model File", 10, 180, 80, 20 )
g_filename = String("", EVENT_NOEVENT, 10, 150, 300, 20, g_filename.val, 255, "Model file to save")
Button( "Browse", EVENT_CHOOSE_FILENAME, 310, 150, 80, 20 )
g_toggle_outputshaders = Toggle("Output Shaders/Skins", EVENT_NOEVENT, 10, 120, 130, 20, g_toggle_outputshaders.val, "Output Shader and Skin files" )
# Log / Log Level
Label( "Logging", 10, 90, 80, 20 )
g_toggle_outputtolog = Toggle("Output Log", EVENT_NOEVENT, 10, 60, 80, 20, g_toggle_outputtolog.val, "Output export progress to log file" )
Label( "Log Level", 160, 60, 80, 20 )
g_integer_loglevel = Menu("Log Level %t|Debug %x1|Info %x2|Warning %x3|Error %x4|Critical %x5", EVENT_NOEVENT, 230, 60, 80, 20, g_integer_loglevel.val, "Logging Level to use" )
# Export / Exit
Button( "Export", EVENT_SAVE_MODEL, 10, 10, 80, 20 )
Button( "Exit", EVENT_EXIT ,100, 10, 80, 20 )
The above approach works and if your UI is simple and not subject to change you should be fine to implement a UI like this. If on the other hand, you plan to iteratively extend the exporter as and when new requirements appear, you'll probably want something a little more dynamic. For example, inserting a button in the middle of a UI implemented like this means calculating and changing the coordinates of all other UI elements in the surrounding area.
Silverlight and WPF have various types of Panel controls that makes insertion and removal of controls in a layout very easy. I didn't have time to implement a complete panel control, so instead I implemented a Cursor object that allows me to output UI elements at the current location of the Cursor.
class Cursor:
"""A Cursor object, use to maintain context of where to insert UI elements."""
def __init__(self):
self.x = 10
self.y = 10
self.width = 80
self.height = 20
self.virticalpad = 10
self.horizontalpad = 10
def set_x(self, newx):
self.x = newx
def set_y(self, newy):
self.y = newy
def set_width(self, newwidth):
self.width = newwidth
def set_height(self, newheight):
self.height = newheight
def set_virtical_pad(self, newvirticalpad):
self.virticalpad = newvirticalpad
def set_horizontal_pad(self, newhorizontalpad):
self.horizontalpad = newhorizontalpad
def move_up(self):
self.y = self.y + (self.height + self.virticalpad)
def move_down(self):
self.y = self.y - (self.height + self.virticalpad)
def offset_up(self, up):
self.y = self.y + up
def offset_down(self, down):
self.y = self.y - down
def move_left(self):
self.x = self.x - (self.width + self.horizontalpad)
def move_right(self):
self.x = self.x + (self.width + self.horizontalpad)
def offset_left(self, left):
self.x = self.x - left
def offset_right(self, right):
self.x = self.x + right
def Button(self, title, handle):
Button(title, handle, self.x, self.y, self.width, self.height)
def Toggle(self, title, handle, value, tip):
return Toggle(title, handle, self.x, self.y, self.width, self.height, value, tip)
def Label(self, title):
Label(title, self.x, self.y, self.width, self.height)
def Menu(self, options, handle, value, tip):
return Menu(options, handle, self.x, self.y, self.width, self.height, value, tip)
def String(self, text, handle, value, l, tip):
return String(text, handle, self.x, self.y, self.width, self.height, value, l, tip)
By using the cursor we can convert the hard-coded UI code from above to look more like the following. Note, this code now makes it much easier to add and remove UI elements without the need to update the coordinates of the surrounding elements. It does increase the amount of code a little, but the trade off is probably worth it.
def draw_gui():
... # globals removed to save space!
glClear(GL_COLOR_BUFFER_BIT)
# Export / Exit
cursor = Cursor()
cursor.Button( "Export", EVENT_SAVE_MODEL)
cursor.move_right()
cursor.Button( "Exit", EVENT_EXIT)
# Log / Log Level
cursor.move_left()
cursor.move_up()
cursor.offset_up(20)
g_toggle_outputtolog = cursor.Toggle("Output Log", EVENT_NOEVENT, g_toggle_outputtolog.val, "Output export progress to log file")
cursor.offset_right(150)
cursor.Label( "Log Level" )
cursor.move_right()
g_integer_loglevel = cursor.Menu("Log Level %t|Debug %x1|Info %x2|Warning %x3|Error %x4|Critical %x5", EVENT_NOEVENT, g_integer_loglevel.val, "Logging Level to use")
cursor.set_x(10)
cursor.move_up()
cursor.Label( "Logging")
# Content Root / Model File
cursor.move_up()
cursor.set_width(130)
g_toggle_outputshaders = cursor.Toggle("Output Shaders/Skins", EVENT_NOEVENT, g_toggle_outputshaders.val, "Output Shader and Skin files" )
cursor.move_up()
cursor.set_width(300)
g_filename = cursor.String("", EVENT_NOEVENT, g_filename.val, 255, "Model file to save" )
cursor.offset_right(300)
cursor.set_width(80)
cursor.Button( "Browse", EVENT_CHOOSE_FILENAME )
cursor.move_up()
cursor.set_x(10)
cursor.Label( "Model File")
cursor.move_up()
cursor.set_width(300)
g_content_root = cursor.String("", EVENT_NOEVENT, g_content_root.val, 255, "Content root path" )
cursor.offset_right(300)
cursor.set_width(80)
cursor.Button( "Browse", EVENT_CHOOSE_CONTENT_ROOT)
cursor.move_up()
cursor.set_x(10)
cursor.Label( "Content Root Path")
# Title
cursor.move_up()
cursor.move_up()
cursor.set_width(300)
cursor.Label( "UtopiaGL .model Export")
# VNormals / UVs / VColors / VWeights
cursor.set_x(430)
cursor.set_y(10)
cursor.move_up()
cursor.offset_up(20)
g_menu_facewinding = cursor.Menu("Face Winding %t|Counter-Clockwise %x1|Clockwise %x2|", EVENT_NOEVENT, g_menu_facewinding.val, "Face winding to use" )
cursor.move_up()
cursor.Label( "Faces")
cursor.move_up()
cursor.offset_up(10)
cursor.set_width(100)
g_toggle_outputvcolors = cursor.Toggle("Vertex Colors", EVENT_NOEVENT, g_toggle_outputvcolors.val, "Output Vertex Colors")
cursor.move_right()
g_toggle_outputvweights = cursor.Toggle("Vertex Weights", EVENT_NOEVENT, g_toggle_outputvweights.val, "Output Vertex Weights")
cursor.move_left()
cursor.move_up()
g_toggle_outputvnormals = cursor.Toggle("Vertex Normals", EVENT_NOEVENT, g_toggle_outputvnormals.val, "Output Vertex Normals")
cursor.move_right()
g_toggle_outputuvs = cursor.Toggle("Vertex UVs", EVENT_NOEVENT, g_toggle_outputuvs.val, "Output Vertex UV Coordinates")
cursor.move_left()
cursor.move_up()
cursor.set_width(120)
cursor.Label( "Properties To Export")
Here's hoping the improvements made in the 2.5 release make this stuff redundant. But for now, this is a manageable way to implement an exporter UI in Blender to make updates and modifications a little easier.
I didn't get around to writing about the Material to Shader mismatch that I mentioned in the previous post. So, there's definitely going to be a part 3 to this series at a minimum. Hopefully, I'll get to it soon.