4. Enabling Textures with ES2::RenderingEngine
The ES 2.0 backend requires some changes to
both the vertex shader (to pass along the texture coordinate) and the
fragment shader (to apply the texel color). You do not call
glEnable(GL_TEXTURE_2D) with ES 2.0; it simply
depends on what your fragment shader
does.
Let’s start with the vertex shader, shown in
Example 10.
Example 10. SimpleLighting.vert with texture
attribute vec4 Position; attribute vec3 Normal; attribute vec3 DiffuseMaterial; attribute vec2 TextureCoord;
uniform mat4 Projection; uniform mat4 Modelview; uniform mat3 NormalMatrix; uniform vec3 LightPosition; uniform vec3 AmbientMaterial; uniform vec3 SpecularMaterial; uniform float Shininess;
varying vec4 DestinationColor; varying vec2 TextureCoordOut;
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; TextureCoordOut = TextureCoord; }
|
Note:
To try these, you can replace the contents
of your existing .vert and
.frag files. Just be sure not to delete the first
line with STRINGIFY or the last line with the
closing parenthesis and semicolon.
Example 10
simply passes the texture coordinates through, but you can achieve many
interesting effects by manipulating the texture coordinates, or even
generating them from scratch. For example, to achieve a “movie
projector” effect, simply replace the last line in Example 5-10 with this:
TextureCoordOut = gl_Position.xy * 2.0;
For now, let’s stick with the boring
pass-through shader because it better emulates the behavior of ES 1.1.
The new fragment shader is a bit more interesting; see Example 11.
Example 11. Simple.frag with texture
varying lowp vec4 DestinationColor; varying mediump vec2 TextureCoordOut;
uniform sampler2D Sampler;
void main(void) { gl_FragColor = texture2D(Sampler, TextureCoordOut) * DestinationColor; }
|
When setting a uniform sampler from within
your application, a common mistake is to set it to the handle of the
texture object you’d like to sample:
glBindTexture(GL_TEXTURE_2D, textureHandle);
GLint location = glGetUniformLocation(programHandle, "Sampler");
glUniform1i(location, textureHandle); // Incorrect
glUniform1i(location, 0); // Correct
The correct value of the sampler is the stage
index that you’d like to sample from, not the handle. Since all uniforms
default to zero, it’s fine to not bother setting sampler values if
you’re not using multitexturing (we’ll cover multitexturing later in
this book).
Warning:
Uniform samplers should be set to the stage
index, not the texture handle.
Newly introduced in Example 5-11 is the texture2D
function call. For input, it takes a uniform sampler and a
vec2 texture coordinate. Its return value is always a
vec4, regardless of the texture format.
Warning:
The OpenGL ES specification stipulates that
texture2D can be called from vertex shaders as
well, but on many platforms, including the iPhone, it’s actually
limited to fragment shaders only.
Note that Example 5-11 uses multiplication to combine the
lighting color and texture color; this often produces good results.
Multiplying two colors in this way is called
modulation, and it’s the default method used in
ES 1.1.
Now let’s make the necessary changes to the
C++ code. First we need to add new class members to store the texture ID
and resource manager pointer, but that’s the same as ES 1.1, so I won’t
repeat it here. I also won’t repeat the texture-loading code because
it’s the same with both APIs.
One new thing we need for the ES 2.0 backend
is an attribute ID for texture coordinates. See Example 12. Note the lack of a
glEnable for texturing; remember, there’s no need for
it in ES 2.0.
Example 12. RenderingEngine.ES2.cpp
struct AttributeHandles { GLint Position; GLint Normal; GLint Ambient; GLint Diffuse; GLint Specular; GLint Shininess; GLint TextureCoord; };
...
void RenderingEngine::Initialize(const vector<ISurface*>& surfaces) {
vector<ISurface*>::const_iterator surface; for (surface = surfaces.begin(); surface != surfaces.end(); ++surface) {
// Create the VBO for the vertices. vector<float> vertices; (*surface)->GenerateVertices(vertices, VertexFlagsNormals|VertexFlagsTexCoords);
// ... m_attributes.TextureCoord = glGetAttribLocation(program, "TextureCoord");
// Load the texture. glGenTextures(1, &m_gridTexture); glBindTexture(GL_TEXTURE_2D, m_gridTexture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
m_resourceManager->LoadPngImage("Grid16.png"); void* pixels = m_resourceManager->GetImageData(); ivec2 size = m_resourceManager->GetImageSize(); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); m_resourceManager->UnloadImage();
// Initialize various state. glEnableVertexAttribArray(m_attributes.Position); glEnableVertexAttribArray(m_attributes.Normal); glEnableVertexAttribArray(m_attributes.TextureCoord); glEnable(GL_DEPTH_TEST);
... }
|
You may have noticed that the fragment shader
declared a sampler uniform, but we’re not setting it to anything in our
C++ code. There’s actually no need to set it; all uniforms default to
zero, which is what we want for the sampler’s value anyway.
Next up is the Render
method, which is pretty straightforward (Example 5-13). The only way it differs from its ES
1.1 counterpart is that it makes three calls to
glVertexAttribPointer rather than
glVertexPointer, glColorPointer,
and glTexCoordPointer. (Replace everything from
// Draw the surface to the end of the method with the
corresponding code.)
Note:
You must also make the same changes to the
ES 2.0 renderer that were shown earlier in Example 7.
Example 13. ES2::RenderingEngine::Render with texture
void RenderingEngine::Render(const vector<Visual>& visuals) const { glClearColor(0.5f, 0.5f, 0.5f, 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) {
// ...
// Draw the surface. int stride = sizeof(vec3) + sizeof(vec3) + sizeof(vec2); const GLvoid* normalOffset = (const GLvoid*) sizeof(vec3); const GLvoid* texCoordOffset = (const GLvoid*) (2 * sizeof(vec3)); GLint position = m_attributes.Position; GLint normal = m_attributes.Normal; GLint texCoord = m_attributes.TextureCoord; 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, normalOffset); glVertexAttribPointer(texCoord, 2, GL_FLOAT, GL_FALSE, stride, texCoordOffset); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, drawable.IndexBuffer); glDrawElements(GL_TRIANGLES, drawable.IndexCount, GL_UNSIGNED_SHORT, 0); } }
|
That’s it! You now have a textured model
viewer. Before you build and run it, select Build→Clean All Targets (we’ve made a lot of changes
to various parts of this app, and this will help avoid any surprises by
building the app from a clean slate). I’ll explain some of the details
in the sections to come.