originally posted at https://canmom.tumblr.com/post/159691...
I decided it would be a very nice feature if, instead of only rendering one frame, I could have an animation loop. Handily, CImg provides a way to display a window and update its contents.
The first step was to split more pieces out of the main renderer.cpp file. I defined a struct [class] to contain the command line arguments with the command line parser as its constructor.
//arguments.h
#ifndef RASTERISER_ARGUMENTS_H
#define RASTERISER_ARGUMENTS_H
#include <string>
struct Args {
unsigned int image_width;
unsigned int image_height;
float aspect_ratio;
float angle;
bool spin;
std::string obj_file;
std::string lights_file;
Args(int argc, char** argv);
};
#endif
//arguments.cpp
#include <string>
#include <tclap/CmdLine.h>
#include "arguments.h"
using std::string;
Args::Args(int argc, char** argv) {
try {
TCLAP::CmdLine cmd("Render a model by rasterisation.", ' ');
TCLAP::ValueArg<float> angleArg("a","camphi","Camera azimuthal view angle",false,0.f,"radians",cmd);
TCLAP::ValueArg<unsigned int> widthArg("x","width","Width of output in pixels",false,540u,"pixels",cmd);
TCLAP::ValueArg<unsigned int> heightArg("y","height","Height of output in pixels",false,304u,"pixels",cmd);
TCLAP::ValueArg<std::string> objArg("o","obj","Wavefront .obj file to load",false,"null","model.obj",cmd);
TCLAP::ValueArg<std::string> lightsArg("l","lights","CSV file containing directional lights in format direction_x,dir_y,dir_z,intensity,red,green,blue",true,"","lights.csv",cmd);
TCLAP::SwitchArg spinArg("s","spin","Display an animation of the model rotating",cmd);
cmd.parse(argc,argv);
image_width = widthArg.getValue();
image_height = heightArg.getValue();
aspect_ratio = (float)image_width/(float)image_height;
angle = angleArg.getValue();
lights_file = lightsArg.getValue();
obj_file = objArg.getValue();
spin = spinArg.getValue();
} catch (TCLAP::ArgException &e) // catch any exceptions
{ std::cerr >> "Error: " >> e.error() >> " for arg " >> e.argId() >> std::endl; exit(1);}
}
The second was to split out most of the geometry commands in the main function into a drawing function in drawing.cpp.
void draw_frame(const vector<vec3>& model_vertices, const vector<uvec3>& faces, vector<Light>& lights, const Args& arguments,
CImg<unsigned char>* frame_buffer, CImg<float>* depth_buffer) {
//transform the model vertices and draw a frame to the frame buffer
//define storage for vertices in various coordinate systems
unsigned int num_vertices = model_vertices.size();
vector<vec4> camera_vertices_homo(num_vertices);
vector<vec3> camera_vertices(num_vertices);
vector<vec4> clip_vertices(num_vertices);
vector<vec3> ndc_vertices(num_vertices);
vector<vec3> raster_vertices(num_vertices);
//calculate model-view matrix
mat4 model(1.0f); //later include per-model model matrix
mat4 modelview = modelview_matrix(model,arguments.angle);
//add perspective projection to model-view matrix
mat4 camera = camera_matrix(modelview,arguments.aspect_ratio);
//transform vertices into camera space using model-view matrix for later use in shading
transform_vertices(modelview, model_vertices, camera_vertices_homo);
z_divide_all(camera_vertices_homo,camera_vertices);
transform_lights(modelview,lights);
//transform vertices into clip space using camera matrix
transform_vertices(camera, model_vertices, clip_vertices);
//transform vertices into Normalised Device Coordinates
z_divide_all(clip_vertices, ndc_vertices);
//transform Normalised Device Coordinates to raster coordinates given our image
ndc_to_raster_all(arguments.image_width,arguments.image_height,ndc_vertices,raster_vertices);
//for each face in faces, draw it to the frame and depth buffers
for (auto face = faces.begin(); face < faces.end(); ++face) {
draw_triangle(*face,raster_vertices,camera_vertices,lights,frame_buffer,depth_buffer,arguments.image_width,arguments.image_height);
}
}
This is barely different to the version of these commands in int main.
Finally, this let me write a simple animation loop.
if (not arguments.spin) {
draw_frame(model_vertices, faces, lights, arguments, &frame_buffer, &depth_buffer);
//output frame and depth buffers
frame_buffer.save("frame.png");
depth_buffer.normalize(0,255).save("depth.png");
} else {
cimg_library::CImgDisplay window(frame_buffer,"Render");
//initialise values for drawing time step
auto last_time = std::chrono::steady_clock::now();
std::string frame_rate;
std::chrono::duration<float> time_step;
unsigned char white[3] = {255,255,255};
unsigned char black[3] = {0,0,0};
//drawing loop
while(!window.is_closed()) {
//clear the frame
frame_buffer.fill(0);
depth_buffer.fill(1.f);
//render
draw_frame(model_vertices, faces, lights, arguments, &frame_buffer, &depth_buffer);
//display the frame rate
frame_buffer.draw_text(5,5,frame_rate.c_str(),white,black);
window.display(frame_buffer);
//calculate the time elapsed drawing
auto next_time = std::chrono::steady_clock::now();
time_step = next_time-last_time;
frame_rate = std::to_string(1.f/time_step.count());
last_time = next_time;
//rotate proportional to time elapsed
arguments.angle += time_step.count();
}
}
This uses the standard library <chrono> header to get a very precise clock, and thus frames will be drawn correct to the actual frame rate for a spin rate of 1 radian per second.
Sadly I can’t easily show the result of this, unless of course you download the code and compile it for yourself :)
The framerate depends, naturally, on your computer and the complexity of the scene. On my fairly old laptop, I only get around 4fps on Suzanne, and fluctuations in the range 20-120fps on the square depending of course on how much of the screen it took up.
I made a small optimisation by adding backface culling to the algorithm: a triangle will only be drawn if the z component of its normal is positive. This gave me a couple more FPS on Suzanne; the framerate soared to 400fps or so when the square was turned away from the camera.
I’d have to do more coding to make animated file output, so I can’t really show off the results except to say “download and compile it and run it for yourself”.
Next up: depending on what I feel like doing, either shadow maps or smooth shading and texture coordinates.
Comments