Sunday, August 21, 2011

Introducing shaders in D

Update 3/15/2012: This code is quite old, consider my new article Blur effect, which uses Derelict3, OpenGL3+ API and SCons.

Last weekend I was at the Evoke, the largest German demoparty held in Cologne. I will release a story about this weekend, because it is the first time I go to a demoparty. Demos heavily rely on shaders to take advantage of the calculation power of GPU. But actually, I never wrote a shader program. I met another newbie, and we were willing to write some shaders and the associated OpenGL program to load them during the event. But because it would be too simple to do it in C++, I wanted to test the D programming language.

At first sight, D may look like C++. Well, its syntax is based on the C/C++ and it is quite easy to learn for developers used to this language family. But D improves some features of the C++, and sometimes it just breaks the old legacy. I would say it's to compare with C# and Java, but in another direction. I can't tell right now the advantage of D over these two languages because I'm inexperienced, but maybe after some prototypes...

Update 22/8/2011: here's what Nical told us about D:
  • it compiles VERY fast (productivity++)
  • you don't suffer from the C/C++'s horrible include system (productivity++)
  • the syntax is much clearer than C++ and is easy to parse for static analysis tools (software engineering++)
  • the behaviour of the language is more concistent than C++'s (ninja coding++)
  • it encourages unittesting (TDD++)
  • by design, the language does not let you do dangerous things unless you explicitly want to
  • it has true support for parallel programming
  • it is awesome
  • it is fast (you can even turn off garbage collection in critical portions of code)
  • it has awesome metaprogramming facilities (that C++/boost is not likely to have any time soon) and full compiletime introspection (on top of which it is easy to build run time introspection)
  • and a thousand of other things that makes you realize that C++ is a powerful but inconcistent, error prone, ambiguous and unproductive language.
Lacks a little bit of visibility on internet, some tutorials and dedicated libraries, though (yet it can use any C library).

I didn't want to write tutorials on this blog, but we had pain to install the environment and do a basic program, so I write the procedure we followed. This is for Windows and uses

Installing the environment on Windows

You also have to install a SVN command-line interface because DSSS uses it during the installation. TortoiseSVN does NOT have this interface. I installed Win32SVN.

Go to http://www.digitalmars.com/d/download.html and download the dmd Windows installer on top of the page. Launch the installer: it asks you to choose components to install. Check D1 and D2. Uncheck dmc unless you really want it. Continue the installation.

Once installed, open a terminal: press the keys Windows + R, type cmd and validate.

C:\Users\MyUserName>

Checkout the DSSS' repository somewhere, but avoid C:\dsss as you will understand later. In this example, I use the directory D:\Code\dsss.

C:\Users\MyUserName> d:
D:\> cd Code
D:\Code> svn checkout http://svn.dsource.org/projects/dsss/trunk dsss

Type the following lines (the original ones are in Makefile.dmd.win). You may change the installation path at the last line.

D:\Code> cd dsss
D:\Code\dsss> copy rebuild\defaults\dmd-win rebuild\rebuild.conf\default
D:\Code\dsss> rebuild\rebuild.exe -full -Irebuild sss\main.d -ofdsss_int
D:\Code\dsss> dsss_int.exe build
D:\Code\dsss> dsss.exe install --prefix=C:\dsss

