Dear,

Today we will see how to create a 3D application in D programming using the MVC Pattern. My code is not perfect, if you have suggestions feel free to propose them. :)

What you need

  • a D compiler like LDC
  • Tango library
  • Derelict library for use openGL

Architecture tree

src/
|-- mvc
|   |-- controller
|   |   `-- Controller.d
|   |-- event
|   |   `-- EventListener.d
|   |-- model
|   |   `-- Molecule.d
|   |-- observed
|   |   |-- Observable.d
|   |   |-- Observer.d
|   |   `-- Strategy.d
|   |-- mvc.d
|   `-- view
|       |-- View.d
|       `-- Window.d

Each directory is a package and each D file (*.d) a module.

  • Controller.d module is used to check events and datas.
  • EventListener.d module contains the structure to store events.
  • Molecule.d is a model of how it should be displayed.
  • Observed.d package contains some modules to implement some patterns.
  • View package contains some modules to display datas.
  • mvc.d is the main file.

Code Source

mvc.d

module mvc.mvc;
 
private import mvc.model.Molecule;
private import mvc.view.Window;
private import mvc.controller.Controller;
 
void main(char[][] args){
    Molecule    model       = new Molecule(10000,10);
    Window      window      = new Window(800, 600, "mvc", model);
}

The order in which you write your code is very important. At first, you need to create your model and to give a reference to the controller constructor. Then, you have to create an instance of your view with some parameters as a reference of controller. At last, you give a reference of this view to the controller and you run your application here with window.create().

Observer.d

module mvc.observed.Observer;
 
public interface Observer{
    public void update();
}

Observable.d

module mvc.observed.Observer;
 
public interface Observer{
    public void update(float[][] coordinates);
    public void update(size_t index, float[] coordinate);
}

Strategy.d

module mvc.model.Strategy;
 
public interface Strategy{
    public float[][]    getCoordinates3D();
    public float[]      getCoordinate3D(size_t index);
    public void         setCoordinates3D(float[][] coordinates3D);
    public void         setCoordinate3D(size_t index, float[] axis);
}

View.d

module mvc.view.View;
 
private import mvc.controller.Controller;
private import mvc.observed.Observer;
private import mvc.observed.Observable;
public import mvc.model.Molecule;
 
 
public abstract class View: Observer {
    // Width window
    protected uint          width;
    // Height window
    protected uint          height;
    // Window title
    protected char[]        title;
    // Controller
    protected Controller    controller;
    // Model
    protected Molecule      model;
 
    public this(uint width, uint height, char[] title, ref Molecule model){
        this.width      = width;
        this.height     = height;
        this.title      = title;
        this.model      = model;
        this.controller = new Controller(model, this);
    }
 
    abstract public void update();
}

The view takes window size and a reference to the controller instance.

Window.d

module mvc.view.Window;
 
private import derelict.sdl.sdl;
private import derelict.sdl.sdltypes;
private import derelict.opengl.gl;
private import derelict.opengl.glu;
private import derelict.util.compat;
 
private import tango.stdc.stringz;
private import tango.math.Math;
import tango.io.Stdout;
 
private import mvc.view.View;
private import mvc.controller.Controller;
 
public class Window : View{
    private static uint nbArray = 0;
    private float           alpha;
    // Window
    private SDL_Surface     *screen;
    // Number of bits per pixel used for display. 24 => true color
    private uint            bitsPerPixel;
    // Field of view => the angle our camera will see vertically
    private float           fov;
    // Distance of the near clipping plane
    private float           nearPlane;
    // Distance of the far clipping plane
    private float           farPlane;
    // Event
    private SDL_Event       event;
    // Pixel on
    private float[][]      coordinates3D;
 
