Gravitator programmer's guide
Tools used
Gravitator is developped in C. It uses the GTK library for it's GUI, OpenGL for 3D rendering to the gtkglarea widget and glib for some
data structures. It was developped in KDevelop IDE, the GUI was made by Glade.
Basic Ideas
Gravitator is intended to simulate large quantities of objects. This is made possible by the fact that the objects are of 2 types:
heavy and light. Heavy objects affect other objects with their gravity force, light object don't. This means that for every object
it has to calculate the gravitational force only to every heavy object. The typical simulation would simulate mostly tens of heavy
objects and thousands of light objects.
Architecture
The program can be separated into 2 parts: the GUI and the backend. The GUI uses the backend to operate the world, work with files etc.
Here is a picture of the design:
The source files for the GUI are:
- interface.c - this file is generated by Glade, it is responsible for creating the widgets, the look of the GUI
- callbacks.c - in this file there are the routines which are executed after some event occured in the GUI, e.g. user clicked
on a button, these routines are called callbacks. The callbacks are generated by Glade, I have written what do they do.
- gui.c - in this file are other utility functions used by the callbacks, and callbacks for the widgets which weren't created
by Glade but by me (it's mostly the gtkglarea widgets).
- gl.c - here are the functions which use OpenGL to render the world
The source files for the backend are:
- world.c - here are the functions for manipulating the world, for work with data structures, for generating objects etc.
- sim.c - here are the functions for calculating the gravity and drawing the simulation windows
- init.c - functions in this file handle inicialization of the application, loading configuration, adding gtkglareas to the
application etc.
- file - functions which work with files are here, e.g. loading a world, saving a world etc.
Other source files are:
- main.c - this is where the program starts
- misc.c - miscelaneous functions, now there is only a function handling exit of the application
- support.c - file generated by Glade, the only used function from there is lookup_widget
Every function from a source file which is used somewhere else has it's prototype in a header file named the same as the file in which
it's declared. The global data structures are defined in main.h
Data structures
There are 2 global pointers to global data : world and gui. world holds information about the objects in the world, gui holds the
information about GUI, e.g. in which state it is, which objects are selected etc.. Detailed description of the data strucures is in
main.c
gui
gui pointer has 5 subpointers, in new_obj there is information about the created object, in selection there is information about
selected objects and metaobjects, in mode there is information about what is going on (e.g. user is dragging a selection), in view
there are informations about position and rotation in the world in the views on the world and in misc there are other miscelaneous
information which don't fall into any previous category.
world
The world pointer holds 5 subpointers: to the list of metaobjects containing light objects, to the list of metaobjects containing heavy
objects, to the list of metaobjects containing light objects in the simulation, to the list of metaobjects containing heavy
objects in the simulation and to a temporary list containing heavy objects in simulation.
The lists of metaobjects are pointers to the typeT_ METAOBJECT_LIST. This is a single linked list, so every member of the list contains
information about itself, pointer to it's objects and pointer to next metaobject. The information about objects is also stored in a
single linked list which nodes are of OBJECT_LIST type. They contain information about themselves and pointers to next object.
General program working
The program of course begins in the main function. First, it inicializes itself by calling init, which allocates global
data structures, sets their default values, loads config and adds gtkglareas to the application. Then the main loop of the GTK library
is started. All of the actions performed by user are handled by callbacks in callbacks.c and gui.c (here are callbacks for gtkglareas).
The gtkglareas are drawn by the editor_draw function, which is set to be permanently called by gtk_idle_add function. The
simulation gtkglarea is redrawn by the sim_draw function and the script gtkglarea is redrawn by the script_draw function.
Most important functions
- file.c :
- load_config: - loads the configuration file and sets the global variables according to it
- save_world: - saves the world to the given file
- load_world: - loads the world from the given file
- gl.c:
- glarea_draw: -draws everything that should be drawn in the given gtkglarea
- gui.c:
- clean_gui: - allocates the gui pointer and sets its contents to defaults
- editor_draw: - draw the 4 gtkglareas in the editor
- add_* : - add new object, galaxy, etc., get it's properties and call generate_*
- modify_sim_view: - modify the position and rotation according to the view mode in the simulation/script window
- show_error_dialog: - show a dialog with the specified error message
- init.c:
- init: - initialize the application
- sim.c:
- apply_gravity: - apply gravity on the objects in the simulation part of the world
- sim_draw: - calls apply_gravity and then draws the simulation gtkglarea
- script_draw: - draw a script file
- world.c:
- clean_world: - clean the world pointer, and reallocates it (empty)
- world_nav: - change position/rotation in the world after user pressed a key
- generate_*: - generate a metaobject with given properties
Algorithms
Gravity
The basic idea behind calculating the gravity is that there are 2 kinds of objects, light and heavy. Only heavy objects apply gravity
on other objects. So the algorithm basically works like this: for every light object calculate how is it attracted by every heavy
object, then copy the heavy objects to a temporary position (so you don't calculate how are they attracted by an already moved
heavy objects), calculate how are they attracted by heavy objects (and store the position change to the temporary position), and
finally copy the position change from the temporary position to the original one. Part of the code looks like this (this part applies
gravity only on the light objects):
// Aply gravity on every light object in the world
light_meta_list = world->sim_light_obj_list;
while(light_meta_list != NULL){
light_obj = light_meta_list->objects;
while(light_obj != NULL){ // NULL means the end of the object list
//The acceleration is 0 at the begining
x_accel = 0;
y_accel = 0;
z_accel = 0;
// Aply gravity from every heavy object to the current object
heavy_meta_list = world->sim_heavy_obj_list;
while(heavy_meta_list != NULL){
heavy_obj = heavy_meta_list->objects;
while (heavy_obj != NULL){ // NULL means the end of the object list
// Apply gravity from every object of the heavy metaobject
// Calculate the direction vector from the light object to the heavy object
x_vect = heavy_obj->x - light_obj->x;
y_vect = heavy_obj->y - light_obj->y;
z_vect = heavy_obj->z - light_obj->z;
// Calculate the distance between them
dist = (double) sqrt((double) (x_vect*x_vect + y_vect*y_vect + z_vect*z_vect));
if (dist < 0.03) dist = 0.03; // Watch out if they get too close
// Normalize the vector
x_vect = x_vect / dist;
y_vect = y_vect / dist;
z_vect = z_vect / dist;
// Calculate the gravity force
force = ((light_obj->mass + heavy_obj->mass) / (dist*dist)) / factor;
// Aply the gravity on the vector
x_vect = x_vect * force / light_obj->mass;
y_vect = y_vect * force / light_obj->mass;
z_vect = z_vect * force / light_obj->mass;
// Add the gravity vector to the acceleration of the light object
x_accel += x_vect;
y_accel += y_vect;
z_accel += z_vect;
heavy_obj = heavy_obj->next; // Go to the next heavy object
}
heavy_meta_list = heavy_meta_list->next; // Go the the next heavy metaobject
}
//All heavy objects applied their gravity on the light one, change it's position
// Modify the speed of the light object
light_obj->x_vel += x_accel;
light_obj->y_vel += y_accel;
light_obj->z_vel += z_accel;
// Modify the position of the light object
light_obj->x +=light_obj->x_vel / factor;
light_obj->y +=light_obj->y_vel / factor;
light_obj->z +=light_obj->z_vel / factor;
// Modify the position of the metaobject in which is the light object
light_meta_list->x = light_obj->x;
light_meta_list->y = light_obj->y;
light_meta_list->z = light_obj->z;
light_obj = light_obj->next; // Go on to the next object
}
light_meta_list = light_meta_list->next; // Go on to the next metaobject with light objects
}
Calculating stable orbit
Calculating a stable orbit is essential for Gravitator. Without it it couldn't generate galaxies. The algorithm is fairly simple. At
first, it must calculate the vector of the orbit axis from the orbit axis angle (the orbit axis is stored using the angles which are
between it and world axis). Then if objects are selected it calculates the stable orbit for every one of the selected objects. If a
metaobject is selected, 2 cases must be considered. If the objects in the metaobject are bound with gravity to one object in the
metaobject (for example to the centre of galaxy), it calculates the stable orbit velocity only for the "cetral" object and changes
the other objects' velocity according to this. If the objects in the galaxy are free (not gravity bound) then it calculates the stable
orbit independently for every one of them. Here is the part of the code calculating the stable orbit:
// Calculate the vector from the object to the orbited object
obj_vector_x = obj->x - gui->misc->orbited_object->x;
obj_vector_y = obj->y - gui->misc->orbited_object->y;
obj_vector_z = obj->z - gui->misc->orbited_object->z;
// Calculate the distance to the orbited object
l = sqrt(obj_vector_x * obj_vector_x
+ obj_vector_y * obj_vector_y
+ obj_vector_z * obj_vector_z);
r = l; // Remember the distance
// Normalize the vector from the object to the orbited object
obj_vector_x = obj_vector_x / l;
obj_vector_y = obj_vector_y / l;
obj_vector_z = obj_vector_z / l;
// The direction vector must be perpendicular to the axis and to the vector from the object to the orbited object, so calculate it as
// a cross product
dir_vector_x = obj_vector_y * axis_vector_z - axis_vector_y * obj_vector_z;
dir_vector_y = obj_vector_z * axis_vector_x - axis_vector_z * obj_vector_x;
dir_vector_z = obj_vector_x * axis_vector_y - axis_vector_x * obj_vector_y;
// Normalize the direction vector
l = sqrt(dir_vector_x * dir_vector_x
+ dir_vector_y * dir_vector_y
+ dir_vector_z * dir_vector_z);
dir_vector_x = dir_vector_x / l;
dir_vector_y = dir_vector_y / l;
dir_vector_z = dir_vector_z / l;
// Calculate the speed for a stable orbit
vel = sqrt((gui->misc->orbited_object->mass + obj->mass)/ (obj->mass * r));
// Change the direction vector to be as long as the speed
dir_vector_x = dir_vector_x * vel;
dir_vector_y = dir_vector_y * vel;
dir_vector_z = dir_vector_z * vel;
// Add the direction vector to object's velocity
dir_vector_x = dir_vector_x + gui->misc->orbited_object->x_vel;
dir_vector_y = dir_vector_y + gui->misc->orbited_object->y_vel;
dir_vector_z = dir_vector_z + gui->misc->orbited_object->z_vel;
// Set the object's velocity
obj->x_vel = dir_vector_x;
obj->y_vel = dir_vector_y;
obj->z_vel = dir_vector_z;
Generating objects
Generating metaobjects in some form is done by simply randomly placing some objects and checking if they're inside the requested form
(e.g. cylinder). More complex metaobjects as galaxies are created using the simpler ones. A disk galaxy is composed of 2 ellipsoids,
one creates the centre of the galaxy and the second one creates the disk. An ellipsoid galaxy is just an ellipsoid. A spiral galaxy's
center is created from an ellipsoid, and it's spirals are created from cylinders starting from the center with their diameter
decreasing to the end. Every such a cylinder is called a segment. After generating the galaxy's shape, stable orbits are
calculated for the galaxy's objects relatively to the galaxy center.