Tutorial 10: Shaders
This tutorial shows how to use shaders for D3D8, D3D9 and OpenGL
with the engine and how to create new material types with them.
It also shows how to disable the generation of mipmaps at texture
loading, and how to use text scene nodes.
This tutorial does not explain how shaders work. I would recommend
to read the D3D or OpenGL documentation, to search a tutorial,
or to read a book about this.
The program which is described here will look like this:
|
| Lets start! |
At first, we need to include all headers and do the stuff
we always do, like in nearly all other tutorials:
#include <irrlicht.h> #include <iostream>
using namespace irr;
#pragma comment(lib, "Irrlicht.lib")
|
Because we want to use some interesting shaders in this
tutorials, we need to set some data for them to make them
able to compute nice colors. In this example, we'll use
a simple vertex shader which will calculate the color
of the vertex based on the position of the camera. For
this, the shader needs the following data: The inverted
world matrix for transforming the normal, the clip matrix
for transforming the position, the camera position and
the world position of the object for the calculation of
the angle of light, and the color of the light. To be
able to tell the shader all this data every frame, we
have to derive a class from the IShaderConstantSetCallBack
interface and override its only method, namely OnSetConstants().
This method will be called every time the material is
set.
The method setVertexShaderConstant() of the IMaterialRendererServices
interface is used to set the data the shader needs. If
the user chose to use a High Level shader language like
HLSL instead of Assembler in this example, you have to
set the variable name as parameter instead of the register
index.
IrrlichtDevice* device = 0; bool UseHighLevelShaders = false;
class MyShaderCallBack : public video::IShaderConstantSetCallBack { public:
virtual void OnSetConstants(video::IMaterialRendererServices* services, s32 userData) { video::IVideoDriver* driver = services->getVideoDriver();
// set inverted world matrix // if we are using highlevel shaders (the user can select this when // starting the program), we must set the constants by name. core::matrix4 invWorld = driver->getTransform(video::ETS_WORLD); invWorld.makeInverse();
if (UseHighLevelShaders) services->setVertexShaderConstant("mInvWorld", &invWorld.M[0], 16); else services->setVertexShaderConstant(&invWorld.M[0], 0, 4);
// set clip matrix core::matrix4 worldViewProj; worldViewProj = driver->getTransform(video::ETS_PROJECTION); worldViewProj *= driver->getTransform(video::ETS_VIEW); worldViewProj *= driver->getTransform(video::ETS_WORLD);
if (UseHighLevelShaders) services->setVertexShaderConstant("mWorldViewProj", &worldViewProj.M[0], 16); else services->setVertexShaderConstant(&worldViewProj.M[0], 4, 4); // set camera position core::vector3df pos = device->getSceneManager()-> getActiveCamera()->getAbsolutePosition();
if (UseHighLevelShaders) services->setVertexShaderConstant("mLightPos", reinterpret_cast<f32*>(&pos), 3); else services->setVertexShaderConstant(reinterpret_cast<f32*>(&pos), 8, 1);
// set light color video::SColorf col(0.0f,1.0f,1.0f,0.0f);
if (UseHighLevelShaders) services->setVertexShaderConstant("mLightColor", reinterpret_cast<f32*>(&col), 4); else services->setVertexShaderConstant(reinterpret_cast<f32*>(&col), 9, 1);
// set transposed world matrix core::matrix4 world = driver->getTransform(video::ETS_WORLD); world = world.getTransposed();
if (UseHighLevelShaders) services->setVertexShaderConstant("mTransWorld", &world.M[0], 16); else services->setVertexShaderConstant(&world.M[0], 10, 4); } }; |
The next few lines start up the engine. Just like in
most other tutorials before. But in addition, we ask the
user if he wants this example to use high level shaders
if he selected a driver which is capable of doing so.
int main() { // let user select driver type
video::E_DRIVER_TYPE driverType = video::EDT_DIRECTX9;
printf("Please select the driver you want for this example:\n"\ " (a) Direct3D 9.0c\n (b) Direct3D 8.1\n (c) OpenGL 1.5\n"\ " (d) Software Renderer\n (e) Apfelbaum Software Renderer\n"\ " (f) NullDevice\n (otherKey) exit\n\n");
char i; std::cin >> i;
switch(i) { case 'a': driverType = video::EDT_DIRECT3D9;break; case 'b': driverType = video::EDT_DIRECT3D8;break; case 'c': driverType = video::EDT_OPENGL; break; case 'd': driverType = video::EDT_SOFTWARE; break; case 'e': driverType = video::EDT_SOFTWARE2;break; case 'f': driverType = video::EDT_NULL; break; default: return 1; }
// ask the user if we should use high level shaders for this example if (driverType == video::EDT_DIRECT3D9 || driverType == video::EDT_OPENGL)
{ printf("Please press 'y' if you want to use high level shaders.\n"); std::cin >> i; if (i == 'y') UseHighLevelShaders = true; }
// create device
device = createDevice(driverType, core::dimension2d<s32>(640, 480));
if (device == 0) { printf("\nWas not able to create driver.\n"\ "Please restart and select another driver.\n"); getch(); return 1; }
video::IVideoDriver* driver = device->getVideoDriver(); scene::ISceneManager* smgr = device->getSceneManager(); gui::IGUIEnvironment* gui = device->getGUIEnvironment(); |
Now for the more interesting parts. If we are using
Direct3D, we want to load vertex and pixel shader programs,
if we have
OpenGL, we want to use ARB fragment and vertex programs.
I wrote the corresponding programs down into the files
d3d8.ps, d3d8.vs, d3d9.ps, d3d9.vs, opengl.ps and opengl.vs.
We only need the right filenames now. This is done in
the following switch. Note, that it is not necessary to
write the shaders into text files, like in this example.
You can even write the shaders directly as strings into
the cpp source file, and use later addShaderMaterial()
instead of addShaderMaterialFromFiles().
c8* vsFileName = 0; // filename for the vertex shader c8* psFileName = 0; // filename for the pixel shader
switch(driverType) { case video::EDT_DIRECT3D8: psFileName = "../../media/d3d8.psh"; vsFileName = "../../media/d3d8.vsh"; break; case video::EDT_DIRECT3D9: if (UseHighLevelShaders) { psFileName = "../../media/d3d9.hlsl"; vsFileName = psFileName; // both shaders are in the same file } else { psFileName = "../../media/d3d9.psh"; vsFileName = "../../media/d3d9.vsh"; } break; case video::EDT_OPENGL: if (UseHighLevelShaders) { psFileName = "../../media/opengl.frag"; vsFileName = "../../media/opengl.vert"; } else { psFileName = "../../media/opengl.psh"; vsFileName = "../../media/opengl.vsh"; } break; }
|
In addition, we check if the hardware and the selected
renderer is capable of executing the shaders we want.
If not, we simply set the filename string to 0. This is
not necessary, but useful in this example: For example,
if the hardware is able to execute vertex shaders but
not pixel shaders, we create a new material which only
uses the vertex shader, and no pixel shader. Otherwise,
if we would tell the engine to create this material and
the engine sees that the hardware wouldn't be able to
fullfill the request completely,
it would not create any new material at all. So in this
example you would see at least the vertex shader in action,
without the pixel shader.
if (!driver->queryFeature(video::EVDF_PIXEL_SHADER_1_1) && !driver->queryFeature(video::EVDF_ARB_FRAGMENT_PROGRAM_1)) { device->getLogger()->log("WARNING: Pixel shaders disabled "\ "because of missing driver/hardware support."); psFileName = 0; } if (!driver->queryFeature(video::EVDF_VERTEX_SHADER_1_1) && !driver->queryFeature(video::EVDF_ARB_VERTEX_PROGRAM_1)) { device->getLogger()->log("WARNING: Vertex shaders disabled "\ "because of missing driver/hardware support."); vsFileName = 0; } |
Now lets create the new materials.
As you maybe know from previous examples, a material type
in the Irrlicht engine is set by simply changing the MaterialType
value in the SMaterial struct. And this value is just a
simple 32 bit value, like video::EMT_SOLID. So we only need
the engine to create a new value for us which we can set
there. To do this, we get a pointer to the IGPUProgrammingServices
and call addShaderMaterialFromFiles(), which returns such
a new 32 bit value. That's all.
The parameters to this method are the following: First,
the names of the files containing the code of the vertex
and the pixel shader.
If you would use addShaderMaterial() instead, you would
not need file names, then you could write the code of the
shader directly as string. The following parameter is a
pointer to the IShaderConstantSetCallBack class we wrote
at the beginning of this tutorial. If you don't want to
set constants, set this to 0. The last paramter tells the
engine which material it should use as base material.
To demonstrate this, we create two materials with a different
base material, one with EMT_SOLID and one with EMT_TRANSPARENT_ADD_COLOR.
// create materials
video::IGPUProgrammingServices* gpu = driver->getGPUProgrammingServices();
s32 newMaterialType1 = 0; s32 newMaterialType2 = 0;
if (gpu) { MyShaderCallBack* mc = new MyShaderCallBack();
// create the shaders depending on if the user wanted high level // or low level shaders:
if (UseHighLevelShaders) { // create material from high level shaders (hlsl or glsl)
newMaterialType1 = gpu->addHighLevelShaderMaterialFromFiles( vsFileName, "vertexMain", video::EVST_VS_1_1, psFileName, "pixelMain", video::EPST_PS_1_1, mc, video::EMT_SOLID);
newMaterialType2 = gpu->addHighLevelShaderMaterialFromFiles( vsFileName, "vertexMain", video::EVST_VS_1_1, psFileName, "pixelMain", video::EPST_PS_1_1, mc, video::EMT_TRANSPARENT_ADD_COLOR); } else { // create material from low level shaders (asm or arb_asm)
newMaterialType1 = gpu->addShaderMaterialFromFiles(vsFileName, psFileName, mc, video::EMT_SOLID);
newMaterialType2 = gpu->addShaderMaterialFromFiles(vsFileName, psFileName, mc, video::EMT_TRANSPARENT_ADD_COLOR); }
mc->drop(); }
|
Now its time for testing out the materials. We create
a test cube and set the material we created. In addition,
we add a text scene node to the cube and a rotatation animator,
to make it look more interesting and important.
// create test scene node 1, with the new created material type 1
scene::ISceneNode* node = smgr->addCubeSceneNode(50);
node->setPosition(core::vector3df(0,0,0));
node->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));
node->setMaterialFlag(video::EMF_LIGHTING, false);
node->setMaterialType((video::E_MATERIAL_TYPE)newMaterialType1);
smgr->addTextSceneNode(gui->getBuiltInFont(),
L"PS & VS & EMT_SOLID",
video::SColor(255,255,255,255), node);
scene::ISceneNodeAnimator* anim = smgr->createRotationAnimator(
core::vector3df(0,0.3f,0));
node->addAnimator(anim);
anim->drop(); |
Same for the second cube, but with the second material
we created.
// create test scene node 2, with the new created material type 2
node = smgr->addCubeSceneNode(50);
node->setPosition(core::vector3df(0,-10,50));
node->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));
node->setMaterialFlag(video::EMF_LIGHTING, false);
node->setMaterialType((video::E_MATERIAL_TYPE)newMaterialType2);
smgr->addTextSceneNode(gui->getBuiltInFont(),
L"PS & VS & EMT_TRANSPARENT",
video::SColor(255,255,255,255), node);
anim = smgr->createRotationAnimator(core::vector3df(0,0.3f,0));
node->addAnimator(anim);
anim->drop(); |
Then we add a third cube without a shader on it, to be able
to compare the cubes.
// add a scene node with no shader
node = smgr->addCubeSceneNode(50);
node->setPosition(core::vector3df(0,50,25));
node->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));
node->setMaterialFlag(video::EMF_LIGHTING, false);
smgr->addTextSceneNode(gui->getBuiltInFont(), L"NO SHADER",
video::SColor(255,255,255,255), node);
|
And last, we add a skybox and a user controlled camera to
the scene. For the skybox textures, we disable mipmap generation,
because we don't need mipmaps on it.
// add a nice skybox
driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, false);
smgr->addSkyBoxSceneNode( driver->getTexture("../../media/irrlicht2_up.jpg"), driver->getTexture("../../media/irrlicht2_dn.jpg"), driver->getTexture("../../media/irrlicht2_lf.jpg"), driver->getTexture("../../media/irrlicht2_rt.jpg"), driver->getTexture("../../media/irrlicht2_ft.jpg"), driver->getTexture("../../media/irrlicht2_bk.jpg"));
driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, true);
// add a camera and disable the mouse cursor
scene::ICameraSceneNode* cam = smgr->addCameraSceneNodeFPS(0, 100.0f, 100.0f); cam->setPosition(core::vector3df(-100,50,100)); cam->setTarget(core::vector3df(0,0,0)); device->getCursorControl()->setVisible(false); |
Now draw everything. That's all.
int lastFPS = -1;
while(device->run()) if (device->isWindowActive()) { driver->beginScene(true, true, video::SColor(255,0,0,0)); smgr->drawAll(); driver->endScene();
int fps = driver->getFPS();
if (lastFPS != fps) { core::stringw str = L"Irrlicht Engine - Vertex and pixel shader example ["; str += driver->getName(); str += "] FPS:"; str += fps; device->setWindowCaption(str.c_str()); lastFPS = fps; } }
device->drop(); return 0;
|
Compile and run this, and I hope you have fun with your new
little shader writing tool :).
|
| Shader Files |
The files containing the shaders can be found in the media
directory of the SDK. However, they look like this:
| D3D9.HLSL |
// part of the Irrlicht Engine Shader example.
// These simple Direct3D9 pixel and vertex shaders // will be loaded by the shaders
// example. Please note that these example shaders don't do // anything really useful.
// They only demonstrate that shaders can be used in Irrlicht.
//-----------------------------------------------------------------------------
// Global variables
//-----------------------------------------------------------------------------
float4x4 mWorldViewProj; // World * View * Projection transformation
float4x4 mInvWorld; // Inverted world matrix
float4x4 mTransWorld; // Transposed world matrix
float3 mLightPos; // Light position
float4 mLightColor; // Light color
// Vertex shader output structure
struct VS_OUTPUT
{
float4 Position : POSITION; // vertex position
float4 Diffuse : COLOR0; // vertex diffuse color
float2 TexCoord : TEXCOORD0; // tex coords
};
VS_OUTPUT vertexMain( in float4 vPosition : POSITION,
in float3 vNormal : NORMAL,
float2 texCoord : TEXCOORD0 )
{
VS_OUTPUT Output;
// transform position to clip space
Output.Position = mul(vPosition, mWorldViewProj);
// transform normal
float3 normal = mul(vNormal, mInvWorld);
// renormalize normal
normal = normalize(normal);
// position in world coodinates
float3 worldpos = mul(mTransWorld, vPosition);
// calculate light vector, vtxpos - lightpos
float3 lightVector = worldpos - mLightPos;
// normalize light vector
lightVector = normalize(lightVector);
// calculate light color
float3 tmp = dot(-lightVector, normal);
tmp = lit(tmp.x, tmp.y, 1.0);
tmp = mLightColor * tmp.y;
Output.Diffuse = float4(tmp.x, tmp.y, tmp.z, 0);
Output.TexCoord = texCoord;
return Output;
}
// Pixel shader output structure
struct PS_OUTPUT
{
float4 RGBColor : COLOR0; // Pixel color
};
sampler2D tex0;
PS_OUTPUT pixelMain( float2 TexCoord : TEXCOORD0,
float4 Position : POSITION,
float4 Diffuse : COLOR0 )
{
PS_OUTPUT Output;
float4 col = tex2D( tex0, TexCoord ); // sample color map
// multiply with diffuse and do other senseless operations
Output.RGBColor = Diffuse * col;
Output.RGBColor *= 4.0;
return Output;
} |
| D3D9.VSH |
; part of the Irrlicht Engine Shader example.
; This Direct3D9 vertex shader will be loaded by the engine.
; Please note that these example shaders don't do anything really useful.
; They only demonstrate that shaders can be used in Irrlicht.
vs.1.1
dcl_position v0; ; declare position
dcl_normal v1; ; declare normal
dcl_color v2; ; declare color
dcl_texcoord0 v3; ; declare texture coordinate
; transpose and transform position to clip space
mul r0, v0.x, c4
mad r0, v0.y, c5, r0
mad r0, v0.z, c6, r0
add oPos, c7, r0
; transform normal
dp3 r1.x, v1, c0
dp3 r1.y, v1, c1
dp3 r1.z, v1, c2
; renormalize normal
dp3 r1.w, r1, r1
rsq r1.w, r1.w
mul r1, r1, r1.w
; calculate light vector
m4x4 r6, v0, c10 ; vertex into world position
add r2, c8, -r6 ; vtxpos - lightpos
; normalize light vector
dp3 r2.w, r2, r2
rsq r2.w, r2.w
mul r2, r2, r2.w
; calculate light color
dp3 r3, r1, r2 ; dp3 with negative light vector
lit r5, r3 ; clamp to zero if r3 < 0, r5 has diffuce component in r5.y
mul oD0, r5.y, c9 ; ouput diffuse color
mov oT0, v3 ; store texture coordinates |
| D3D9.PSH |
; part of the Irrlicht Engine Shader example.
; This simple Direct3D9 pixel shader will be loaded by the engine.
; Please note that these example shaders don't do anything really useful.
; They only demonstrate that shaders can be used in Irrlicht.
ps.1.1
tex t0 ; sample color map
add r0, v0, v0 ; mulitply with color
mul t0, t0, r0 ; mulitply with color
add r0, t0, t0 ; make it brighter and store result
|
| D3D8.VSH |
; part of the Irrlicht Engine Shader example.
; This Direct3D9 vertex shader will be loaded by the engine.
; Please note that these example shaders don't do anything really useful.
; They only demonstrate that shaders can be used in Irrlicht.
vs.1.1
; transpose and transform position to clip space
mul r0, v0.x, c4
mad r0, v0.y, c5, r0
mad r0, v0.z, c6, r0
add oPos, c7, r0
; transform normal
dp3 r1.x, v1, c0
dp3 r1.y, v1, c1
dp3 r1.z, v1, c2
; renormalize normal
dp3 r1.w, r1, r1
rsq r1.w, r1.w
mul r1, r1, r1.w
; calculate light vector
m4x4 r6, v0, c10 ; vertex into world position
add r2, c8, -r6 ; vtxpos - lightpos
; normalize light vector
dp3 r2.w, r2, r2
rsq r2.w, r2.w
mul r2, r2, r2.w
; calculate light color
dp3 r3, r1, r2 ; dp3 with negative light vector
lit r5, r3 ; clamp to zero if r3 < 0, r5 has diffuce component in r5.y
mul oD0, r5.y, c9 ; ouput diffuse color
mov oT0, v3 ; store texture coordinates |
| D3D8.PSH |
; part of the Irrlicht Engine Shader example.
; This simple Direct3D9 pixel shader will be loaded by the engine.
; Please note that these example shaders don't do anything really useful.
; They only demonstrate that shaders can be used in Irrlicht.
ps.1.1
tex t0 ; sample color map
mul_x2 t0, t0, v0 ; mulitply with color
add r0, t0, t0 ; make it brighter and store result |
| OPENGL.VSH |
!!ARBvp1.0
# part of the Irrlicht Engine Shader example.
# Please note that these example shaders don't do anything really useful.
# They only demonstrate that shaders can be used in Irrlicht.
#input
ATTRIB InPos = vertex.position;
ATTRIB InColor = vertex.color;
ATTRIB InNormal = vertex.normal;
ATTRIB InTexCoord = vertex.texcoord;
#output
OUTPUT OutPos = result.position;
OUTPUT OutColor = result.color;
OUTPUT OutTexCoord = result.texcoord;
PARAM MVP[4] = { state.matrix.mvp }; # modelViewProjection matrix.
TEMP Temp;
TEMP TempColor;
TEMP TempNormal;
TEMP TempPos;
#transform position to clip space
DP4 Temp.x, MVP[0], InPos;
DP4 Temp.y, MVP[1], InPos;
DP4 Temp.z, MVP[2], InPos;
DP4 Temp.w, MVP[3], InPos;
#transform normal
DP3 TempNormal.x, InNormal.x, program.local[0];
DP3 TempNormal.y, InNormal.y, program.local[1];
DP3 TempNormal.z, InNormal.z, program.local[2];
#renormalize normal
DP3 TempNormal.w, TempNormal, TempNormal;
RSQ TempNormal.w, TempNormal.w;
MUL TempNormal, TempNormal, TempNormal.w;
# calculate light vector
DP4 TempPos.x, InPos, program.local[10]; # vertex into world position
DP4 TempPos.y, InPos, program.local[11];
DP4 TempPos.z, InPos, program.local[12];
DP4 TempPos.w, InPos, program.local[13];
ADD TempPos, program.local[8], -TempPos; # vtxpos - lightpos
# normalize light vector
DP3 TempPos.w, TempPos, TempPos;
RSQ TempPos.w, TempPos.w;
MUL TempPos, TempPos, TempPos.w;
# calculate light color
DP3 TempColor, TempNormal, TempPos; # dp3 with negative light vector
LIT OutColor, TempColor; # clamp to zero if r3 < 0
MUL OutColor, TempColor.y, program.local[9]; # ouput diffuse color
MOV OutColor.w, 1.0; # we want alpha to be always 1
MOV OutTexCoord, InTexCoord; # store texture coordinate
MOV OutPos, Temp;
END |
| OPENGL.PSH |
!!ARBfp1.0
# part of the Irrlicht Engine Shader example.
# Please note that these example shaders don't do anything really useful.
# They only demonstrate that shaders can be used in Irrlicht.
#Input
ATTRIB inTexCoord = fragment.texcoord; # texture coordinates
ATTRIB inColor = fragment.color.primary; # interpolated diffuse color
#Output
OUTPUT outColor = result.color;
TEMP texelColor;
TEMP tmp;
TXP texelColor, inTexCoord, texture, 2D;
ADD tmp, inColor, inColor; # mulitply with color
MUL texelColor, texelColor, tmp; # mulitply with color
ADD outColor, texelColor, texelColor; # make it brighter and store result
END |
|
|