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