In the previous tutorial we drew a spinning colored cube using CQuadColor. Now we'll load texture files, apply them to materials using CQuadColorUV, and use alpha blending to draw a translucent textured floor beneath the cube.
This tutorial covers:
createTextureFileCPath::addSearchPath to locate data filesTo load a texture from disk, use UDriver::createTextureFile(). This returns a UTextureFile pointer, which inherits from UTexture. The driver searches for the file using CPath, so you need to register your data directory first.
#include <nel/misc/path.h>
Register the search path early, for example in the constructor:
NLMISC::CPath::addSearchPath("data", true, false);
Then load the texture:
m_FloorTex = m_Driver->createTextureFile("floor_checker.tga");
Add the member variable:
NL3D::UTextureFile *m_FloorTex;
And clean it up in the destructor, before releasing the driver:
m_Driver->deleteTextureFile(m_FloorTex);
NeL can load
.tgaand.ddstexture files. You can useCPath::remapExtension("dds", "tga", true)to automatically substitute.ddsfiles for.tgawhen DXT-compressed versions are available in the search path.
Set the texture on a material using setTexture() with a stage index. Stage 0 is the primary texture.
m_FloorMat = m_Driver->createMaterial();
m_FloorMat.initUnlit();
m_FloorMat.setZWrite(true);
m_FloorMat.setZFunc(NL3D::UMaterial::lessequal);
m_FloorMat.setTexture(0, dynamic_cast<NL3D::UTexture *>(m_FloorTex));
The dynamic_cast is needed because setTexture takes a UTexture* and createTextureFile returns a UTextureFile*.
To draw a quad with texture coordinates, use CQuadColorUV instead of CQuadColor. It adds Uv0 through Uv3 fields alongside the vertices and colors:
#include <nel/misc/geom_ext.h>
static void drawTexturedQuad(NL3D::UDriver *driver, NL3D::UMaterial &mat,
const NLMISC::CVector &v0, const NLMISC::CVector &v1,
const NLMISC::CVector &v2, const NLMISC::CVector &v3,
const NLMISC::CUV &uv0, const NLMISC::CUV &uv1,
const NLMISC::CUV &uv2, const NLMISC::CUV &uv3,
NLMISC::CRGBA color)
{
NLMISC::CQuadColorUV q;
q.V0 = v0; q.V1 = v1; q.V2 = v2; q.V3 = v3;
q.Uv0 = uv0; q.Uv1 = uv1; q.Uv2 = uv2; q.Uv3 = uv3;
q.Color0 = q.Color1 = q.Color2 = q.Color3 = color;
driver->drawQuad(q, mat);
}
Draw a textured floor quad at Z=0 with tiling UVs:
float s = 5.f;
drawTexturedQuad(m_Driver, m_FloorMat,
NLMISC::CVector(-s, -s, 0.f), NLMISC::CVector( s, -s, 0.f),
NLMISC::CVector( s, s, 0.f), NLMISC::CVector(-s, s, 0.f),
NLMISC::CUV(0.f, 0.f), NLMISC::CUV(4.f, 0.f),
NLMISC::CUV(4.f, 4.f), NLMISC::CUV(0.f, 4.f),
NLMISC::CRGBA::White);
UV coordinates outside [0,1] will tile the texture by default, since UTexture::TWrapMode defaults to Repeat.
To make a material translucent, enable blending and set the blend function. The most common mode is source-alpha blending:
m_TranspMat = m_Driver->createMaterial();
m_TranspMat.initUnlit();
m_TranspMat.setZWrite(true);
m_TranspMat.setZFunc(NL3D::UMaterial::lessequal);
m_TranspMat.setBlend(true);
m_TranspMat.setBlendFunc(NL3D::UMaterial::srcalpha,
NL3D::UMaterial::invsrcalpha);
m_TranspMat.setDoubleSided(true);
With source-alpha blending, the per-vertex color's alpha channel controls transparency. Set it to draw a translucent overlay:
NLMISC::CRGBA translucentBlue(100, 150, 220, 128); // 50% transparent
Draw translucent geometry after all opaque geometry, since alpha blending requires the opaque scene to already be in the depth buffer.
If you don't have a texture file handy, you can create one programmatically using UTextureMem:
// Create a 64x64 checkerboard texture in memory
m_CheckerTex = m_Driver->createTextureMem(64, 64, NLMISC::CBitmap::RGBA);
uint8 *pixels = m_CheckerTex->getPointer();
for (int y = 0; y < 64; ++y)
{
for (int x = 0; x < 64; ++x)
{
uint8 c = ((x / 8) + (y / 8)) % 2 == 0 ? 200 : 60;
int idx = (y * 64 + x) * 4;
pixels[idx + 0] = c; // R
pixels[idx + 1] = c; // G
pixels[idx + 2] = c; // B
pixels[idx + 3] = 255; // A
}
}
m_CheckerTex->touch(); // mark the texture data as modified
Set it on a material:
m_FloorMat.setTexture(0, dynamic_cast<NL3D::UTexture *>(m_CheckerTex));
Clean up with m_Driver->deleteTextureMem(m_CheckerTex) in the destructor.
This example builds on the spinning cube tutorial, adding a textured checker floor and a translucent colored overlay quad.
#include <nel/misc/types_nl.h>
#include <nel/misc/app_context.h>
#include <nel/misc/event_listener.h>
#include <nel/misc/debug.h>
#include <nel/misc/time_nl.h>
#include <nel/misc/geom_ext.h>
#include <nel/misc/path.h>
#include <nel/misc/bitmap.h>
#include <nel/3d/u_driver.h>
#include <nel/3d/u_material.h>
#include <nel/3d/frustum.h>
using namespace std;
using namespace NLMISC;
// Build a view matrix from eye position, target, and up vector
static CMatrix buildViewMatrix(const CVector &eye, const CVector &target, const CVector &up)
{
CVector jj = (target - eye).normed();
CVector ii = (jj ^ up).normed();
CVector kk = ii ^ jj;
CMatrix camWorld;
camWorld.setRot(ii, jj, kk, true);
camWorld.setPos(eye);
CMatrix viewMatrix = camWorld;
viewMatrix.invert();
return viewMatrix;
}
// Draw a colored quad face
static void drawFace(NL3D::UDriver *driver, NL3D::UMaterial &mat,
const CVector &v0, const CVector &v1, const CVector &v2, const CVector &v3,
CRGBA color)
{
CQuadColor quad;
quad.V0 = v0; quad.V1 = v1; quad.V2 = v2; quad.V3 = v3;
quad.Color0 = quad.Color1 = quad.Color2 = quad.Color3 = color;
driver->drawQuad(quad, mat);
}
// Draw a colored cube
static void drawCube(NL3D::UDriver *driver, NL3D::UMaterial &mat,
const CMatrix &transform, float s)
{
CVector v[8] = {
transform * CVector(-s, -s, -s), transform * CVector( s, -s, -s),
transform * CVector( s, s, -s), transform * CVector(-s, s, -s),
transform * CVector(-s, -s, s), transform * CVector( s, -s, s),
transform * CVector( s, s, s), transform * CVector(-s, s, s),
};
drawFace(driver, mat, v[3], v[2], v[1], v[0], CRGBA(200, 50, 50));
drawFace(driver, mat, v[4], v[5], v[6], v[7], CRGBA( 50, 200, 50));
drawFace(driver, mat, v[0], v[1], v[5], v[4], CRGBA( 50, 50, 200));
drawFace(driver, mat, v[2], v[3], v[7], v[6], CRGBA(200, 200, 50));
drawFace(driver, mat, v[3], v[0], v[4], v[7], CRGBA(200, 50, 200));
drawFace(driver, mat, v[1], v[2], v[6], v[5], CRGBA( 50, 200, 200));
}
class CMyGame : public IEventListener
{
public:
CMyGame();
~CMyGame();
void run();
virtual void operator()(const CEvent &event) NL_OVERRIDE;
private:
NL3D::UDriver *m_Driver;
NL3D::UMaterial m_CubeMat;
NL3D::UMaterial m_FloorMat;
NL3D::UMaterial m_OverlayMat;
NL3D::UTextureMem *m_CheckerTex;
bool m_CloseWindow;
bool m_KeyForward;
bool m_KeyBackward;
double m_LastTime;
float m_CubeAngle;
float m_CamAngle;
float m_CamDist;
};
CMyGame::CMyGame()
: m_CloseWindow(false)
, m_KeyForward(false)
, m_KeyBackward(false)
, m_CheckerTex(NULL)
, m_CubeAngle(0.f)
, m_CamAngle(0.f)
, m_CamDist(6.f)
{
m_Driver = NL3D::UDriver::createDriver(0, NL3D::UDriver::OpenGl3);
if (!m_Driver)
{
nlerror("Failed to create driver");
return;
}
m_Driver->EventServer.addListener(EventCloseWindowId, this);
m_Driver->EventServer.addListener(EventKeyDownId, this);
m_Driver->EventServer.addListener(EventKeyUpId, this);
m_Driver->setDisplay(NL3D::UDriver::CMode(800, 600, 32));
m_Driver->setWindowTitle("Textures and Blending");
// Create a 64x64 checkerboard texture in memory
m_CheckerTex = m_Driver->createTextureMem(64, 64, CBitmap::RGBA);
uint8 *pixels = m_CheckerTex->getPointer();
for (int y = 0; y < 64; ++y)
{
for (int x = 0; x < 64; ++x)
{
uint8 c = ((x / 8) + (y / 8)) % 2 == 0 ? 200 : 60;
int idx = (y * 64 + x) * 4;
pixels[idx + 0] = c;
pixels[idx + 1] = c;
pixels[idx + 2] = c;
pixels[idx + 3] = 255;
}
}
m_CheckerTex->touch();
// Opaque material for the cube
m_CubeMat = m_Driver->createMaterial();
m_CubeMat.initUnlit();
m_CubeMat.setZWrite(true);
m_CubeMat.setZFunc(NL3D::UMaterial::lessequal);
// Textured material for the floor
m_FloorMat = m_Driver->createMaterial();
m_FloorMat.initUnlit();
m_FloorMat.setZWrite(true);
m_FloorMat.setZFunc(NL3D::UMaterial::lessequal);
m_FloorMat.setTexture(0, dynamic_cast<NL3D::UTexture *>(m_CheckerTex));
// Translucent overlay material
m_OverlayMat = m_Driver->createMaterial();
m_OverlayMat.initUnlit();
m_OverlayMat.setZWrite(false);
m_OverlayMat.setZFunc(NL3D::UMaterial::lessequal);
m_OverlayMat.setBlend(true);
m_OverlayMat.setBlendFunc(NL3D::UMaterial::srcalpha, NL3D::UMaterial::invsrcalpha);
m_OverlayMat.setDoubleSided(true);
m_LastTime = CTime::ticksToSecond(CTime::getPerformanceTime());
}
CMyGame::~CMyGame()
{
m_FloorMat.setTexture(0, NULL);
m_Driver->deleteTextureMem(m_CheckerTex);
m_Driver->deleteMaterial(m_OverlayMat);
m_Driver->deleteMaterial(m_FloorMat);
m_Driver->deleteMaterial(m_CubeMat);
m_Driver->release();
delete m_Driver;
}
void CMyGame::operator()(const CEvent &event)
{
if (event == EventCloseWindowId)
{
m_CloseWindow = true;
}
else if (event == EventKeyDownId)
{
CEventKeyDown &kd = (CEventKeyDown &)event;
if (kd.Key == KeyUP) m_KeyForward = true;
if (kd.Key == KeyDOWN) m_KeyBackward = true;
}
else if (event == EventKeyUpId)
{
CEventKeyUp &ku = (CEventKeyUp &)event;
if (ku.Key == KeyUP) m_KeyForward = false;
if (ku.Key == KeyDOWN) m_KeyBackward = false;
}
}
void CMyGame::run()
{
while (m_Driver->isActive() && !m_CloseWindow)
{
m_Driver->EventServer.pump();
double now = CTime::ticksToSecond(CTime::getPerformanceTime());
float dt = float(now - m_LastTime);
m_LastTime = now;
m_CubeAngle += dt * 1.0f;
m_CamAngle += dt * 0.3f;
if (m_KeyForward) m_CamDist -= dt * 4.f;
if (m_KeyBackward) m_CamDist += dt * 4.f;
if (m_CamDist < 2.f) m_CamDist = 2.f;
uint32 screenW, screenH;
m_Driver->getWindowSize(screenW, screenH);
NL3D::CFrustum frustum;
frustum.initPerspective(float(Pi / 3.0),
float(screenW) / float(screenH), 0.1f, 100.f);
m_Driver->setFrustum(frustum);
CVector eye(cosf(m_CamAngle) * m_CamDist,
sinf(m_CamAngle) * m_CamDist, 3.f);
m_Driver->setViewMatrix(buildViewMatrix(eye, CVector(0, 0, 1), CVector(0, 0, 1)));
CMatrix modelMatrix;
modelMatrix.identity();
m_Driver->setModelMatrix(modelMatrix);
m_Driver->clearBuffers(CRGBA(30, 30, 30));
// 1. Draw opaque textured floor
float s = 5.f;
CQuadColorUV floorQ;
floorQ.V0.set(-s, -s, 0.f); floorQ.V1.set( s, -s, 0.f);
floorQ.V2.set( s, s, 0.f); floorQ.V3.set(-s, s, 0.f);
floorQ.Uv0.set(0, 0); floorQ.Uv1.set(4, 0);
floorQ.Uv2.set(4, 4); floorQ.Uv3.set(0, 4);
floorQ.Color0 = floorQ.Color1 = floorQ.Color2 = floorQ.Color3 = CRGBA::White;
m_Driver->drawQuad(floorQ, m_FloorMat);
// 2. Draw opaque spinning cube
CMatrix cubeTransform;
cubeTransform.identity();
cubeTransform.setPos(CVector(0.f, 0.f, 1.5f));
cubeTransform.rotateZ(m_CubeAngle);
cubeTransform.rotateX(m_CubeAngle * 0.7f);
drawCube(m_Driver, m_CubeMat, cubeTransform, 0.8f);
// 3. Draw translucent overlay after all opaque geometry
CQuadColor overlayQ;
float os = 3.f;
overlayQ.V0.set(-os, -os, 0.01f); overlayQ.V1.set( os, -os, 0.01f);
overlayQ.V2.set( os, os, 0.01f); overlayQ.V3.set(-os, os, 0.01f);
CRGBA overlayColor(100, 150, 255, 80);
overlayQ.Color0 = overlayQ.Color1 = overlayQ.Color2 = overlayQ.Color3 = overlayColor;
m_Driver->drawQuad(overlayQ, m_OverlayMat);
m_Driver->swapBuffers();
}
}
int main(int argc, char *argv[])
{
CApplicationContext applicationContext;
CMyGame myGame;
myGame.run();
return EXIT_SUCCESS;
}
Now that you can draw textured and translucent geometry, it's time to move beyond manual drawing. In the next tutorial, you will learn how to use the scene graph to load and render 3D shapes.