    public this(uint width, uint height, char[] title,ref Molecule model){
        super(width, height, title, model);
        this.screen         = null;
        this.bitsPerPixel   = 24;
        this.alpha          = 0.0f;
        this.fov            = 45.0f;
        this.nearPlane      = 0.1f;
        this.farPlane       = 100.0f;
        this.coordinates3D  = model.getCoordinates3D();
        Window.nbArray++;
        // Initialize SDL Derelict modules
        DerelictSDL.load();
        // Initialize GL Derelict modules
        DerelictGL.load();
        // Initialize GLU Derelict modules
        DerelictGLU.load();
        // Initialize SDL's VIDEO module
        SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);
        // Set buffer size
        SDL_GL_SetAttribute(SDL_GL_BUFFER_SIZE, 16);
        // Set depth size
        SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
        // Set stencil sizse
        SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 0);
        // Enable double-buffering
        SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
        // Create our OpenGL window
        SDL_SetVideoMode(height, width, bitsPerPixel, SDL_OPENGL);
        // Set window title
        SDL_WM_SetCaption(toStringz(title), null);
        // switch to the projection mode matrix
        glMatrixMode(GL_PROJECTION);
        // load the identity matrix for projection
        glLoadIdentity();
        // Specifies the aspect ratio that determines the field of view in the x direction. The aspect ratio is the ratio of x (width) to y (height)
        float aspect = cast(GLfloat)height / cast(GLfloat)width;
        // Setup a perspective projection matrix
        gluPerspective(fov, aspect, nearPlane, farPlane);
        // Switch back to the modelview transformation matrix
        glMatrixMode(GL_MODELVIEW);
        // Load the identity matrix for modelview
        glLoadIdentity();
        // Create the window
        create();
    }
 
    public ~this(){
        cleanup();
    }
 
    public void refresh(){
        SDL_Flip(screen);
    }
 
    private void create(){
        SDL_Event   event;
        bool        isRunning   = true;
 
        this.screen = SDL_SetVideoMode(width, height, 0, SDL_OPENGL);
        if(this.screen == null)
            throw new Exception("Failed to set video mode: " ~ toDString(SDL_GetError()));
 
        glClearColor(0.0f,0.0f,0.0f,1.0f);
        glClearDepth(1.0f);
        glEnable(GL_DEPTH_TEST);
        glDepthFunc(GL_LEQUAL);
        glLineWidth(0);
        controller.initModel();
 
        while(isRunning){
            SDL_PollEvent(&event);
            switch(event.type){
                case SDL_MOUSEMOTION:
                    break;
                case SDL_MOUSEBUTTONDOWN:
                    break;
                // user has clicked on the window's close button
                case SDL_QUIT:
                    isRunning = controller.quit(event.type);
                    break;
                // by default, we do nothing => break from the switch
                default:
                    display();
                    break;
            }
        }
    }
 
    private void cleanup(){
        // tell SDL to quit
        if(SDL_Quit !is null)
            SDL_Quit();
        // release GL, GLU and SDL's shared libs
        DerelictGLU.unload();
        DerelictGL.unload();
        DerelictSDL.unload();
    }
 
    private void clear(){
        glClear(GL_COLOR_BUFFER_BIT);
    }
 
    private void display(){
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);//definit couleur de fond
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glViewport(0,0,width,height);
        glPushMatrix();
        glTranslatef(0.0f, 0.0f, -40.0f);
        glRotatef(alpha,0.0f,1.0f,0.0f);
        glRotatef(30.0f,0.0f, 1.0f, 0.0f); //rotation 30 degre autour axe x
        glRotatef(45.0f,1.0f, 0.0f, 0.0f);
        draw();
        glPopMatrix();
        alpha = alpha + 0.1f;
        SDL_GL_SwapBuffers();//pour interchanger les buffers
  }
 
    public void draw(){
        if(coordinates3D != null && coordinates3D[0] != null){
            float[][] colorArray = createColors();
            glBegin (GL_LINE_STRIP);
            for(size_t color = 0; color < coordinates3D[0].length; color++){
                glColor3f(colorArray[0][color], colorArray[1][color], colorArray[2][color]);
                glVertex3f(coordinates3D[0][color],coordinates3D[1][color],coordinates3D[2][color]);
            }
            glEnd();
        }
    }
 
    public void update(){
        this.coordinates3D = model.getCoordinates3D();
        display();
    }
 
    private float[][] createColors(){
        float[][] colors = new float[][](3);
 
        // RGB are each on [0, 1]. S and V are returned on [0, 1] and H is
        // returned on [0, 6]. Exception: H is returned UNDEFINED if S==0.
        float r, g, b, h, s, v, m, n, f;
        int i;
        size_t index = 0;
        for (size_t k=0; k < coordinates3D[0].length; k++){
            //h = k * 360/ num;
            h = PI*2.0f/coordinates3D[0].length * k;
            s = 1.0f;
            v = 1.0f;
 
            i = cast(int)floor(h);
            f = h - i;
            if ( !(i&1) ) f = 1 - f; // if i is even
            m = v * (1 - s);
            n = v * (1 - s * f);
            switch (i) {
                case 6:
                case 0: r=v; g=n; b=m; break;
                case 1: r=n; g=v; b=m; break;
                case 2: r=m; g=v; b=n; break;
                case 3: r=m; g=n; b=v; break;
                case 4: r=n; g=m; b=v; break;
                case 5: r=v; g=m; b=n; break;
                default:break;
            }
 
            if(colors[0].length == index){
                colors[0].length = colors[0].length+50;
                colors[1].length = colors[1].length+50;
                colors[2].length = colors[2].length+50;
            }
            colors[0][index]=r; colors[1][index]=g; colors[2][index]=b;
            index++;
        }
        //resize
        colors[0].length = index;
        colors[1].length = index;
        colors[2].length = index;
        return colors;
    }
 
}

