- Intro
The complete code for this tutorial can be found in "src/examples/Applets/App_Texturing.cpp"
.
"Texturing" is the technique used to apply in various ways an image over the surface of an object. Textures can be used to change or modify the color, the transparency and even the lighting properties of an object. In this tutorial we will go through the implementation of App_Texturing, the texturing test part of the Visualization Library regression test suite. This will give us the chance to see how to use several standard and advanced texturing techniques like 2D texturing, multi-texturing, 3D textures, 1D and 2D texture arrays, sphere-mapping and cubemaps environment mapping. We will also see how to use the texture lod bias to simulate opaque reflections.
The most important classes involved in the texturing process are:
- Textures with Visualization Library
Currently Visualization Library supports all kinds of textures supported by OpenGL up to OpenGL 4.1 which is:
- 1D textures
- 2D textures
- 3D textures
- Cubemap textures
- Texture rectangles
- 1D texture arrays
- 2D texture arrays
- Texture buffer objects
- 2D multisample textures
- 2D multisample array textures
You can setup and create textures in three different ways:
- Using a
vl::Texture
constructor.
With this method you can create 1D, 2D and 3D textures. The actual OpenGL texture object is immediately created.
- Using one of
vl::Texture::prepare*
() methods.
With this method you can create all supported texture types. The actual OpenGL texture is not created immediately but rather the first time it is used in a rendering.
- Using one of
vl::Texture::create*
() methods.
With this method you can create all supported texture types. The actual OpenGL texture is created immediately.
This is equivalent to calling a vl::Texture::prepare*
() method followed by vl::Texture::createTexture()
.
Notes on vl::Image:
- In order to create 1D texture arrays you need a 2D image, in which every y slice will be considered a layer of the 1D texture array
- In order to create 2D texture arrays you need a 3D image, in which every z slice will be considered a layer of the 2D texture array.
- When creating 1D textures make sure you have a 1D image not a 1px high 2D image. VL can distinguish between 1D images and 1px high 2D images by the value of vl::Image::height() which is 0 in the first case and 1 in the second.
Be careful when loading 1D images as usually they are loaded as 1px high 2D images. Thus, if you intend to use them as 1D textures, make sure to set the image height to 0.
Texture parameters:
All the parameters that are part of the texture object, such as texture filtering modes, anisotropic filtering, clamping modes etc. can be accessed via vl::Texture::getTexParameter()
. See the code below for a few examples.
- Creating the texturing test
Our test like all the other tests is a subclass of BaseDemo
so the only thing we do is to derive from it, implement a set of functions testing the different texturing technique and reimplementing the virtual functions initEvent()
and updateScene()
to initialize and animate our test.
class App_Texturing: public BaseDemo
{
public:
After which we start implementing a method for each texture type test.
- Multitexturing
This example shows how to use multiple textures to enhance the detail of your 3D objects. We will create 2 cubes: the one on the right will use a single texture, the one on the left will use multitexturing to add detail to the base texture. The two cubes will be animated by the updateScene() method.
void multitexturing()
{
{
Log::error("Multitexturing not supported.\n");
return;
}
const bool generate_tex_coords = true;
ref<Geometry> box =
makeBox(
vec3(0,0,0), 5,5,5, generate_tex_coords );
box->computeNormals();
box->setTexCoordArray(1, box->texCoordArray(0));
ref<Texture> tex_holebox =
new Texture(
"/images/holebox.tif",
TF_UNKNOWN, mMipmappingOn );
tex_holebox->getTexParameter()->setMagFilter(
TPF_LINEAR);
ref<Texture> tex_detail =
new Texture(
"/images/detail.tif",
TF_UNKNOWN, mMipmappingOn );
tex_detail->getTexParameter()->setMagFilter(
TPF_LINEAR);
ref<Light> light = new Light;
ref<Effect> fx_right_cube = new Effect;
fx_right_cube->shader()->setRenderState( light.get(), 0 );
fx_right_cube->shader()->enable(
EN_BLEND);
fx_right_cube->shader()->gocAlphaFunc()->set(
FU_GEQUAL, 0.98f);
fx_right_cube->shader()->gocLightModel()->setTwoSide(true);
fx_right_cube->shader()->gocTextureSampler(0)->setTexture( tex_holebox.get() );
ref<Effect> fx_left_cube = new Effect;
fx_left_cube->shader()->setRenderState( light.get(), 0 );
fx_left_cube->shader()->enable(
EN_BLEND);
fx_left_cube->shader()->gocAlphaFunc()->set(
FU_GEQUAL, 0.98f);
fx_left_cube->shader()->gocLightModel()->setTwoSide(true);
fx_left_cube->shader()->gocTextureSampler(0)->setTexture( tex_holebox.get() );
fx_left_cube->shader()->gocTextureSampler(1)->setTexture( tex_detail.get() );
fx_left_cube->shader()->gocTexEnv(1)->setMode(
TEM_MODULATE);
mRightCubeTransform = new Transform;
rendering()->
as<Rendering>()->transform()->addChild(mRightCubeTransform.get());
sceneManager()->tree()->addActor( box.get(), fx_right_cube.get(), mRightCubeTransform.get() );
mLeftCubeTransform = new Transform;
rendering()->
as<Rendering>()->transform()->addChild(mLeftCubeTransform.get());
sceneManager()->tree()->addActor( box.get(), fx_left_cube.get(), mLeftCubeTransform.get() );
}
- 3D textures
In this paragraph we will use 3D textures to implement a sort of animation on a 2d flat plane.
Since we want to animate the texture coordinates of our plane we manually allocate a vl::ArrayFloat3 to be used as the texture coordinate array for the texture unit #0. Note also that since VBOs are activated by default after animating the texture coordintes we need to tell VL that the textue coordinates' VBOs should be updated. See the updateScene()
method for the details.
void texture3D()
{
{
Log::error("Texture 3D not supported.\n");
return;
}
mQuad3DTex->transform( mat4::getRotation(90, 1,0,0), false );
mTexCoords_3D = new ArrayFloat3;
mTexCoords_3D->resize( 2*2 );
mQuad3DTex->setTexCoordArray(0, mTexCoords_3D.get());
ref<Effect> fx_3d = new Effect;
Actor* act_3d = sceneManager()->tree()->addActor( mQuad3DTex.get(), fx_3d.get(), new Transform );
act_3d->transform()->setLocalAndWorldMatrix( mat4::getTranslation(-6,+6,-6) );
ref<Texture> texture_3d = new Texture;
texture_3d->createTexture3D(
"/volume/VLTest.dat",
TF_UNKNOWN, mMipmappingOn );
texture_3d->getTexParameter()->setMagFilter(
TPF_LINEAR);
fx_3d->shader()->gocTextureSampler(0)->setTexture( texture_3d.get() );
}
- 2D Texture Arrays
Using 2D texture arrays (GL_TEXTURE_2D_ARRAY
) is very similar to using normal 3D textures (GL_TEXTURE_3D
), but with the following differences:
- the "r" coordinate of a 2D texture array is expressed as an integer and not as normalized floats (as it is for
GL_TEXTURE_1D/2D/3D
and cubemaps), i.e. 1, 2, 3, 4, 5 etc. instead of 0.02, 0.04, 0.06, 0.08 etc. for example.
- The mipmaps are computed in a different way: if you have a 3D texture whose dimensions are 256x256x256 at level #0, it's mipmap level #1 will be 128x128x128. Instead in 2D texture array each "layer" is kept separate somehow from one another. In fact if the mipmap level #0 of a 2D texture array is 256x256x256, its mipmap level #1 will be 128x128x256. That is, the texture size is half but you still have 256 "layers". For this reason 2D texture arrays occupy more memory that 3D textures.
- 3D textures can have a texture border, while 2D texture arrays are not allowed to have a border. For more information about the border of a texture refer to the OpenGL function glTexImage1D/2D/3D() documentation.
- 1D and 2D texture arrays are not available in the fixed function pipeline. In order to take advantage of them you have to use GLSL, that is, your Shader needs a vl::GLSLProgram.
For our demo we will use the 2D texture array in the very same way as we did for the 3D textures, but in this case we will put the textured plane on the top right corner.
void texture2DArray()
{
{
Log::error("Texture 2d array not supported.\n");
return;
}
mQuad2DArrayTex =
makeGrid(
vec3(0,0,0), 10.0f, 10.0f, 2, 2 );
mQuad2DArrayTex->transform( mat4::getRotation(90, 1,0,0), false );
mTexCoords_2DArray = new ArrayFloat3;
mTexCoords_2DArray->resize( 2*2 );
mQuad2DArrayTex->setTexCoordArray(0, mTexCoords_2DArray.get());
ref<Effect> fx_2darray = new Effect;
Actor* act_2darray = sceneManager()->tree()->addActor( mQuad2DArrayTex.get(), fx_2darray.get(), new Transform );
act_2darray->transform()->setLocalAndWorldMatrix( mat4::getTranslation(+6,+6,-6) );
ref<Image> img_volume =
loadImage(
"/volume/VLTest.dat");
m2DArraySize = img_volume->depth();
ref<Texture> texture_2darray = new Texture;
texture_2darray->createTexture2DArray( img_volume.get(),
TF_RGBA, mMipmappingOn );
texture_2darray->getTexParameter()->setMagFilter(
TPF_LINEAR);
fx_2darray->shader()->gocTextureSampler(0)->setTexture( texture_2darray.get() );
GLSLProgram* glsl = fx_2darray->shader()->gocGLSLProgram();
glsl->attachShader( new GLSLFragmentShader("/glsl/texture_2d_array.fs") );
glsl->gocUniform("sampler0")->setUniformI(0);
}
The fragment shader glsl/texture_2d_array.fs
used in the example looks like this:
#extension GL_ARB_texture_array: enable
uniform sampler2DArray sampler0;
{
gl_FragColor = texture2DArray(sampler0, gl_TexCoord[0].xyz);
}
- 1D Texture Arrays
For 1D texture arrays count the considerations that we did for 2D texture arrays.
In this example we create again a plane oriented towards the views with the difference that this time instead of creating a simple plane with 2*2
vertices we create 2*img_holebox->height()
vertices, that is, we cut the plane in img_holebox->height()
slices. Each slice will be textured using a 1D texture taken from the 1D texture array. The resulting image will look very similar to a 2D textured quad.
void texture1DArray()
{
{
Log::error("Texture 1d array not supported.\n");
return;
}
ref<Image> img_holebox =
loadImage(
"/images/holebox.tif");
m1DArraySize = img_holebox->height();
mQuad1DArrayTex =
makeGrid(
vec3(0,0,0), 10, 10, 2, img_holebox->height() );
mQuad1DArrayTex->transform( mat4::getRotation(90, 1,0,0), false );
mTexCoords_1DArray = new ArrayFloat2;
mTexCoords_1DArray->resize( 2 * img_holebox->height() );
mQuad1DArrayTex->setTexCoordArray(0, mTexCoords_1DArray.get());
ref<Effect> fx_1darray = new Effect;
Actor* act_1darray = sceneManager()->tree()->addActor( mQuad1DArrayTex.get(), fx_1darray.get(), new Transform );
act_1darray->transform()->setLocalAndWorldMatrix( mat4::getTranslation(+6,-6,-6) );
ref<Texture> texture_1darray = new Texture;
texture_1darray->createTexture1DArray( img_holebox.get(),
TF_RGBA, mMipmappingOn );
texture_1darray->getTexParameter()->setMagFilter(
TPF_LINEAR);
fx_1darray->shader()->gocTextureSampler(0)->setTexture( texture_1darray.get() );
GLSLProgram* glsl = fx_1darray->shader()->gocGLSLProgram();
glsl->attachShader( new GLSLFragmentShader("/glsl/texture_1d_array.fs") );
glsl->gocUniform("sampler0")->setUniformI(0);
}
The fragment shader glsl/texture_1d_array.fs
used in the example looks like this:
#extension GL_ARB_texture_array: enable
uniform sampler1DArray sampler0;
{
gl_FragColor = texture1DArray(sampler0, gl_TexCoord[0].xyz );
}
- Texture Rectangle
A texture rectangle (GL_TEXTURE_RECTANGLE
) is a special kind of 2D textures mainly used for post processing effects. They differ from normal 2D texture for the following:
- Texture rectangle do not support mipmapping.
- Texture rectangle do not support texture borders.
- Texture rectangle s/t coordinates are expressed as integers instead of normalized 0..1 values.
- Texture rectangle supports only the following clamping modes:
GL_CLAMP
, GL_CLAMP_TO_EDGE
, GL_CLAMP_TO_BORDER
.
void textureRectangle()
{
{
Log::error("Texture rectangle not supported.\n");
return;
}
ref<Image> img_holebox =
loadImage(
"/images/holebox.tif");
float s_max = (float)img_holebox->width();
float t_max = (float)img_holebox->height();
ref<Geometry> quad_rect =
makeGrid(
vec3(0,0,0), 10.0f, 10.0f, 2, 2,
true,
fvec2(0, 0),
fvec2(s_max, t_max) );
quad_rect->transform( mat4::getRotation(90, 1,0,0), false );
ref<Effect> fx_rect = new Effect;
Actor* act_rect = sceneManager()->tree()->addActor( quad_rect.get(), fx_rect.get(), new Transform );
act_rect->transform()->setLocalAndWorldMatrix( mat4::getTranslation(-6,-6,-6) );
ref<Texture> texture_rectangle = new Texture;
texture_rectangle->createTextureRectangle( img_holebox.get(),
TF_RGBA );
texture_rectangle->getTexParameter()->setMagFilter(
TPF_LINEAR);
texture_rectangle->getTexParameter()->setMinFilter(
TPF_LINEAR);
texture_rectangle->getTexParameter()->setWrapS(
TPW_CLAMP);
texture_rectangle->getTexParameter()->setWrapT(
TPW_CLAMP);
texture_rectangle->getTexParameter()->setWrapR(
TPW_CLAMP);
fx_rect->shader()->gocTextureSampler(0)->setTexture( texture_rectangle.get() );
}
- Spherical mapping
Spherical mapping is a very simple and cheap way to simulate environmental reflection over an object using simple 2D textures.
All we need to use spherical mapping is:
- A spherical map texture (a normal 2D texture with a special image in it)
- A 3d object WITH normals computed
- Appropriate texture coordinates dynamically generated using
glTexGen()
/GL_SPHERE_MAP
For more information about spherical mapping see also "OpenGL Cube Map Texturing": http://developer.nvidia.com/object/cube_map_ogl_tutorial.html
In our test we will apply spherical mapping to a rotating torus.
void sphericalMapping()
{
{
Log::error("Spherical mapping texture coordinate generation not supported.\n");
return;
}
mFXSpheric = new Effect;
mFXSpheric->shader()->setRenderState( new Light, 0 );
mActSpheric = sceneManager()->tree()->addActor( torus.get(), mFXSpheric.get(), new Transform );
rendering()->
as<Rendering>()->transform()->addChild( mActSpheric->transform() );
ref<Texture> texture_sphere_map = new Texture;
texture_sphere_map->createTexture2D(
"/images/spheremap_klimt.jpg",
TF_UNKNOWN, mMipmappingOn );
texture_sphere_map->getTexParameter()->setMagFilter(
TPF_LINEAR);
mFXSpheric->shader()->gocTextureSampler(0)->setTexture( texture_sphere_map.get() );
}
- Cubemaps
Cubemapping is a very flexible technique used to achieve many different kinds of effects. Here we will use cubmapping to implement the so called "environment mapping" which is a technique that simulates the environmental reflection over an object. While using spherical mapping the reflection always faces the camera (unless you regenerate it on the fly every frame), cubemapping lets you use a single cubemap texture to simulate a much more realistic three-dimensional reflection.
For more information about spherical mapping see also "OpenGL Cube Map Texturing": http://developer.nvidia.com/object/cube_map_ogl_tutorial.html
All we need to use cubemapping is:
- A cubemap texture, which is a special texture made out of 6 2D images
- A 3d model WITH normals
- Appropriate texture coordinates dynamically generated using
glTexGen()
/GL_REFLECTION_MAP
Note that you can load cubemap images in many different ways. You can assemble them on the fly using the vl::loadAsCubemap() functions or you can load it directly from a DDS file with the vl::loadImage() function.
We use the GL_CLAMP_TO_EDGE
mode here to minimize the seams of the cubemaps. This does not remove the seams totally. In order to have a cubemap without seams the cubemap must be properly generated and adjusted by the texture artist.
Note that we use GL_REFLECTION_MAP
texture generation mode for the S, T and R texture coordinates which requires the rendered geometry to have proper normals.
Note also the line:
mFXCubic->shader()->gocTextureMatrix(0)->setUseCameraRotationInverse(true);
This tells VL to put in the texture matrix the inverse of the camera rotation. This transforms into world-space the cubemap texture coordinates automatically generated by OpenGL, which would otherwise be in eye-space (ie. always facing the camera). Basically this way the virtual texture cube will be aligned with the world axes, which is what we want, instead of being aligned with the eye space axes.
void cubeMapping()
{
{
Log::error("Texture cubemap not supported.\n");
return;
}
"/images/cubemap/cubemap00.png",
"/images/cubemap/cubemap01.png",
"/images/cubemap/cubemap02.png",
"/images/cubemap/cubemap03.png",
"/images/cubemap/cubemap04.png",
"/images/cubemap/cubemap05.png");
mFXCubic = new Effect;
mFXCubic->shader()->setRenderState( new Light, 0 );
mActCubic = sceneManager()->tree()->addActor( torus.get(), mFXCubic.get(), new Transform );
rendering()->
as<Rendering>()->transform()->addChild( mActCubic->transform() );
ref<Texture> texture_cubic = new Texture;
texture_cubic->createTextureCubemap( img_cubemap.get(),
TF_RGBA, mMipmappingOn );
texture_cubic->getTexParameter()->setMagFilter(
TPF_LINEAR);
mFXCubic->shader()->gocTextureSampler(0)->setTexture( texture_cubic.get() );
mFXCubic->shader()->gocTextureMatrix(0)->setUseCameraRotationInverse(true);
}
- Applet initialization
The following function shows the simple steps used to initialize our test and the protected data used by our applet.
void initEvent()
{
Log::notify(appletInfo());
mMipmappingOn = true;
mLodBias = 0.0;
multitexturing();
textureRectangle();
texture3D();
texture2DArray();
texture1DArray();
sphericalMapping();
cubeMapping();
}
- Animation
The animation of the texture coordinates and of the transformed objects is implemented in the updateScene() virtual function as shown below:
void updateScene()
{
float t =
sin( Time::currentTime()*
fPi*2.0f/5.0f) * 0.5f + 0.5f;
t = t * (1.0f - 0.02f*2) + 0.02f;
{
mRightCubeTransform->setLocalMatrix( mat4::getTranslation(+6,0,0) * mat4::getRotation( Time::currentTime()*45, 0, 1, 0) );
mLeftCubeTransform ->setLocalMatrix( mat4::getTranslation(-6,0,0) * mat4::getRotation( Time::currentTime()*45, 0, 1, 0) );
}
if (mTexCoords_3D)
{
mTexCoords_3D->at(0) =
fvec3(0, 0, t);
mTexCoords_3D->at(1) =
fvec3(0, 1, t);
mTexCoords_3D->at(2) =
fvec3(1, 0, t);
mTexCoords_3D->at(3) =
fvec3(1, 1, t);
mTexCoords_3D->setBufferObjectDirty(true);
mQuad3DTex->setBufferObjectDirty(true);
}
if (mTexCoords_2DArray)
{
mTexCoords_2DArray->at(0) =
fvec3(0, 0, t*m2DArraySize);
mTexCoords_2DArray->at(1) =
fvec3(0, 1, t*m2DArraySize);
mTexCoords_2DArray->at(2) =
fvec3(1, 0, t*m2DArraySize);
mTexCoords_2DArray->at(3) =
fvec3(1, 1, t*m2DArraySize);
mTexCoords_2DArray->setBufferObjectDirty(true);
mQuad2DArrayTex->setBufferObjectDirty(true);
}
if (mTexCoords_1DArray)
{
for(int i=0; i<m1DArraySize; ++i)
{
float x_offset = 0.1f *
cos( t*3.14159265f + 10.0f*((
float)i/m1DArraySize)*3.14159265f );
mTexCoords_1DArray->at(i*2+0) =
fvec2(0+x_offset, (
float)i);
mTexCoords_1DArray->at(i*2+1) =
fvec2(1+x_offset, (
float)i);
}
mTexCoords_1DArray->setBufferObjectDirty(true);
mQuad1DArrayTex->setBufferObjectDirty(true);
}
if (mActSpheric)
{
mActSpheric->transform()->setLocalMatrix( mat4::getTranslation(0,+6,0)*mat4::getRotation(45*Time::currentTime(),1,0,0) );
mActSpheric->transform()->computeWorldMatrix();
}
if (mActCubic)
{
mActCubic->transform()->setLocalMatrix( mat4::getTranslation(0,-6,0)*mat4::getRotation(45*Time::currentTime(),1,0,0) );
mActCubic->transform()->computeWorldMatrix();
}
}
- Reflectivity
A classic method to simulate sharp/dull reflectivity is to manually change the lod bias via the glTexEnv() command. The Lod Bias modifies the way OpenGL selects the set of mipmaps to be used during the rendering. A higher lod bias will make OpenGL select mipmaps of a higher level (smaller images) thus the reflected image will look more blurry and less sharp. This will produce an effect similar to a rough and opaque surface. Instead, if the lod bias is set to 0 (default) the reflection will look very sharp and definite as if the surface was a perfectly polished mirror. In our test we can dynamically adjust the lod bias using the mouse wheel:
void mouseWheelEvent(int w)
{
mLodBias += w*0.3f;
mLodBias =
clamp(mLodBias, 0.0f, 4.0f);
mFXSpheric->shader()->gocTexEnv(0)->setLodBias(mLodBias);
mFXCubic->shader()->gocTexEnv(0)->setLodBias(mLodBias);
}
- Conclusions
This tutorial gave you the basic knowledge to start using several standard and advanced texturing techniques like 2D texturing, multi-texturing, 3D textures, 1D and 2D texture arrays, sphere-mapping, cubemap environment mapping and lod bias manipulation.