CSE3313 - Computer Graphics
assessment

CSE3313
Computer Graphics

Assignment 1 Marks: General Comments on Submissions

Prepared by the markers: Ben Porter and Jon McCormack.

 


Synopsis


Specific comments and feedback are provided when you get your mark. Here are some general comments and further explanation of common mistakes for the Superformula Assignment.

Generally everyone went as well as expected, however the quality of the software design and coding was, in general, poor. Also some OpenGL concepts seemed to be misunderstood. The following is a summary of the major issues people had, and suggestions for fixing them.

1. Makefile and README

Some submissions showed a reasonable understanding of how to use make and Makefiles. Others looked like they were copied from the first google hit under "poor makefile". Hence, a few tips are in order. Here's what a simple Makefile for the assignment should look like:

CFLAGS= -Wall -I/usr/include/GL -I/usr/X11R6/include/
LIBS= -lX11 -lXi -lXmu -lglut -lGL -lGLU
SRCS = main.c drawing.c superformula.c
OBJS = main.o drawing.o superformula.o
PROGRAM = superformula
	
$(PROGRAM): $(OBJS)
	$(CC) -o $@ $(OBJS) $(CFLAGS) $(LIBS)
clean: -rm -f $(PROGRAM) $(OBJS)

Note that the clean target doesn't do anything silly like rm *.o.

This makefile doesn't look at header file dependencies. If it did it would be better. Likewise there is a better way to convert SRCS to OBJS. I'll leave these modifications as an exercise.

README files should include the following information:

  1. Program Title, Your name, student id
  2. Any assumptions you've made when implementing your solution
  3. Basic operation instructions and special features (there is no need to repeat what is listed in the assignment sheet – key functions for example).
  4. Any limitations, bugs, or features not implemented
  5. Description of how your program works – list the key algorithms and data structures you've used to implement the solution. Do not list every single function you've written and what it does. Provide a summary of your design and pointers to the relevant parts of your code.

The README does not need to be longer than an A4 page. It should be submitted in plain text, suitable for viewing in a 80 column text editor (e.g. vim).

Also a general point: please read the assignment sheet carefully. Please listen to the advice given by the lecturer in the lectures and tutes. If you are unsure please ask for advice. A number of submissions did not read the specifications correctly or interpreted them incorrectly.


2. OpenGL

