5.1 - Introduction to Rendering

The Big Picture

You have created model data using Blender and you are ready to render your models in a scene using WebGL. Now the real fun begins!

Before we start, let’s discuss the nature of WebGL. WebGL is an API (application programmer interface) to a GPU (graphics processing unit). A GPU is a hardware device that is optimized for rendering real-time 3D graphics. You will be writing shader programs that are compiled into hardware and run at lightning speed. The WebGL API is not optimized for programmers; it is optimized for fast hardware rendering. WebGL commands literally “flip switches” in the hardware to connect circuitry. Don’t expect high-level commands. The WebGL API is a very low-level set of commands that control hardware level functionality.

As we have already discussed, there are pre-processing steps that happen only once to setup WebGL and your scene. Then there are steps to render models into a scene. These steps are repeated each time you re-draw the scene.

Pre-processing: WebGL Setup

At a high level of abstraction, the pre-processing steps are:

  1. Get the HTML canvas element you will be rendering into.
  2. Get a WebGL context for the canvas element, which is typically called gl.
  3. Set the desired state for the gl context.
  4. Compile and link your vertex shader and your fragment shader programs into a rendering program. (More than one rendering program can be created.)
  5. Get references to the variables in your rendering program so you can set their values at render time.
  6. For each model in your scene:
    1. Convert your OBJ model data into appropriate arrays for rendering.
    2. Create a buffer object in the GPU’s memory.
    3. Copy your model data into the buffer object.

We will capture the details of these steps in several JavaScript classes because:

  • A JavaScript class encapsulates functionality in a single place that can be easily executed once.
  • A JavaScript class can hide functionality details behind just a few function calls.
  • A JavaScript class allows functionality to be easily reused.

A Brief Introduction to Shader Programs

Before we discuss the specific steps for rendering models, we need to discuss some basic ideas behind shader programs. There are two stages in the graphics pipeline that you have to program yourself. You write programs in GLSL (GL Shader Language), compile them to machine instructions, link them into a “program”, download the program to the GPU, and then activate the program when you render a scene. The result of a rendering is a 2D array of pixels. Many pieces of data are stored and manipulated for each pixel. The group of data associated with a single pixel is called a fragment. When you see the word fragment, think “pixel and its associated rendering data.”

Please recognize that a triangle rendered to an image might require 100’s or even 1000’s of pixels to render because the viewer is very close to the triangle. If the viewer is far removed from a triangle, the triangle might be rendered with one or two pixels. The number of fragments that are created to render an individual triangle will constantly change based on the distance between a viewer and the triangle.

Your two shader programs have a very limited scope of functionality:

  • A vertex shader transforms each vertex of a model to its correct location for the current scene.
  • A fragment shader assigns a color to each fragment (pixel) that composed a point, line, or triangle.

That sounds simple enough, and it can be very simple. You can create shader programs that perform their required functionality in a single line of code. Or you can do amazing manipulations using very complex algorithms. The beauty of shader programs is that you have total control over the graphics process.

Note that vertex and fragment shaders share variables. A vertex shader will often setup values for the fragment shader to use. This will become clearer when we study shader programs in detail.

Shader programs use three types of data. Data that is the same for an entire model, data that changes for each vertex, and data that changes for every fragment on the interior of a point, line, or triangle. These three types of data are:

uniform A data value that is the same for an execution of the graphics pipeline. For example, if you are going to assign the same color to every processed vertex, then that color could be a uniform variable. When you think of a uniform value, think of a fixed, unvarying, unchanging value.
attribute A data value that changes for every vertex as the graphics pipeline is processing vertices. A typical attribute value is the (x,y,z) location of a vertex. When you think of a attribute value, always include vertex with the term, as in vertex attribute.
varying A data value that changes for every fragment as the graphics pipeline is executing. A varying variable allows every pixel that composes a point, line or triangle to be assigned a different color.

Rendering Steps

Each time you render your scene, your JavaScript program must perform the following steps:

  1. Clear the color buffer that holds your rendering to a background color.
  2. If you are doing hidden surface removal, clear the depth buffer.
  3. Select your shader program.
  4. For each model in your scene:
    1. Pass the values of your uniform variables to your shader program.
    2. Attach each attribute variable to an appropriate buffer object.
    3. Call the WebGL gl.drawArrays() function.