Window class inherits from View class. The constructor initializes an openGL/SDL application. There is a destructor for to quit properly an openGL application.
The create method is used to create a window and to handle user events. This method contains the main loop i.e while(isRunning) statement . Each turn of loop SDL give the last event with SDL_PollEvent(&event);. Each event is sent to controller.
Method createColors() converts a coordinate point to an openGL colour for fun. :)

Controller.d

module mvc.controller.Controller;
 
private import derelict.sdl.sdl;
private import mvc.model.Molecule;
private import mvc.event.EventListener;
private import mvc.observed.Observer;
private import mvc.observed.Observable;
private import mvc.view.View;
 
public class Controller: Observer{
 
    private Molecule    model;
    private View        view;
    // Event handler
    private EventListener   eventListener;
 
    public this(ref Molecule model, ref View view){
        this.model  = model;
        this.view   = view;
        model.addObserver(this);
    }
 
    public void initModel(){
        model.create();
    }
 
    public bool quit(ubyte type){
        eventListener ~= (ubyte type)   {
                                            delete view;
                                        };
        eventListener(type);
        eventListener -= (ubyte type){};
        return false; // is running => no
    }
 
    public void update(){
        // do something
    }
}

We use a struct eventListener to handle events. At each turn, an event is added to eventListener i.e SDL_QUIT with symbol ~=. In fact, we add to the delegate several things to do. When you do eventListener(event.type); the function stored in the delegate for this event type will run. Here it is a little example but you can perform by addind a timer and handle a double click.

EventListener.d

module mvc.event.EventListener;
 
private import Array = tango.core.Array;
 
struct EventListener{
    alias void delegate(ubyte) delegateEvent;
    delegateEvent[] events;
 
    void opCall(ubyte event){
        foreach(eventInvocation; events)
            eventInvocation(event);
     }
 
    void opCatAssign( delegateEvent eventsInvocation ){
        events ~= eventsInvocation;
    }
 
    void opSubAssign( delegateEvent eventsInvocation ){
        Array.remove(events, eventsInvocation);
        events.length = events.length - 1;
    }
}

This structure is used to handle event. In D programming, to handle an event it is the same way as C#. You need to use a delegate, you can see to in some examples written in C# that the way is close. Here, we overload 3 operators:

  • () as opCall when you do eventListener(event.type); all things stored will run
  • ~= as opCatAssign you add several things to do in queue (delegate array)
  • -= as opSubAssign once time you have run all things to do for an event, you can remove these things from queue

