Today I'm going to show you another way to draw coloured triangles in Warp3D Nova. The result will be more boring than the previous tutorial's method. However, it's a good way to introduce using multiple vertex attributes and you'll need that for almost everything from here on out.

Per Vertex Colour

Vertex attributes are essentially "attributes that describe each vertex." The vertex's position is a vertex attribute, but vertices can have other properties too. For example, in this tutorial each vertex will also have its own colour. We'll make one vertex red, another green, and the third blue. The GPU will render smooth gradients between one vertex and the next (see the screenshot at the end of this tutorial).

Other typical attributes are texture coordinates and surface normals. However, let's not get ahead of ourselves. For now we'll stick to just position and colour.

Step 1: The New Shaders

We'll use the hello triangle code as a starting point, so download that now. Here's a direct link: W3DNTutorial2.lha.

The Vertex Shader

Open the vertex shader (Colour2D.vert), and replace its code with the following:

#version 310 es
/** Simple 2D per-vertex colour shader.
 */

in layout(location = 0) vec2 vertPos;
in layout(location = 1) vec4 vertCol;

out vec4 colour;

void main() {
    colour = vertCol;
    gl_Position = vec4(vertPos, 0.0, 1.0);
}

This shader adds an extra input variable which is the vertex colour:

in layout(location = 1) vec4 vertCol;

You've probably noticed that both inputs have layout(location = n) qualifiers. This is a useful newish GLSL feature that allows us to fix the locations of the input variables. Warp3D Nova will sort the inputs in ascending location order.

Using layout(location = n) is optional. However, if you don't use it, then the shader compiler can put the variables in any order, and you'll need ShaderGetOffset() to find out which attribute indiex belongs to which shader variables. Life's simpler when the locations are fixed.

Layout(location = n) is only available in relatively new versions of GLSL. So, a #version is required at the top of the file, or the compiler won't accept it:

#version 310 es

The Fragment Shader

No changes are needed for the fragment shader. It already outputs the interpolated colour that it receives. Here's the code:

#version 140
/** Simple 2D per-fragment colour shader.
 */

in vec4 colour;

void main() {
    gl_FragColor = colour;
}

Step 2: The Vertex Attributes

With the shaders done, the next step is to set up the vertex attribute arrays. Our vertices now have two attributes which we'll put in a structure:

/** Encapsulates the data for a single vertex
 */
typedef struct Vertex_s {
	float position[2];
	float colour[4];
} Vertex;

/** The index for the position data in the VBO (must match the Vertex structure).
 */
const uint32 posArrayIdx = 0;

/** The index for the colour data in the VBO (must match the Vertex structure).
 */
const uint32 colArrayIdx = 1;

The Vertex structure makes writing the vertices easier, while the *Idx variables will be used to tell Warp3D Nova where to find the position and colour arrays.

Now, go down to the main() function, and find the CreateVertexBufferObjectTags() call. We must allocate a Vertex Buffer Object (VBO) with two arrays to store both vertPos and vertCol:

// Create the Vertex Buffer Object (VBO) containing the triangle 
uint32 numVerts = 3;
uint32 numArrays = 2; // Have a position and colour array
vbo = context->CreateVertexBufferObjectTags(&errCode, 
			numVerts * sizeof(Vertex), W3DN_STATIC_DRAW, numArrays, TAG_DONE);
FAIL_ON_ERROR(errCode, "CreateVertexBufferObjectTags");

Next, the VBO's layout needs to be set. This lets Warp3D Nova know where to find the data for each vertex attribute. We're using an "interleaved" format where each vertex's data is stored together:

// Set the VBO's layout
uint32 stride = sizeof(Vertex);
uint32 posNumElements = 2;
uint32 colourNumElements = 4;
uint32 colourArrayOffset = offsetof(Vertex, colour[0]);
errCode = context->VBOSetArray(vbo, posArrayIdx, W3DNEF_FLOAT, FALSE, posNumElements, 
	stride, 0, numVerts);