Most submissions displayed a reasonable basic understanding of OpenGL in 2D. A number of submissions had trouble with the resize and zoom using multiple viewports. The key thing to understand is how a primitive goes from world co-ordinates to screen co-ordinates and then pixels set on the screen. The basic process should be:

  1. divide the screen into viewports with a fixed ratio of screen size (e.g. viewport 1 uses 50% horizontally and 100% vertically).
  2. call glViewport
  3. Set matrix mode to GL_PROJECTION
  4. call gluOrtho2D to map world coordinates to screen coordinates within the viewport. This involves matching the aspect ratio of the viewport (calculated from the screen resolution and coverage of the current viewport. If the view can zoom this will also needed to be included in the calculation.
  5. Set matrix mode to GL_MODELVIEW

Display Lists

There were a couple of problems here. The first was that some people were recompiling their display lists every time display() was called – this usually implies that your geometry/drawing instructions are too dynamic to utilise the benefits of display lists, or that you are incorrectly using them. Display lists should be used for geometry that doesn't change every frame. The other problem is less critical but still a valid point – almost everyone was calling glNewList with identifiers that they created. This is prone to error and glGenLists should be used for each new single or set of identifiers that you need.

gl[Push/Pop][Matrix/Attribs]

Consider the following code:

glTranslatef(1,0,0); drawSuperFormula(...); glTranslatef(-1,0,0); drawSphere(...);

...we can all see clearly what it does (draws a superformula at (1,0,0) and draws a box at (0,0,0)). However, we are all assuming that drawSuperFormula(...) doesn't modify the ModelView or Projection matrices upon returning --- this is a logical (but unspoken) assumption about the behaviour of drawSuperFormula(). In fact, we can assume the same about any function called drawXXX() simply because of the name of the function. Unfortunately many submissions violated this by immediately calling glLoadIdentity() upon entering a drawXXX() function, thus destroying the current transformation matrix. In conclusion, if you modify something in a function, make sure you restore it to its original state UNLESS the function name/description implies that it is going to modify that state (e.g., a function called setGLColour(...) is obviously going to modify the current colour). You can use glPush/PopAttribs to store/restore all other state information (including colour, lighting, texture info, ...).

On the other side of the function call, we can also assume that the caller has set up the transformation matrices appropriately to position/scale/rotate the object in the correct manner ... so all you need to do is draw the object at the local origin. This is how glutCube, glutSphere, etc, all work.

void drawSuperFormula(...)
{
  // we assume the user has selected the modelview matrix as this is a "drawXXX" call..
  // store current state
  glPushMatrix();

  ... draw the super formula here ...

  // restore transformation matrix
  glPopMatrix();
}

3. Coding

Writing robust and efficient programs still seems to be a challenge for many of you. Here are some common mistakes.

Global variables ARGH!

Please refer to your software engineering textbooks for good style and usage of global variables. The general idea is that you shouldn't be sharing global variables between files, you should have a naming convention which identifies them as global (e.g., append a g_ to each), you shouldn't have more than say 7 (and if you have more than this then you should make your code more modular (with e.g., a global variable ViewportParameters that holds all the globals related to viewport parameters)). As someone reading your code, I should be able to look at a single function and figure out what it does by examining its input's (parameters), its internal logic, and its output – I can't do this if some of its inputs/outputs are via globally shared variables, and the entire logic of your system becomes a tangled mess. As a general rule, if your system has lots of globals then it is poorly designed.

Modularity

This is also related to the above point about global variables. You should design your system out of modular components -- these components should be clearly defined by an interface (a set of functions) and should be general enough to be able to be used in other places. Separate modules should go in separate files, and compiled into separate objects. A good example of a module which I saw in a few submissions is a Viewport module. This is either a viewport class, or a set of viewport related functions and data structures, that took over all the responsibility of setting up and managing the different viewports in the system. You could have a further module Window that is responsible for managing a collection of viewports and assigning them different rendering tasks. It was disappointing and confusing that many submissions were in a single large source file.

A further point I want to make is on loosely coupled systems. Your modules should each do a large amount of (specific) work, and you should limit the communication between different modules. One thing I saw a lot of is a huge list of get/set methods in the SuperFormula module, one for each parameter. Consider whether this was necessary in your design. For example, if you needed that data in order to print out the current parameters then maybe a function SuperFormula::toString() would be better ... thus loosening the coupling in the system and reducing the dependency on X to know about the different parameters that SuperFormula has.

The set of modules that I would have liked to see are:

GLUT and OO

Glut is not an object-oriented toolkit, which makes it a bit tricky to incorporate into an OO design. One particular problem is the use of function pointers in the callbacks ... we would ideally like to pass pointers to member functions of classes, but we can't. One solution to this problem is as follows...

class Window
{
  Window* win;
  
  public:
  Window(int* argc, char** argv){win = this; glutInit(argc,argv); init();}
  
  void init(){ ... blah blah ... glutDisplayCallback(&Window::DISPLAY);}
  void run(){... glutMainLoop();} 
 
  static void DISPLAY(){win->display();}
};

...

void main(int argc, char** argv)
{
  Window win(&argc,argv);
  win.run();
}

In that code I am using static class functions as wrappers that call a singleton's appropriate member function. Now we can also derive a new class from Window and this solution will still work (if display() is virtual).

Miscellaneous

PI and other maths
Many people were defining their own pi constant. Both the C standard library math.h and the C++ library cmath supply pi as M_PI.

If you want to square a number don't do this: y = pow(x,2.0) its very inefficient and prone to rounding errors. Use y = x * x; or better define a macro so you can say y = SQUARED(x); or y = SQUARED(x + 2); [equivalent to (x+2)(x+2)]


Some people were using their own custom linked list. The STL (for the C++ people) provides a powerful set of containers (e.g., a linked list std::list) that you should use – they make your code more readable, faster, and it's good practice!

GL_POLYGON was used by a large number of submissions. GL_POLYGON only works for convex polygons (a shape is convex if you can connect any two points inside the shape by a single straight line without crossing the shape boundary). The best solution for this assignment was to use GL_TRIANGLE_FAN.

Avoid the use of "magic numbers" in your code:
Consider statements like: glScale(0.61, 0.61, 0.61) or char s[20]
This is bad programming. Its not clear why you need to scale by that amount. In the second example its not clear why you need an array of exactly 20 characters. What if you need more space? The identifier name ('s') gives no indication of what the purpose of this array is. Here's a better solution:

#define STRING_BUFFER_SIZE		20

// some time later...

char tmpString[STRING_BUFFER_SIZE];
Yes, its a bit more typing but you'll save in the long run because by using descriptive names you make the code easier to understand and so need fewer comments in the code. Also in this case, you can then do checks to see that you haven't exceeded the space you've allocated for a temporary buffer (e.g. ASSERT(strlen(newString) < STRING_BUFFER_SIZE)). If sometime later in your programs development you decide you need more space for buffers you only need to change the STRING_BUFFER_SIZE at one location, even though you might be using many temporary buffers throughout your program.

Note that this solution is not applicable if you're using C++. In fact in C++ the need to #define constants is very rare – use the const qualifier because this gives you stricter type checking and more traceable errors. Also, C++ has a built-in variable length string type so you don't normally need temporary buffers.

The basic thing to remember is that if you find yourself putting any numeric or other constants in your code, consider defining them as constants in an associated header file.


Factorise code
I saw lots of code like this:

  glRasterPos2f(-9.0, -1.5);
  writetext("[+/-]: inc/dec theta");

  glRasterPos2f(-9.0, -2.5);
  writetext("[ s / S ]: inc/dec scale");

  glRasterPos2f(-9.0, -3.5);
  writetext("[up/dn]: move in y");

  glRasterPos2f(-1.5, -3.5);
  writetext("[left/right]: move in x");

  glRasterPos2f(-9.0, -4.7);
  writetext("[Z]: zoom in");

  etc...

Why not add an parameter to writetext to include a specification of the position? You've instantly cut the amount of code you have to write in half! Better still, write a function that takes strings with new lines in them and then calls writetext so then you only need one function call for all the text, which you can predefine (or even put in a display list).


if-then-else chains
Another common mistake is using long if-then-else chains when a case statement would be simpler and clearer. It also handles exceptional cases better via the default: case.

Here's another example I saw a lot:

#define BLACK		0
#define GREEN		1
#define RED			2
...

  if (choice == 1)
  	colour = BLACK;
  else if (choice == 2)
  	colour = GREEN;
  else if (choice == 3)
    colour = RED;
  else if (choice == 4)
  ... etc. etc. 20-30 more lines of this

Why not just say:

colour = choice - 1;

You just saved yourself typing in 30+ lines of code!
Even better change the colour constants to match the choice variable.

In many situations you have to set a variable or variables based on the value of some other variable, often a choice set. The best way to do this is to develop some intuitive mapping between the variable and choice set so you can use numeric or other functions/expressions rather than long if-then-else statements. I've written quite a bit of code over the years and I can't think of a time when I've had to have more than 3 successive it-then-else chains at any one level. So if you find yourself doing this, think of my smiling face telling you to STOP IT RIGHT AWAY!


Cut & Paste
Turn off or do not use cut and paste in your text editor when programming. Real programmers don't cut and paste, they think about how they can turn repeated code into functions :-).


Dynamic Memory
Pointers and dynamic allocation at run time are difficult concepts to understand and require a fair bit of practice and time. Clearly, many of you still need to "do the time". For example, I see quite a bit of this:

char * text = (char *)malloc(sizeof (char) * 25);

This is a waste of time. Malloc is used to allocate memory when you don't know how much you'll need at compile time. Clearly in this case we do know (we'll need 25 chars worth thanks, but see comments on magic numbers in code). So this can be replaced with:

char text[BUFFER_LENGTH];

Where we would define BUFFER_LENGTH to be 25.

For the assignment, you need a dynamic data structure because you don't know at compile time how many sets of superformula primitives are required by the user. Using linked lists, queues, or dynamic vectors (arrays) are all acceptable solutions. It seems a number of people learnt how to do a linked list in 1st year and are still using the same code without understanding it (shame!). Basic data structures like these are the bread and butter of programming – if you want to call yourself a professional computer scientist you should know how to implement these basic data structures from scratch!

Analysis of some submissions also revealed things like calling free on the same pointer twice (that is illegal) or allocating new memory to a pointer before freeing what it previously pointed to.

Pointer problems and memory leak (failing to free memory allocated to a program when it is no longer needed) are the two most common sources of bugs in C programs. That's why java was invented :-(


Static Initialisation in C++
A few of you decided to use C++ to do the assignment. Good choice, however there is a common problem with a number of submissions due to static initialisation. In this case an object representing a primitive was created during static initialisation (i.e. before main is called, for example when an object is defined at global scope). While this is of course legal in C++ it can cause problems if your object's constructor calls any OpenGL (or creates other objects that do). Since you haven't had a chance to enter main yet, you haven't been able to call glutInit() or glutCreateWindow(). It is necessary to call these functions before making any OpenGL calls. The solution here is not to put OpenGL calls in any classes where you might want to do static initialisation. Alternatively, you can defer initialisation until after you've set up OpenGL correctly.

Also be aware that the C++ standard does not specify the order of static initialisation so if you have interdependencies between two different objects you want to initalise before main is called (i.e. object A needs initialisation before object B) you will be in trouble. What's worse is that such a scheme may work on some platforms and not others so it will not always appear as a bug.

One more general comment about using C++ with GLUT. GLUT's use of callback functions does not work well with an OO approach, because you want to call objects (or their member functions) if an event happens (like resize, key press or display). As you can't send the address of a member function to a glut callback, it makes things a bit messy. Unfortunately, this lead a number of people to develop hybrid OO/procedural solutions which were ugly. If you are interested in how to do it better please come and see me during consultation.


Tips and Tricks

The easier it is for someone to understand the code you've written, the better your mark is likely to be.


This material is part of the CSE3313 Computer Graphics course.
Copyright © Jon McCormack, 2007.  All rights reserved.

Last Modified: September 6, 2007