Molecule.d

module mvc.model.Molecule;
 
private import tango.math.random.Random;
private import tango.math.Math;
private import Array    = tango.core.Array;
import tango.io.Stdout;
 
private import mvc.observed.Strategy;
private import mvc.observed.Observer;
private import mvc.observed.Observable;
 
class Molecule : Strategy, Observable{
    private size_t      numberOfAtom;
    private size_t      radius;
    private float[][]   coordinates3D; //coordinates3D[0] -> x, coordinates3D[1] -> y, coordinates3D[2] -> z
    // Observer array;
    private Observer[]  observers;
 
    public this(size_t numberOfAtom, size_t radius){
        this.numberOfAtom           = numberOfAtom;
        this.radius                 = radius;
        this.coordinates3D          = new float[][](3);
        this.coordinates3D[0].length= numberOfAtom;
        this.coordinates3D[1].length= numberOfAtom;
        this.coordinates3D[2].length= numberOfAtom;
    };
 
    public void create(){
        size_t theta    = 0;
        size_t phi      = 0;
        size_t counter  = 0;
        while(counter < numberOfAtom){
            if(counter == 0){
                theta   = rand.uniformR(180);
                phi     = rand.uniformR(360);
            }
            else{
                theta   = rand.uniformR2( theta, theta+5 );
                phi     = rand.uniformR2( phi, phi+5 );
            }
            if(coordinates3D[0].length == counter){
                 coordinates3D[0].length = coordinates3D[0].length + 50;
                 coordinates3D[1].length = coordinates3D[1].length + 50;
                 coordinates3D[2].length = coordinates3D[2].length + 50;
            }
            // X
            coordinates3D[0][counter] = radius * sin(theta*PI/180) * cos(phi*PI/180);
            // Y
            coordinates3D[1][counter] = radius * sin(theta*PI/180) * sin(phi*PI/180);
            // Z
            coordinates3D[2][counter] = radius * cos(theta*PI/180);
            counter++;
        }
        // resize array
        coordinates3D[0].length = counter;
        coordinates3D[1].length = counter;
        coordinates3D[2].length = counter;
        notify();
    }
 
    /* *********************************************
     * Stragtegy
     */
 
    public float[][] getCoordinates3D(){
            return this.coordinates3D.dup;
    }
 
    public float[] getCoordinate3D(size_t index){
            return this.coordinates3D[index].dup; // Array of x,y,z
    }
 
    public void setCoordinates3D(float[][] coordinates3D){
            this.coordinates3D = coordinates3D;
            notify();
    }
 
    public void setCoordinate3D(size_t index, float[] axis){
            this.coordinates3D[index] = axis;
            notify();
    }
 
    /* *********************************************
     * Observable
     */
 
    public void addObserver(Observer observer){
        observers.length= observers.length + 1;
        observers[$-1]  = observer;
    }
 
    public void removeObserver(Observer observer){
        Array.remove(observers, observer);
        observers.length = observers.length - 1;
    }
 
    public void removeObserver(size_t index){
        Observer tmp[] = observers[index+1..$].dup;
        observers[index..$-1] = tmp;
        observers.length = observers.length - 1;
    }
 
    public void notify(){
        foreach(observer; observers)
            observer.update();
    }
}

This object it is a model and stores atom coordinate in space. You can add some observers each time controller accepts a change, controller notifies model and model updates the view, the view converts data to graphic.

Build

just do in your source directory:

$ ldc  -g -w -L -ldl -L -lDerelictGL -L -lDerelictGLU  -L -lDerelictSDL -L -lDerelictUtil $(find . -name "*.d")

The end

It is a big code for an how-to, several new things. You will need to read more than once to understand all; otherwise you are a chief :) . OpenGL syntax is the same as in C, I think that the most harder thing to understand i think it is the event handler system. You can take a look to C# code or all things about delegate/clojure. Delegate system it is the same thing as function pointer in C.

Thanks for all

signed: Bioinfornatics aka Jonathan MERCIER