Skip to content

Basics of GLSL in Possumwood

Martin Prazak edited this page Sep 23, 2018 · 1 revision

This tutorial introduces basic concepts of OpenGL drawing in Possumwood.

The OpenGL rendering implementation closely mirrors the OpenGL 4 API concepts of a VBO, vertex shader, fragment shader and a program object, while translating them to a node graph. The basic nodes are draw node executing the rendering, vertex shader and fragment shader nodes implementing shader compilation, the program node performing linking and finally a vertex data source, in our case a source converting a polymesh instance.

Input mesh

We will use the polymesh plugin to load an .obj mesh file. Just right-click in the node editor and create a new polymesh loader.

alt text

After creation, you can see on the right hand side a number of properties of the newly created node.

alt text

By clicking the browse icon next to the filename, you can go and select a mesh to load. In this tutorial, we will use the fsu_models/alfa147.obj from examples directory.

After loading mesh, you will notice that the generic polymesh now contains 53k vertices, 97k polygons, and by clicking on the "..." details icon, you can see we've loaded P vertex attribute, N and uv varying attributes and objectId polygon attribute.

alt text

Basic structure

At the core of the OpenGL drawing in Possumwood is the render/draw node. This node has 3 parameters - program, vertex data and uniforms.

alt text

The uniforms parameter has a default value that allows the OpenGL shaders to access viewport data. To begin with, we don't need anything else - we will not alter this parameter or connect anything else to this input.

The vertex data parameter needs to be connected to a vertex data source - a specific node that converts its inputs into a set of VBOs. In our case, we need to convert a generic polymesh object using the polymesh/vertex_data node.

alt text

Because the program parameter has a default value as well, just by connecting the vertex data attribute, you can see the a car in the viewport.

alt text

In the properties of the vertex data node, you can see that all properties of the original polymesh instance have been converted to vertex data attributes. The default implementation of the shaders does not make use of most of these - it will only tap into the P parameter to get the positional information of each vertex.

OpenGL Program

The default program already provides a good starting point for displaying an object in the viewport. It provides a simple flat shader, with normals derived from polygon differentials (i.e., "flat" normals) and a very simple shading model.

To allow for editing the GLSL code, we need to replace the default program with a network of editable nodes. Currently, an OpenGL program in Possumwood consists of a program node, with a connection to a fragment_shader and a vertex_shader.

alt text

Selecting any of the shader nodes with the editor panel open allows to change the source code of each shader. The compilation is run immediately after each press of the "apply" button.

alt text

Vertex shader

The default vertex shader simply passes through the position attribute to the fragment shader, after applying projection and modelview transformations. For the vertexPosition value, we are interested in vertices in world space - inly modelview matrix is applied.

#version 430

in vec3 P;                     // position attr from the vbo

uniform mat4 iProjection;      // projection matrix
uniform mat4 iModelView;       // modelview matrix

out vec3 vertexPosition;       // vertex position for the fragment shader

void main() {
	vec4 pos4 = vec4(P.x, P.y, P.z, 1);

	vertexPosition = (iModelView * pos4).xyz;
   	gl_Position = iProjection * iModelView * pos4;
}

The P attribute comes from the vertex data buffer, which in turn is passed from the vertex_data node - all data passed from the polymesh/loader node can be accessed in this way. The uniforms are part of the set of attributes accessible in the draw node by default. To see all vertex data loaded from the mesh and accessible uniform attributes, just select the draw node.

alt text

The model we are using as our example also includes the N attribute, representing the surface normals. Our current setup synthesizes flat per-polygon normals - let's use the N attribute to show normals read from the file.

To do that, we need to send the explicit vertex normal N to the fragment shader, transformed using the normal modelview matrix, which actually simplifies the shader code:

#version 430

in vec3 P;                     // position attr from the vbo
in vec3 N;                     // normal attr from the vbo

uniform mat4 iProjection;      // projection matrix
uniform mat4 iModelView;       // modelview matrix
uniform mat4 iModelViewNormal; // normal modelview matrix

out vec3 vertexPosition;       // vertex position for the fragment shader
out vec3 vertexNormal;         // vertex normal for the fragment shader