Change your PATH (see http://www.windows7hacker.com/index.php/2010/05/how-to-addedit-environment-variables-in-windows-7/ for example) by adding the directory C:\dsss\bin and removing the directory C:\D\dmd\bin. It's a little bit crazy, because dsss has to be compiled in D version 1, but then we want to use it with the version 2. You may remove the directory C:\D\dmd as well because we don't use it anymore.

Close the terminal and open a new one (to take the new PATH into account). Checkout the Derelict2's repository (I still use D:\Code).

D:\Code> svn checkout http://svn.dsource.org/projects/derelict/branches/Derelict2 derelict

Compile the wrappers for SDL and OpenGL.

D:\Code> cd derelict
D:\Code\derelict> dsss build DerelictSDL DerelictGL
D:\Code\derelict> dsss install

And you finally have your environment. Phew!

Interfacing SDL and OpenGL with D

The bindings Derelict provides are very useful. They allow us to use the functions and constants as if it was written in C++.

Here is a simple program we made, based on this tutorial, refactored and expanded to load the shaders.

module main;
 
private 
{
 import core.time;
 import core.thread;
 import derelict.sdl.sdl;
 import derelict.opengl.gl;
 import derelict.opengl.glext;
 import derelict.opengl.glu;
 import std.file;
 import std.stdio;
 import std.string;
}
 
class Shader
{
 private GLuint shader = 0;
 
 public ~this()
 {
  glDeleteShader(shader);
 }
 
 public bool loadShaderFromFile(GLenum type, string filename)
 {
  try
  {
   shader = glCreateShader(type);
   string code = readText(filename);
   return loadShader(code);
  }
  catch (Exception)
  {
   return false;
  }
 }
 
 private bool loadShader(string code)
 {
  const char* ptr = code.ptr;
  int len = code.length;
 
  glShaderSource(shader, 1, &ptr, &len);
 
  glCompileShader(shader);
 
  return true;
 }
}
 
class Program
{
 private GLuint program;
 
 public this()
 {
  program = glCreateProgram();
 }
 
 public ~this()
 {
  glDeleteProgram(program);
 }
 
 public void attachShader(Shader shader)
 {
  glAttachShader(program, shader.shader);
 }
 
 public void link()
 {
  glLinkProgram(program);
 }
 
 public void use()
 {
  glUseProgram(program);
 }
 
 static void useFixed()
 {
  glUseProgram(0);
 }
 
 public GLint getUniformLocation(string name)
 {
  return glGetUniformLocation(program, toStringz(name));
 } 
}
 
class Display
{
 private uint height;
 private uint width;
 private uint bitsPerPixel;
 private float fov;
 private float nearPlane;
 private float farPlane;
 
 private Program program;
 private GLint timeLoc;
 
 private double time;
 
 public this()
 {
  height = 800;
  width = 600;
  bitsPerPixel= 24;
  fov = 90;
  nearPlane = 0.1f;
  farPlane = 100.f;
 
  time = 0;
 
  DerelictSDL.load();
  DerelictGL.load();
  DerelictGLU.load();
 
  setupSDL();
  setupGL();
 
  DerelictGL.loadClassicVersions();
  DerelictGL.loadModernVersions();
 
  setupShaders();
 }
 
 private void setupSDL()
 {
  SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);
  SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
  SDL_SetVideoMode(height, width, bitsPerPixel, SDL_OPENGL);
  SDL_WM_SetCaption(toStringz("D is the best"), null);
 }
 
 private void setupGL()
 {
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(fov, cast(float)height / width, nearPlane, farPlane);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
 }
 
 private void setupShaders()
 {
  Shader vertexShader = new Shader();
  vertexShader.loadShaderFromFile(GL_VERTEX_SHADER, "shader.vert");
 
  Shader fragmentShader = new Shader();
  fragmentShader.loadShaderFromFile(GL_FRAGMENT_SHADER, "shader.frag");
 
  program = new Program();
  program.attachShader(vertexShader);
  program.attachShader(fragmentShader);
  program.link();
  program.use();
 
  timeLoc = program.getUniformLocation("time");
 }
 
 public static void cleanup()
 {
  SDL_Quit();
 
  DerelictGLU.unload();
  DerelictGL.unload();
  DerelictSDL.unload();
 }
 
 public void update(double dt)
 {
  time += dt;
 
  glUniform1f(timeLoc, time);
 }
 
 public void render()
 {
  glClear(GL_COLOR_BUFFER_BIT);
 
  glBegin(GL_TRIANGLES);
 
  glColor3f (1, 0, 0);
  glVertex3f(-1, -1, -2);
  glColor3f (0, 1, 0);
  glVertex3f(1, -1, -2);
  glColor3f (0, 0, 1);
  glVertex3f(0, 1, -2);
 
  glEnd();
 
  SDL_GL_SwapBuffers();
 }
 
 public bool event()
 {
  SDL_Event event;
  while (SDL_PollEvent(&event))
  {
   switch (event.type)
   {
    case SDL_QUIT:
     return false;
     break;
 
    case SDL_KEYDOWN:
     if(event.key.keysym.sym == SDLK_ESCAPE)
     {
      return false;
     }
     break;
 
    default:
     break;
   }
 
  }
 
  return true;
 }
}
 
int main()
{
 Display display = new Display();
 
 TickDuration lastTime = TickDuration.currSystemTick();
 TickDuration newTime;
 TickDuration dt;
 
 while (display.event())
 {
  newTime = TickDuration.currSystemTick();
  dt = newTime - lastTime;
  lastTime = newTime;
 
  display.update(dt.length / cast(double)TickDuration.ticksPerSec);
 
  display.render();
 
  Thread.sleep(10_000);
 }
 
 display.cleanup();
 
 return 0;
}

Save this code in a file called main.d and then compile it with DSSS:

D:\Code\shader-d> dsss build main.d

It creates an executable main.exe, which works. If you plan to extend the application, you may write a dsss.conf (configuration file for DSSS). However it will stay light because unlike in C++, D mixes code and interface, and has a module / import system thus loads automatically other source files.

But there are no shaders. Let's write them.

Simple shaders in GLSL

Note that they are not compliant with GLSL 1.30, their purpose is just to show that it works.

shader.vert (vertex shader):

uniform float time;

void main()
{
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
    gl_FrontColor = gl_Color * (0.5 + 0.5 * sin(time));
}

shader.frag (fragment shader):

void main()
{
    gl_FragColor = gl_Color;
}

Save them on main.d's directory and relaunch the program.
A triangle which likes to disappear.

Voilà, we have a program in D which can load shaders. I will try to do some prototypes in 3D using shaders, and maybe in a few months I'll be able to write my own demo... :D

Edit: Someone has got issues following this tutorial on Arch Linux. Eventually it worked at the end.

No comments:

Post a Comment