Drawing
Drawing graphics in ClanLib is done by first acquiring an instance of the CL_GraphicContext interface. Graphical contexts can be provided by different sources, with CL_DisplayWindow and CL_Canvas being the common sources.
A simple example of obtaining a graphic-context:
CL_DisplayWindow window(0, 0, 640, 480, cl_text("Hello World"));
CL_GraphicContext gc = window.get_gc();
A graphic context is an interface which you can use to draw graphics and create graphical objects like textures, shader objects and so on. Each graphic context has a number of states that determine how each drawing command is to be executed. For example, the following code creates a system font, sets the font to the current font, then draws some text with the font:
CL_Font font_tahoma(gc, "Tahoma", 16); gc.set_font(font_tahoma); gc.draw_text(10, 10, "Hello There", CL_Colord::lemonchiffon);
If the graphic context comes from a CL_DisplayWindow object, it is normally written to an off-screen buffer in order to avoid the user seeing your changes as they are being made. In order to change the content being drawn onto the screen, you need to call CL_DisplayWindow::flip() or CL_DisplayWindow::update(rect). The following example shows how to make a ClanLib application drawing some graphics onto the screen:
CL_SetupCore setup_core;
CL_SetupDisplay setup_display;
CL_SetupGL setup_gl;
CL_DisplayWindow window(640, 480, cl_text("Hello World"));
CL_GraphicContext gc = window.get_gc();
CL_InputContext ic = window.get_ic();
CL_Font font_tahoma(gc, "Tahoma", 16);
CL_BlendMode blend_transparent;
blend_transparent.enable_blending(true);
while (ic.get_keyboard().get_keycode(CL_KEY_ESCAPE) == false)
{
gc.clear(CL_Colord::smokewhite);
gc.set_map_mode(cl_map_2d_upper_left);
gc.set_font(font_tahoma);
gc.set_blend_mode(blend_transparent);
gc.draw_text(10, 10, "Hello There", CL_Colord::lemonchiffon);
window.flip();
CL_DisplayMessageQueue::process();
}
Rendering Details
The graphic context draws primitives, with each primitive being a point, line segment, polygon or a rectangle of pixels. The primitives are defined by a group of one or more vertices. A vertex defines a point, an endpoint of an edge, or a corner of a polygon where two edges meet. Additional data is associated with each vertex, such as colors, normals and texture coordinates.
For example, a single line drawn is described as one line-segment primitive using two vertices. The first vertex may have position 10,10 using a red color, and the second vertex may have position 100,100 using a green color. The line drawn will then go from 10,10 to 100,100 and changing color from red to green along the line.
In order to draw this line, the vertices are processed and the line-segment primitive executed through a series of steps, usually referered to as the processing pipeline. Roughly put the pipeline consists of three main steps:
- Vertices are transformed and lit. Primitives are clipped.
- Primitives are rasterized. A fragment is produced for each pixel a primitive covers.
- The resulting fragments are blended with the frame buffer.
The details of the first two steps can be either described by writing a shader program, or by using the fixed function pipeline. A shader program is a small piece of code (handled by CL_ProgramObject) that is compiled and uploaded to the graphics card's processing unit (GPU), which then executes the details of the above steps.
Rendering Primitives
To draw one or more primitives, you have to first construct some vertex data, then describe your vertex data to ClanLib and finally tell the graphic context how many vertices you got and what type of primitive you want to render. The following example does this to draw a rectangle:
void draw_rectangle(CL_GraphicContext &gc, const CL_Rect &rect, const CL_Colord &color)
{
CL_Vec2i positions[] =
{
CL_Vec2i(rect.left, rect.top),
CL_Vec2i(rect.right, rect.top),
CL_Vec2i(rect.right, rect.bottom),
CL_Vec2i(rect.left, rect.bottom)
}
CL_PrimitivesArray vertex_data(gc);
vertex_data.set_positions(positions);
vertex_data.set_primary_color(color);
gc.draw_primitives(cl_polygon, 4, vertex_data);
}
As mentioned earlier, each vertex defines a point and can have additional data associated with it. In traditional (fixed function pipeline) rendering, the vertex data attributes are:
| Attribute | Shader Variable | Attribute Bind Index |
|---|---|---|
| Position | gl_Vertex | 0 |
| Normal | gl_Normal | 2 |
| Primary color | gl_Color | 3 |
| Secondary color | gl_SecondaryColor | 4 |
| Fog coordinate | gl_FogCoord | 5 |
| Texture coordinates | gl_MultiTexCoord0-7 | 8 to 15 |
You can either specify a vertex attribute by supplying an array of values, or you can specify one single value to be applied to all vertices. In the above example, set_positions specifies an array of CL_Vec2i values, while set_primary_color specifies a single value to be used for all vertices.
Vertex attributes, however, are not limited to only the above listed types. If you write your own shader programs, the shaders can define new attributes which you supply using CL_PrimitivesArray::set_attributes. The officially defined attributes above basically just cover the types of data which the fixed function pipeline uses - you do not have to use any of them if your vertex shader does not.
The following example only uses our own special vertex attribute:
// vertex.glsl:
attribute vec1 angle;
void main()
{
gl_Position = vec2(200+cos(radians(angle))*100.0, 200+sin(radians(angle)*100.0);
gl_TexCoord0 = gl_Position.xy;
}
// fragment.glsl:
uniform shader2D texture;
void main()
{
gl_FragColor = texture2D(texture, gl_TexCoord0.st);
}
// ClanLib code:
void draw_circle(CL_GraphicContext &gc, CL_Texture &texture)
{
CL_ProgramObject program = CL_ProgramObject::load(
gc, cl_text("vertex.glsl"), cl_text("fragment.glsl"));
int angle_index = 0;
program.bind_attribute_location(angle_index, cl_text("angle"));
program.link();
int texture_index = 0;
program.set_uniform(cl_text("texture"), texture_index);
CL_Vec2i angles[360];
for (int i = 0; i < 360; i++)
angles[i].x = i;
CL_PrimitivesArray vertex_data(gc);
vertex_data.set_attributes(angle_index, angles);
gc.set_program_object(program);
gc.set_texture(texture_index, texture);
gc.draw_primitives(cl_polygon, 360, vertex_data);
gc.reset_texture(texture_index);
gc.reset_program_object();
}
If you mix your own attributes with built-in attributes, make sure that you do not bind the attributes to indexes already used by the built-in attributes you use. For example, you cannot bind angle in the above example to index 0 if you also use gl_Vertex, since gl_Vertex always uses index 0.
Buffer Objects
To do: introduce CL_VertexArrayBuffer and other methods to store data in GPU memory.
The Fixed Function Pipeline
The fixed function pipeline can be considered a pre-written shader program that is configured by a number of states you can set on CL_GraphicContext using functions such as set_modelview, set_texture_unit, set_light, etc.
Vertex Shading
The vertex transformation sequence of the fixed function pipeline is as follows:
- Object coordinates -> Model-view matrix -> Eye coordinates
- Eye coordinates -> Projection matrix -> Clip coordinates
- Clip coordinates -> Perspective division -> Normalized (-1.0 to 1.0) coordinates
- Normalized coordinates -> Viewport transformation -> Window coordinates
The vertex coordinates passed along with the line-segment primitive command are first multiplied with the model-view matrix. This produces what is called eye coordinates. This set of coordinates are then multiplied with the projection matrix, which creates clip coordinates. A perspective division is performed and finally the coordinates are scaled up to fit the size of the window, i.e. (0.0,0.0) in normalized coordinates maps to the center of the window.
The above transformation sequence is mostly useful for drawing 3D and can be somewhat complicated to work with when doing 2D drawing operations. ClanLib therefore offers a function on the graphic context, CL_GraphicContext::set_map_mode, which preconfigures certain parts of the sequence. If the mapping mode is set to cl_map_2d_upper_left then the projection matrix and viewport transformation will be configured to let eye coordinates map to window coordinates. So eye coordinates of (0,0) will map to the upper left corner, and (100,100) will map to 100 pixels down and 100 pixels to the right. Likewise, cl_map_2d_lower_left will do the same but with (0,0) being lower left corner and the y-axis going upwards.
Each vertex have two colors, the primary and secondary color. If lighting is enabled in the pipeline, the primary and secondary color is calculated by applying all active CL_LightSource objects. The colors are set by using the currently active CL_Material object, the position of the light sources and the position of the vertex itself. For further detail on how light is applied, see the OpenGL specification.
Fragment Shading
After the vertices have been processed, the primitive being drawn (a line in our example) is realized by generating fragments for each pixel the line covers. For example, a fragment for (10,10), one for (11,11), another for (12,12), etc.
Each of these fragments go through the following steps which determine what color the fragment gets:
- Texturing
- Color Sum
- Fog
Texturing maps a portion of one or more specified images onto each primitive for which texturing is enabled. The mapping is accomplished by using the color of an image at the location indicated by texture coordinates. Each vertex carries multiple sets of texture coordinates, one for each active texture unit. If we take the line segment example again, the first vertex at (10,10) may have a texture coordinate for texture unit 1 saying (0.0,0.0) and at second vertex at (100,100) it might say (1.0,1.0). The fragment located at (50,50) will then have the texture coordinate of (0.5,0.5). Likewise it might have another set for texture unit 2, going from (1.0,1.0) to (0.0,0.0)
The color in the image located at the texture coordinate position is called a texel. The texel and the primary color of the fragment is blended by the texturing unit based on the currently active texture function. When using several texture units, the result of the first texturing unit is passed into the next texture unit. A texture unit's texture function can be described as a function with the following syntax:
color = texture_function( fragment_primary_color, texel_color, result_color_from_previous_unit);
The result returned by the last of the active texture units will be the new primary color of the fragment.
After the texturing step, a color sum between the fragment primary color and secondary color is applied. The resulting color of the fragment will then be normalize(primary_color+secondary_color). Finally fog coloring is applied to the fragment color, if enabled.
Blending Fragments with the Frame Buffer
The last step of rendering a primitive is to apply the fragments to the frame buffer. This is done by running a few tests on the fragment and then a blending operation with the pixel in the frame buffer. The steps are as follows:
- Scissor (Clipping) Test
- Alpha Test
- Stencil Test
- Depth Buffer Test
- Blending
- Dithering
- Logical Operation
The scissor test checks if the fragment is inside the clipping rectangle of the graphic context. The alpha test, if enabled, checks if the alpha part of the fragment color is above, equal or below a specified value. The stencil test discards a fragment based on the outcome of a comparison between the value in the stencil buffer and the fragment. Likewise, the depth buffer test compares the fragment to the value in the depth buffer. All these tests are configured by the CL_BufferControl states in the graphic context. If the fragment fails any of these tests, the fragment is dropped.
If blending is enabled (CL_BlendMode), the color of the fragment will be changed to a value calculated by the blending function. The blending functions use the color of the fragment and the current color of the pixel in the frame buffer beneath the fragment. If blending is not enabled, the fragment color will always become the new color in the frame buffer, no matter what value the alpha part of the color is.
Finally, a logical operation is applied between the incoming fragment's color and the color in the frame buffer. The result of this operation becomes the new color in the frame buffer.
