OpenGL ES 2.0 does not automatically perform
lighting math behind the scenes; instead, it relies on developers to
provide it with shaders that perform whatever type of lighting they
desire. Let’s come up with a vertex shader that mimics the math done by ES
1.1 when lighting is enabled.To keep things simple, we’ll use the infinite
light source model for diffuse
combined with the infinite viewer model for specular . We’ll also assume that the light is white.
Example 1 shows the pseudocode. Example 1. Basic lighting pseudocodevec3 ComputeLighting(vec3 normal) { N = NormalMatrix * normal L = Normalize(LightPosition) E = (0, 0, 1) H = Normalize(L + E) df = max(0, N ∙ L) sf = max(0, N ∙ H) sf = sf ^ Shininess return AmbientMaterial + DiffuseMaterial * df + SpecularMaterial * sf }
|
Note the NormalMatrix
variable in the pseudocode; it would be silly to recompute the
inverse-transpose of the model-view at every vertex, so we’ll compute it
up front in the application code then pass it in as the
NormalMatrix uniform. In many cases, it happens to be
equivalent to the model-view, but we’ll leave it to the application to
decide how to compute it. Let’s add a new file to the ModelViewer project
called SimpleLighting.vert for the lighting
algorithm. In Xcode, right-click the Shaders folder,
and choose Add→New file. Select the
Empty File template in the Other
category. Name it SimpleLighting.vert, and add
/Shaders after the project
folder name in the location field. Deselect the checkbox in the
Targets list, and click
Finish. Example 2
translates the pseudocode into GLSL. To make the shader usable in a
variety of situations, we use uniforms to store light position, specular,
and ambient properties. A vertex attribute is used to store the diffuse
color; for many models, the diffuse color may vary on a per-vertex basis
(although in our case it does not). This would allow us to use a single
draw call to draw a multicolored model.
Example 2. SimpleLighting.vert
attribute vec4 Position; attribute vec3 Normal; attribute vec3 DiffuseMaterial;
uniform mat4 Projection; uniform mat4 Modelview; uniform mat3 NormalMatrix; uniform vec3 LightPosition; uniform vec3 AmbientMaterial; uniform vec3 SpecularMaterial; uniform float Shininess;
varying vec4 DestinationColor;
void main(void) { vec3 N = NormalMatrix * Normal; vec3 L = normalize(LightPosition); vec3 E = vec3(0, 0, 1); vec3 H = normalize(L + E);
float df = max(0.0, dot(N, L)); float sf = max(0.0, dot(N, H)); sf = pow(sf, Shininess);
vec3 color = AmbientMaterial + df * DiffuseMaterial + sf * SpecularMaterial; DestinationColor = vec4(color, 1); gl_Position = Projection * Modelview * Position; }
|
Take a look at the pseudocode in Example 1; the vertex shader is an implementation
of that. The main difference is that GLSL requires you to qualify many of
the variables as being attributes, uniforms, or varyings. Also note that
in its final code line, Example 4-15 performs
the standard transformation of the vertex position, just as it did for the
nonlit case.
Warning: GLSL is a bit different from many other
languages in that it does not autopromote literals from integers to
floats. For example, max(0, myFloat) generates a
compile error, but max(0.0, myFloat) does not. On the
other hand, constructors for vector-based types do
perform conversion implicitly; it’s perfectly legal to write either
vec2(0, 0) or vec3(0.0,
0.0).
1. New Rendering EngineTo create the ES 2.0 backend to ModelViewer,
let’s start with the ES 1.1 variant and make the following changes, some
of which should be familiar by now: Copy the contents of
RenderingEngine.ES1.cpp into
RenderingEngine.ES2.cpp. Remove the _OES and
OES suffixes from the FBO code. Change the namespace from
ES1 to ES2. Change the two
#includes to point to the
ES2 folder rather than the
ES1 folder. Add the BuildShader
and BuildProgram methods . You must change all instances of
RenderingEngine2 to
RenderingEngine because we are using namespaces
to distinguish between the 1.1 and 2.0 renderers. Add declarations for
BuildShader and BuildProgram
to the class declaration. Add the #include for
iostream.
Now that the busywork is out of the way,
let’s add declarations for the uniform handles and attribute handles
that are used to communicate with the vertex shader. Since the vertex
shader is now much more complex than the simple pass-through program
we’ve been using, let’s group the handles into simple substructures, as
shown in Example 3. Add this code
to RenderingEngine.ES2.cpp, within the namespace
declaration, not above it. (The bold part of the listing shows the two
lines you must add to the class declaration’s
private: section.) Example 3. ES2::RenderingEngine structures#define STRINGIFY(A) #A #include "../Shaders/SimpleLighting.vert" #include "../Shaders/Simple.frag"
struct UniformHandles { GLuint Modelview; GLuint Projection; GLuint NormalMatrix; GLuint LightPosition; }; struct AttributeHandles { GLint Position; GLint Normal; GLint Ambient; GLint Diffuse; GLint Specular; GLint Shininess; };
class RenderingEngine : public IRenderingEngine { // ... UniformHandles m_uniforms; AttributeHandles m_attributes; };
|
Next we need to change the
Initialize method so that it compiles the shaders,
extracts the handles to all the uniforms and attributes, and sets up
some default material colors. Replace everything from the comment
// Set up various GL state to the end of the method
with the contents of Example 4. Example 4. ES2::RenderingEngine::Initialize() // Create the GLSL program. GLuint program = BuildProgram(SimpleVertexShader, SimpleFragmentShader); glUseProgram(program);
// Extract the handles to attributes and uniforms. m_attributes.Position = glGetAttribLocation(program, "Position"); m_attributes.Normal = glGetAttribLocation(program, "Normal"); m_attributes.Ambient = glGetAttribLocation(program, "AmbientMaterial"); m_attributes.Diffuse = glGetAttribLocation(program, "DiffuseMaterial"); m_attributes.Specular = glGetAttribLocation(program, "SpecularMaterial"); m_attributes.Shininess = glGetAttribLocation(program, "Shininess"); m_uniforms.Projection = glGetUniformLocation(program, "Projection"); m_uniforms.Modelview = glGetUniformLocation(program, "Modelview"); m_uniforms.NormalMatrix = glGetUniformLocation(program, "NormalMatrix"); m_uniforms.LightPosition = glGetUniformLocation(program, "LightPosition");
// Set up some default material parameters. glVertexAttrib3f(m_attributes.Ambient, 0.04f, 0.04f, 0.04f); glVertexAttrib3f(m_attributes.Specular, 0.5, 0.5, 0.5); glVertexAttrib1f(m_attributes.Shininess, 50);
// Initialize various state. glEnableVertexAttribArray(m_attributes.Position); glEnableVertexAttribArray(m_attributes.Normal); glEnable(GL_DEPTH_TEST);
// Set up transforms. m_translation = mat4::Translate(0, 0, -7);
|
Next let’s replace the
Render() method, shown in Example 5. Example 5. ES2::RenderingEngine::Render()void RenderingEngine::Render(const vector<Visual>& visuals) const { glClearColor(0, 0.125f, 0.25f, 1); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
vector<Visual>::const_iterator visual = visuals.begin(); for (int visualIndex = 0; visual != visuals.end(); ++visual, ++visualIndex) {
// Set the viewport transform. ivec2 size = visual->ViewportSize; ivec2 lowerLeft = visual->LowerLeft; glViewport(lowerLeft.x, lowerLeft.y, size.x, size.y); // Set the light position. vec4 lightPosition(0.25, 0.25, 1, 0); glUniform3fv(m_uniforms.LightPosition, 1, lightPosition.Pointer());
// Set the model-view transform. mat4 rotation = visual->Orientation.ToMatrix(); mat4 modelview = rotation * m_translation; glUniformMatrix4fv(m_uniforms.Modelview, 1, 0, modelview.Pointer()); // Set the normal matrix. // It's orthogonal, so its Inverse-Transpose is itself! mat3 normalMatrix = modelview.ToMat3(); glUniformMatrix3fv(m_uniforms.NormalMatrix, 1, 0, normalMatrix.Pointer());
// Set the projection transform. float h = 4.0f * size.y / size.x; mat4 projectionMatrix = mat4::Frustum(-2, 2, -h / 2, h / 2, 5, 10); glUniformMatrix4fv(m_uniforms.Projection, 1, 0, projectionMatrix.Pointer()); // Set the diffuse color. vec3 color = visual->Color * 0.75f; glVertexAttrib4f(m_attributes.Diffuse, color.x, color.y, color.z, 1); // Draw the surface. int stride = 2 * sizeof(vec3); const GLvoid* offset = (const GLvoid*) sizeof(vec3); GLint position = m_attributes.Position; GLint normal = m_attributes.Normal; const Drawable& drawable = m_drawables[visualIndex]; glBindBuffer(GL_ARRAY_BUFFER, drawable.VertexBuffer); glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, stride, 0); glVertexAttribPointer(normal, 3, GL_FLOAT, GL_FALSE, stride, offset); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, drawable.IndexBuffer); glDrawElements(GL_TRIANGLES, drawable.IndexCount, GL_UNSIGNED_SHORT, 0); } }
| That’s it for the ES 2.0 backend! Turn off
the ForceES1 switch in
GLView.mm, and you should see something very
similar to the ES 1.1.
|