This tutorial shows how to use shaders for D3D8, D3D9, OpenGL, and Cg 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, OpenGL, or Cg documentation, to search a tutorial, or to read a book about this.
At first, we need to include all headers and do the stuff we always do, like in nearly all other tutorials:
#include <iostream>
#ifdef _MSC_VER
#pragma comment(lib, "Irrlicht.lib")
#endif
Main header file of the irrlicht, the only file needed to include.
Everything in the Irrlicht Engine can be found in this namespace.
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.
bool UseHighLevelShaders = false;
bool UseCgShaders = false;
{
public:
{
if (UseHighLevelShaders)
else
worldViewProj = driver->
getTransform(video::ETS_PROJECTION);
if (UseHighLevelShaders)
else
getActiveCamera()->getAbsolutePosition();
if (UseHighLevelShaders)
else
if (UseHighLevelShaders)
reinterpret_cast<f32*
>(&col), 4);
else
if (UseHighLevelShaders)
{
if (UseHighLevelShaders)
}
else
}
};
The Irrlicht device. You can create it with createDevice() or createDeviceEx().
virtual scene::ISceneManager * getSceneManager()=0
Provides access to the scene manager.
4x4 matrix. Mostly used as transformation matrix for 3d calculations.
bool makeInverse()
Calculates inverse of matrix. Slow.
const T * pointer() const
Returns pointer to internal array.
CMatrix4< T > getTransposed() const
Gets transposed matrix.
Interface providing some methods for changing advanced, internal states of a IVideoDriver.
virtual bool setVertexShaderConstant(const c8 *name, const f32 *floats, int count)=0
Sets a constant for the vertex shader based on a name.
virtual bool setPixelShaderConstant(const c8 *name, const f32 *floats, int count)=0
Sets a constant for the pixel shader based on a name.
virtual IVideoDriver * getVideoDriver()=0
Get pointer to the IVideoDriver interface.
Interface making it possible to set constants for gpu programs every frame.
Interface to driver which is able to perform 2d and 3d graphics functions.
virtual const core::matrix4 & getTransform(E_TRANSFORMATION_STATE state) const =0
Returns the transformation set by setTransform.
Class representing a color with four floats.
float f32
32 bit floating point variable.
signed int s32
32 bit signed variable.
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 to use high level shaders in this example, if he selected a driver which is capable of doing so.
int main()
{
if (driverType==video::EDT_COUNT)
return 1;
if (driverType == video::EDT_DIRECT3D9 ||
driverType == video::EDT_OPENGL)
{
char i;
printf("Please press 'y' if you want to use high level shaders.\n");
std::cin >> i;
if (i == 'y')
{
UseHighLevelShaders = true;
printf("Please press 'y' if you want to use Cg shaders.\n");
std::cin >> i;
if (i == 'y')
UseCgShaders = true;
}
}
if (device == 0)
return 1;
{
printf("Warning: No Cg support, disabling.\n");
UseCgShaders=false;
}
virtual video::IVideoDriver * getVideoDriver()=0
Provides access to the video driver for drawing 3d and 2d geometry.
virtual gui::IGUIEnvironment * getGUIEnvironment()=0
Provides access to the 2d user interface environment.
Axis aligned bounding box in 3d dimensional space.
GUI Environment. Used as factory and manager of all other GUI elements.
The Scene Manager manages scene nodes, mesh recources, cameras and all the other stuff.
virtual bool queryFeature(E_VIDEO_DRIVER_FEATURE feature) const =0
Queries the features of the driver.
E_DRIVER_TYPE
An enum for all types of drivers the Irrlicht Engine supports.
IRRLICHT_API IrrlichtDevice *IRRCALLCONV createDevice(video::E_DRIVER_TYPE deviceType=video::EDT_SOFTWARE, const core::dimension2d< u32 > &windowSize=(core::dimension2d< u32 >(640, 480)), u32 bits=16, bool fullscreen=false, bool stencilbuffer=false, bool vsync=false, IEventReceiver *receiver=0)
Creates an Irrlicht device. The Irrlicht device is the root object for using the engine.
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().
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;
}
else
{
psFileName = "../../media/d3d9.psh";
vsFileName = "../../media/d3d9.vsh";
}
break;
case video::EDT_OPENGL:
if (UseHighLevelShaders)
{
if (!UseCgShaders)
{
psFileName = "../../media/opengl.frag";
vsFileName = "../../media/opengl.vert";
}
else
{
psFileName = "../../media/d3d9.hlsl";
vsFileName = psFileName;
}
}
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 fulfill 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.
{
device->
getLogger()->
log(
"WARNING: Pixel shaders disabled "\
"because of missing driver/hardware support.");
psFileName = "";
}
{
device->
getLogger()->
log(
"WARNING: Vertex shaders disabled "\
"because of missing driver/hardware support.");
vsFileName = "";
}
virtual void log(const c8 *text, ELOG_LEVEL ll=ELL_INFORMATION)=0
Prints out a text into the log.
virtual ILogger * getLogger()=0
Provides access to the message logger.
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 parameter 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.
s32 newMaterialType1 = 0;
s32 newMaterialType2 = 0;
if (gpu)
{
MyShaderCallBack* mc = new MyShaderCallBack();
if (UseHighLevelShaders)
{
UseCgShaders ? video::EGSL_CG:video::EGSL_DEFAULT;
vsFileName, "vertexMain", video::EVST_VS_1_1,
psFileName, "pixelMain", video::EPST_PS_1_1,
mc, video::EMT_SOLID, 0, shadingLanguage);
vsFileName, "vertexMain", video::EVST_VS_1_1,
psFileName, "pixelMain", video::EPST_PS_1_1,
mc, video::EMT_TRANSPARENT_ADD_COLOR, 0 , shadingLanguage);
}
else
{
psFileName, mc, video::EMT_SOLID);
psFileName, mc, video::EMT_TRANSPARENT_ADD_COLOR);
}
mc->drop();
}
Interface making it possible to create and use programs running on the GPU.
virtual s32 addShaderMaterialFromFiles(io::IReadFile *vertexShaderProgram, io::IReadFile *pixelShaderProgram, IShaderConstantSetCallBack *callback=0, E_MATERIAL_TYPE baseMaterial=video::EMT_SOLID, s32 userData=0)=0
Like IGPUProgrammingServices::addShaderMaterial(), but loads from files.
virtual s32 addHighLevelShaderMaterialFromFiles(const io::path &vertexShaderProgramFileName, const c8 *vertexShaderEntryPointName, E_VERTEX_SHADER_TYPE vsCompileTarget, const io::path &pixelShaderProgramFileName, const c8 *pixelShaderEntryPointName, E_PIXEL_SHADER_TYPE psCompileTarget, const io::path &geometryShaderProgramFileName, const c8 *geometryShaderEntryPointName="main", E_GEOMETRY_SHADER_TYPE gsCompileTarget=EGST_GS_4_0, scene::E_PRIMITIVE_TYPE inType=scene::EPT_TRIANGLES, scene::E_PRIMITIVE_TYPE outType=scene::EPT_TRIANGLE_STRIP, u32 verticesOut=0, IShaderConstantSetCallBack *callback=0, E_MATERIAL_TYPE baseMaterial=video::EMT_SOLID, s32 userData=0, E_GPU_SHADING_LANGUAGE shadingLang=EGSL_DEFAULT)=0
Like IGPUProgrammingServices::addShaderMaterial(), but loads from files.
virtual IGPUProgrammingServices * getGPUProgrammingServices()=0
Gets the IGPUProgrammingServices interface.
E_GPU_SHADING_LANGUAGE
Enumeration for different types of shading languages.
Now it's time for testing 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 rotation animator to make it look more interesting and important.
L"PS & VS & EMT_SOLID",
bool drop() const
Drops the object. Decrements the reference counter by one.
virtual IGUIFont * getBuiltInFont() const =0
Returns the default built-in font.
virtual ITextSceneNode * addTextSceneNode(gui::IGUIFont *font, const wchar_t *text, video::SColor color=video::SColor(100, 255, 255, 255), ISceneNode *parent=0, const core::vector3df &position=core::vector3df(0, 0, 0), s32 id=-1)=0
Adds a text scene node, which is able to display 2d text at a position in three dimensional space.
virtual ISceneNodeAnimator * createRotationAnimator(const core::vector3df &rotationSpeed)=0
Creates a rotation animator, which rotates the attached scene node around itself.
virtual IMeshSceneNode * addCubeSceneNode(f32 size=10.0f, ISceneNode *parent=0, s32 id=-1, const core::vector3df &position=core::vector3df(0, 0, 0), const core::vector3df &rotation=core::vector3df(0, 0, 0), const core::vector3df &scale=core::vector3df(1.0f, 1.0f, 1.0f))=0
Adds a cube scene node.
Animates a scene node. Can animate position, rotation, material, and so on.
void setMaterialTexture(u32 textureLayer, video::ITexture *texture)
Sets the texture of the specified layer in all materials of this scene node to the new texture.
virtual void addAnimator(ISceneNodeAnimator *animator)
Adds an animator which should animate this node.
virtual void setPosition(const core::vector3df &newpos)
Sets the position of the node relative to its parent.
void setMaterialFlag(video::E_MATERIAL_FLAG flag, bool newvalue)
Sets all material flags at once to a new value.
void setMaterialType(video::E_MATERIAL_TYPE newType)
Sets the material type of all materials in this scene node to a new material type.
virtual ITexture * getTexture(const io::path &filename)=0
Get access to a named texture.
Class representing a 32 bit ARGB color.
E_MATERIAL_TYPE
Abstracted and easy to use fixed function/programmable pipeline material modes.
Same for the second cube, but with the second material we created.
L"PS & VS & EMT_TRANSPARENT",
Then we add a third cube without a shader on it, to be able to compare the cubes.
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.
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"));
virtual gui::ICursorControl * getCursorControl()=0
Provides access to the cursor control.
virtual void setVisible(bool visible)=0
Changes the visible state of the mouse cursor.
Scene Node which is a (controlable) camera.
virtual void setTarget(const core::vector3df &pos)=0
Sets the look at target of the camera.
virtual ISceneNode * addSkyBoxSceneNode(video::ITexture *top, video::ITexture *bottom, video::ITexture *left, video::ITexture *right, video::ITexture *front, video::ITexture *back, ISceneNode *parent=0, s32 id=-1)=0
Adds a skybox scene node to the scene graph.
virtual ICameraSceneNode * addCameraSceneNodeFPS(ISceneNode *parent=0, f32 rotateSpeed=100.0f, f32 moveSpeed=0.5f, s32 id=-1, SKeyMap *keyMapArray=0, s32 keyMapSize=0, bool noVerticalMovement=false, f32 jumpSpeed=0.f, bool invertMouse=false, bool makeActive=true)=0
Adds a camera scene node with an animator which provides mouse and keyboard control appropriate for f...
virtual void setTextureCreationFlag(E_TEXTURE_CREATION_FLAG flag, bool enabled=true)=0
Enables or disables a texture creation flag.
Now draw everything. That's all.
int lastFPS = -1;
{
if (lastFPS != fps)
{
core::stringw str = L
"Irrlicht Engine - Vertex and pixel shader example [";
str += "] FPS:";
str += fps;
lastFPS = fps;
}
}
return 0;
}
virtual bool run()=0
Runs the device.
virtual void setWindowCaption(const wchar_t *text)=0
Sets the caption of the window.
virtual bool isWindowActive() const =0
Returns if the window is active.
virtual void drawAll()=0
Draws all the scene nodes.
virtual bool beginScene(bool backBuffer=true, bool zBuffer=true, SColor color=SColor(255, 0, 0, 0), const SExposedVideoData &videoData=SExposedVideoData(), core::rect< s32 > *sourceRect=0)=0
Applications must call this method before performing any rendering.
virtual s32 getFPS() const =0
Returns current frames per second value.
virtual const wchar_t * getName() const =0
Gets name of this video driver.
virtual bool endScene()=0
Presents the rendered image to the screen.
Compile and run this, and I hope you have fun with your new little shader writing tool :).