void main() {
	vec4 pos4 = vec4(P.x, P.y, P.z, 1);
	vec4 norm4 = vec4(N.x, N.y, N.z, 0);

	vertexPosition = (iModelView * pos4).xyz;
	vertexNormal = (iModelViewNormal * norm4).xyz;

   	gl_Position = iProjection * iModelView * pos4;
}

Fragment shader

The default fragment shader relies only on the vertexPosition attribute to be passed from the vertex shader. It then uses dFdx() and dFdy() to compute X and Y differentials per-pixel, and synthesize a normal vector using cross product.

The synthesized normal vector is then used to create a simple shading by converting its Z axis to a color. This leads to a simple grey material - the surfaces parallel to the camera plane will be white, with a gradient as the surface normal edges away from the camera view direction.

#version 430

out vec4 color;

in vec3 vertexPosition;

void main() {
	vec3 dx = dFdx(vertexPosition);
	vec3 dy = dFdy(vertexPosition);

	vec3 norm = normalize(cross(dx, dy));

	color = vec4(norm.z, norm.z, norm.z, 1);
}

To use the N attribute directly, we only need to use the vertexNormal input variable (passed from the vertex shader) instead of the computation described above.

#version 430

out vec4 color;

in vec3 vertexPosition;
in vec3 vertexNormal;

void main() {
	vec3 norm = normalize(vertexNormal);

	color = vec4(norm.z, norm.z, norm.z, 1);
}

alt text

Transformations

The control over the vertex shader also allows for transforming the geometry for display. For example, we can easily scale the car model to 0.1 of its original size, and rotate it so it is displayed correctly in the viewport by switching the Y and Z axis.

#version 430

in vec3 P;                     // position attr from the vbo
in vec3 N;                     // normal attr from the vbo

uniform mat4 iProjection;      // projection matrix
uniform mat4 iModelView;       // modelview matrix
uniform mat4 iModelViewNormal; // normal modelview matrix

out vec3 vertexPosition;       // vertex position for the fragment shader
out vec3 vertexNormal;         // vertex position for the fragment shader

void main() {
	vec4 pos4 = vec4(P.x * 0.1, P.z * 0.1, -P.y * 0.1, 1);
	vec4 norm4 = vec4(N.x, N.z, -N.y, 0);

	vertexPosition = (iModelView * pos4).xyz;
	vertexNormal = (iModelViewNormal * norm4).xyz;

   	gl_Position = iProjection * iModelView * pos4;
}

alt text

Turntable

We can also use the vertex shader transformations to create a simple turntable. For this, we need to add an additional uniform to the uniforms input of the draw node.

In Possumwood, uniform nodes can be chained together to provide a composite of all uniforms used by a drawing node. We will need to get the output of the time node (a special node with a single output returning the time value from the timeline as a float parameter), feed it to a render/uniforms/float node instance, and chain the result with render/uniforms/viewport to provide access to the viewport matrices (previously included in the default uniforms value).

alt text

Using the new time uniform, we can generate a new transformation matrix in the vertex shader, and use it to transform the normal and vertex position before rendering.

#version 430

in vec3 P;                     // position attr from the vbo
in vec3 N;                     // normal attr from the vbo

uniform mat4 iProjection;      // projection matrix
uniform mat4 iModelView;       // modelview matrix
uniform mat4 iModelViewNormal; // normal modelview matrix
uniform float time;            // time uniform input

out vec3 vertexPosition;       // vertex position for the fragment shader
out vec3 vertexNormal;         // vertex position for the fragment shader

void main() {
	/// 360-degree rotation in 5 seconds of time
	float t = time / 2.5 * 3.1415;

	// rotation transformation along the Y axis
	mat4 tr = mat4(
		sin(t), 0, cos(t), 0,
		0, 1, 0, 0,
		-cos(t), 0, sin(t), 0,
		0, 0, 0, 1
	);

	vec4 pos4 = vec4(P.x, P.z, -P.y, 1);
	vec4 norm4 = vec4(N.x, N.z, -N.y, 0);

	vertexPosition = (iModelView * tr * pos4).xyz;
	vertexNormal = (iModelViewNormal * tr * norm4).xyz;

   	gl_Position = iProjection * iModelView * tr * pos4;
}

This leads us to the final output - a simple turntable of a car, rendered using OpenGL and shaded using OpenGL4 shaders.

alt text