FAIL_ON_ERROR(errCode, "VBOSetArray");
errCode = context->VBOSetArray(vbo, colArrayIdx, W3DNEF_FLOAT, FALSE, colourNumElements, 
	stride, colourArrayOffset, numVerts);
FAIL_ON_ERROR(errCode, "VBOSetArray");

Pay close attention to the colour array's offset. The offset is the bytes from the VBO's base to the first vertex's colour. Fortunately, C has the offsetof() macro which makes calculating this offset easy.

Next, the actual vertex data is written to the VBO. Adding the colour data results in the following code:

// Lock the VBO for access
// NOTE: We're replacing all data in the VBO, so the read range is 0
W3DN_BufferLock *vboLock = context->VBOLock(&errCode, vbo, 0, 0);
FAIL_ON_ERROR(errCode, "VBOLock");

// Vertex 0
// -- position
Vertex *vertices = (Vertex*)vboLock->buffer;
vertices[0].position[0] = 100.0f;
vertices[0].position[1] = 50.0f;
// -- Colour
vertices[0].colour[0] = 1.0f;
vertices[0].colour[1] = 0.0f;
vertices[0].colour[2] = 0.0f;
vertices[0].colour[3] = 1.0f;

// Vertex 1
// -- position
vertices[1].position[0] = 320.0f;
vertices[1].position[1] = 420.0f;
// -- Colour
vertices[1].colour[0] = 0.0f;
vertices[1].colour[1] = 1.0f;
vertices[1].colour[2] = 0.0f;
vertices[1].colour[3] = 1.0f;

// Vertex 2
// -- position
vertices[2].position[0] = 540.0f;
vertices[2].position[1] = 50.0f;
// -- Colour
vertices[2].colour[0] = 0.0f;
vertices[2].colour[1] = 0.0f;
vertices[2].colour[2] = 1.0f;
vertices[2].colour[3] = 1.0f;

// Unlock the VBO
// NOTE: We've updated the entire VBO, so the parameters reflect that
errCode = context->BufferUnlock(vboLock, 0, vboLock->size);
FAIL_ON_ERROR(errCode, "BufferUnlock");

The final change is to bind the two vertex attribute arrays to their respective shader input variables. Since the variable locations were explicitly set in the shader, this is relatively easy:

uint32 posAttribIdx = 0;
uint32 colAttribIdx = 1;

// Bind the VBO to the default Render State Object (RSO)
errCode = context->BindVertexAttribArray(NULL,
	posAttribIdx, vbo, posArrayIdx);
FAIL_ON_ERROR(errCode, "BindVertexAttribArray");
errCode = context->BindVertexAttribArray(NULL, 
	colAttribIdx, vbo, colArrayIdx);
FAIL_ON_ERROR(errCode, "BindVertexAttribArray");

The code above could be shortened even further because the vertex array and attribute indices are identical (e.g., posArrayIdx == posAttribIdx). I've created separate variables for clarity.


IMPORTANT: Warp3D Nova version 1.20+ is required to use layout(location = n). The first Warp3D Nova pre-release didn't include this feature. If you're using the pre-release then delete layout(location = n) from the vertex shader, and set posAttribIdx and posAttribIdx as follows:

uint32 posAttribIdx = context->ShaderGetOffset(W3DNSOT_INPUT, "vertPos");
uint32 colAttribIdx = context->ShaderGetOffset(W3DNSOT_INPUT, "vertCol");

This is the old way of mapping shader variables to attributes and exists for compatibility with old shaders. It's highly recommended that you fix variable locations using layout(location = n).


With these code changes done, the program now renders a coloured triangle (see below).

W3DNTutorial4 screenshotConclusion

This tutorial has shown how to use multiple vertex attributes to add colour. Each vertex has its own colour and the GPU creates a smooth gradient between them via interpolation.

Yes, it looks rather boring, but make sure you understand how to use multiple vertex attributes; you're going to need it a lot in future. Next time we'll add surface normals (as vertex attributes) and finally render something in 3D!

Download the full tutorial code: W3DNovaTutorial4.lha