The following diagram will help you visualize these steps.

../_images/rendering_steps.png

Steps for rendering a model.

There is a lot going on in these rendering steps. The following lessons will cover all of the details.

Rendering Speed Considerations

All rendering is done within a context. The same can be said for anything that you do as a person. You might study in the library, or study while watching a football game. The context you are in affects your studying! And people context switch all the time, either consciously or unconsciously. If you are thinking deeply about something, it might take you a few seconds to recognize that someone is talking to you. All context switching takes time. If you are constantly context switching you will not be very productive.

All modern-day computers are constantly context switching between running processes. The GPU is no different. You are always rendering within a context. The fewer times that you switch contexts, the faster your rendering. So when you setup a rendering process, one of your main goals should be to minimize the amount of context switching. All of the following actions cause a GPU to context switch:

  • Selecting a shader program.
  • Setting the value of a uniform variable in a shader program.
  • Attaching an attribute variable to a buffer object.
  • Any action that changes the state of the gl JavaScript object.

Any communication between your JavaScript program and the GPU slows down rendering. To get maximum rendering speeds you need to minimize JavaScript function calls to the WebGL API. For example, you can have a separate buffer object for every model, or you can store several models in a single buffer object. The number of possible configurations is almost endless. You will have to constantly make trade-offs between the desire for fast rendering, the amount of memory you use for your graphics data, and the complexity of your code.

Let’s say it again, any communication between your JavaScript program and the GPU slows down rendering. In the ideal case you will copy all of your model data to the GPU only once. Then, when you render, the GPU already has most of the data it needs. In cases where model data must be manipulated by your JavaScript code before each rendering, the transfer of the data from RAM to the GPU’s memory will be a major time constraint. In such cases you will want to separate your model data into its various data types and only transfer the data that is changing. For example, if your model data included vertices, colors, and normal vectors, and only your color data is being manipulated by your JavaScript program, then you would put the vertex and normal vector data into a GPU buffer object and your color data into a separate buffer object. When you render the model, your JavaScript code will change the color values, copy them to a GPU buffer object, and then call gl.drawArrays(). Minimizing the amount of data that is copied to the GPU on each render will speed up rendering.

In general, you should first get your graphics to render correctly, and then optimize it for faster rendering. You will be surprised at how fast the GPU is and in many situations no optimization will be needed. For these tutorials, we will write JavaScript code and organize model data such that it emphasizes clarity – not rendering optimization.

Code and Data Dependencies

WebGL programs render models using three major components:

  • A shader program that manipulates vertex locations and assigns colors to pixels. Shader programs execute on the GPU.
  • Vertex object buffers that store model vertex attribute data on the GPU.
  • JavaScript code that sets up and initiates rendering. The JavaScript code is executed by a CPU.

These three components are intertwined to the extent that a simple change in one component will typically require a change in all the components. This is regrettable because it makes incremental code development very difficult. In any case, if you change any part of a shader program, a buffer object, or a JavaScript rendering function, make sure the other components are compatible with your changes.

Glossary

shader program
a computer program written in GLSL (GL Shader Language) that runs on the GPU. It preforms the programmable parts of the graphics pipeline.
vertex shader
a computer program written in GLSL that positions the geometry of models in a scene.
fragment shader
a computer program written in GLSL that assigns a color to the pixels that compose a point, line or triangle.
pixel
a single color value in a raster image.
fragment
a group of data values used to calculate the color for an individual pixel.
gl
the typical name of the JavaScript object that holds a WebGL context for a canvas. All WebGL functionality is accessed through this object.
uniform
a value that stays constant while rendering an array of vertices.
attribute
a value that changes for every vertex during an execution of the graphics pipeline.
varying
a value that changes for every pixel/fragment of a point, line, or triangle.
context switching
a change in the environment (or context) in which a process is executing. Excessive context switching greatly slows down execution speeds.
Next Section - 5.2 - A Primer on Shaders