CS424 Notes, 8 February 2012
- The Upside Down Image Problem [left out last time]
- OpenGL assumes that the y-axis points up; usual image coordinates assume that the y-axis points down. This means that your images will probably be upside down! You can fix this by modifying the t-coordinates, but an easier way is to call gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,true). This will flip texture images vertically. You only need to do this once, probably in an initialization function.
- Texture Units and Multiple Textures
- OpenGL had this nice setup with bindTexture, texImage2D, etc. -- and then they realized that sometimes you might want to apply more than one image texture to the same primitive. If you can only bind one texture at at time, how do you use two primitives at the same time.
- The somewhat kludgey answer is texture units. A texture unit holds a texture binding and its set of texture parameters. A texture unit is really just a way of feeding an image texture into the shader program. If a shader program uses several image textures, you need to work with several texture units. The texture that a texture unit feeds to the shader program is the one that is bound to the texture unit. So, to use multiple textures on a primitive, you just have to bind different texture objects to several different texture units before you draw the primitive. You can do this immediately before drawing the primitive, or earlier when you are loading the texture images.
- You only have to work with texture units when you are binding textures to
be used when rendering a primitive. You can only work with one texture unit at a
time. This is called the active texture. You select the active texture with
gl.activeTexture using one of:
gl.activeTexture( gl.TEXTURE0 ); gl.activeTexture( gl.TEXTURE1 ); // same as gl.activeTexture( gl.TEXTURE0 + 1 ); gl.activeTexture( gl.TEXTURE2 ); // same as gl.activeTexture( gl.TEXTURE0 + 2 );and so on. The number of available texture units is implementation dependent.
- After calling gl.activeTexture, you can call gl.bindTexture to specify the texture object that you want to bind to that texture unit.
- By default, the active texture is texture unit 0 (gl.TEXTURE0), and if you are only going to use one texture at a time, you don't really need to worry about texture units at all. You can go from using one texture to another using gl.bindTexture; you don't need to use texture units for that. (Note however that as I write this, the Safari web browser seems to require a call to gl.activeTexture(gl.TEXTURE0) for textures to work properly, even if you are only using one texture. I believe that this is a bug.)
- (It's actually even more complicated, since there are different kinds of textures. So far, we have only looked at gl.TEXTURE_2D textures. In fact, a texture unit can be bound to one texture of each type, not just to one single texture.)
- In a shader program, your interface to a texture image is a uniform variable of type sampler2D. A sampler2D is used to fetch texel data from a texture unit.
- You have to set the value of a sampler2D to tell it which texture unit to use. The value is simply an integer: 0, 1, 2,... to indicate texture unit 0, 1, 2, ...
- Set the value of the sampler with gl.uniform1i(samplerLoc,unit) where samplerLoc is the location of the sampler variable in the shader program, as returned by gl.getUniformLocation, and unit is the integer identifying the texture unit.
- If you never use more than one texture at a time and you are only using the
default texture unit gl.TEXTURE0, you would have one sampler2D variable in your
shader program, and you would set its value to 0. If the name of the sampler variable
is teximage, and if prog is the shader program object, for example, you could
var texLoc = gl.getUniformLocation( prog, "teximage" ); gl.uniform1i( texLoc, 0 );
- Suppose that you have two sampler2D variables in your shader program, named
teximage1 and teximage2. You have loaded two images into texture objects
identified by texid1 and texid2. To use the two texture images with the two
samplers, you would need something like the following code (not necessarily all in the same place in your
tex1Loc = gl.getUniformLocation( prog, "teximage1" ); gl.uniform1i( texLoc1, 0 ); tex2Loc = gl.getUniformLocation( prog, "teximage2" ); gl.uniform1i( texLoc2, 1 ); gl.activeTexture( gl.TEXTURE0 ); gl.bindTexture( gl.TEXTURE_2D, texid1 ); // "binds" texture to texture unit 0 gl.activeTexture( gl.TEXTURE1 ); gl.bindTexture( gl.TEXTURE_2D, texid2 ); // "binds" texture to texture unit 1
- Using Samplers
- Finally, you need to know how to use a sampler in the shader program to fetch a color from a texture. We're assuming that the texture image has been properly bound and that the value of the sampler has been properly set.
- Samplers are used in fragment shaders. A sampler named teximage would
be declared in the fragment shader as
uniform sampler2D teximage;
- The only thing that you can do with a sampler2D is use it to access
a shader. This is done with the function texture2D(sampler,coords)
where sampler is the sampler2D and coords is a vec2
giving the texture coordinates. This function returns a vec4 that
contains the color from the texture associated with the given texture coordinates.
This might look like
vec4 texcolor = texture2D( teximage, texcoords );You can then use the color any way you want to help compute the color of the fragment.
- Applying Textures
- Once you have a color from a texture in your fragment shader, what can you do with it? This is programming! You can do whatever you want
- One option is to just use the texture color directly as the color of the fragment.
Your fragment shader could then be one line, such as
gl_FragColor = texture2D( teximage, texcoords );
- Another possibility arises if in addition to the texture, you also have a varying or uniform variable that assigns a color to the fragment. You can think of this color as being the "base color" of the fragment, and you can then combine the color from the texture with the base color to produce the color of the fragment.
- A common operation -- probably the most common -- is to multiply the base color
by the texture color. This could be done something like
vec4 texcolor = texture2D( teximage, texcoords ); gl_FragColor = basecolor * texcolor;or, if you only want to multiply the RGB components
vec4 texcolor = texture2D( teximage, texcoords ); gl_FragColor = vec4( basecolor.rgb * texcolor.rbg, 1.0 );
- It's also possible to add the texture color to the base color, but note that color components are automatically clamped to the range 0.0 to 1.0. This means that if the base color is already white, adding a color to that can only produce white. Similarly, you could subtract the base color from the texture color, but note that subtracting a color from black can only produce black.
- If the texture image has an alpha component, you could use the alpha component to blend the colors. The alpha component would give the degree of blending. You would then compute the RGB components of the fragment color as baseColor.rgb*(1-alpha) + texcolor.rgb*alpha, where alpha is texcolor.a.
- Another way of using the alpha component of the texture is to just copy the alpha of the texture color to the fragment color. If you do that, the texture is being used to modulate the transparency of the primitive, rather than to set its color. Later, when we do materials and lighting for 3D graphics, there will be other properties that can be "modulated" by the texture color.
- Applying Multiple Textures
- When you have several samplers in your fragment shader, you get a color from each of them. You then have to decide how to combine those colors with each other (and possibly with a base color) to produce the fragment color.
- You should think of this as a pipeline: Start with the base color (or first texture color), apply a texture color to it in any way you like, then apply the next texture color, and so on. After applying all the textures, you have the fragment color.