Graphics Tech in Cesium - Renderer Architecture
Cesium is built on a custom WebGL engine - the Renderer - that we started developing in March 2011 when the WebGL 1.0 spec was released. I consider this a 4th generation renderer because it is based on my experience developing the OpenGlobe Renderer, which Kevin Ring and I used in our virtual globe book [Cozzi11]. That, in turn, was based on my experience developing the Renderer for AGI’s Insight3D and STK, and - to a lesser extent - a grad school project. So there are a lot of battle-tested lessons in Cesium’s Renderer.
Why a Renderer?
We could have just scattered WebGL calls throughout Cesium, but centralizing them in the Renderer has many benefits:
- Ease of use: The Renderer provides a higher level of abstraction than WebGL so it makes the rest of Cesium concise and less error-prone.
- Shader Pipeline: Cesium’s use cases require a runtime shader pipeline for generating shaders, and a GLSL library of uniforms and functions. This is part of the Renderer.
- Performance: Optimizations and best practices such as caching and minimizing WebGL calls are applied in one place and the rest of Cesium benefits.
- State: WebGL is a state machine and the Renderer manages the state in one place.
- Portability: The Renderer makes it easy (well, easier) to add WebGL extensions, upgrade to WebGL 2, and add workarounds for specific platforms.
Why Roll Our Own?
The main reason we rolled our own WebGL engine is that we have a lot of experience in doing so and it allows us to tune the engine for the best performance and flexibility for Cesium’s use cases. For example, Cesium’s runtime shader pipeline is far beyond what most graphics applications require.
We also rolled our own because I needed something easy-ish to code to learn JavaScript and because WebGL engines were still immature in 2011.
What’s in the Renderer?
In Cesium 1.9, the major components of the Renderer are:
Check out the source in the Source/Renderer directory. If you use these types in your Cesium apps, be warned that they are not part of Cesium’s public API so they may change from release to release.
The objects in the left column form the basis for Cesium’s DrawCommand, which atomically encapsulates a draw call. To render a frame, Cesium executes commands in the potentially visible set.
- VertexArray - a container with a set of vertex attributes and optional indices. Attributes and indices are stored in Buffers. Vertex array uses the OES_vertex_array_object extension when available to reduce the number of WebGL calls.
- RenderState - contains the WebGL fixed-function state, such as the depth, blending, and stencil states, needed to issue a draw call. This is a coarse-grained representation of WebGL’s fine-grained states that actually maps well to today’s GPUs and should be easy to update if a Vulkan-like API comes to the web.
- ShaderProgram - represents a compiled/linked program and its uniforms. Uniforms work directly with Cesium’s matrix, Cartesian, color, texture, and cube map types.
- Framebuffer - a container with texture/renderbuffer attachments that is the target of a draw call.
Shader Pipeline
Shaders are authored using individual .glsl files. The Cesium build removes GLSL comments and whitespace, and converts GLSL files to RequireJS modules that return a JavaScript string so the rest of Cesium can reference them without requiring a request per file. Source for GLSL shaders is in the Source/Shaders directory.
Since many shaders in Cesium are fully or partially handwritten, Cesium provides a large GLSL library of functions, uniforms, structs, and constants that can be used without declaring them or using a custom #include. These identifiers start with czm_. For example, here is a snippet from the sky atmosphere fragment shader:
czm_ellipsoid ellipsoid = czm_getWgs84EllipsoidEC();
vec3 direction = normalize(v_positionEC);
czm_ray ray = czm_ray(vec3(0.0), direction);
czm_raySegment intersection = czm_rayEllipsoidIntersectionInterval(ray, ellipsoid);
if (!czm_isEmpty(intersection)) {
discard;
}
GLSL built-ins can reference other built-ins forming a Directed Acyclic Graph (DAG). At runtime, GLSL source is provided to ShaderSource, which finds the czm_ identifiers and walks the DAG to generate the final source. If the shader will be used for picking, it is also patched to output a pick id, instead of the actual color.
This is done at runtime instead of as part of the Cesium build to avoid downloading several copies of the same GLSL built-ins and because a Cesium app may need many permutations of a shader that are not determined until runtime.
Built-in GLSL uniforms are called automatic uniforms. Like other GLSL built-ins, these do not need to be declared; they are added to the source when walking the DAG. Automatic uniforms generally represent frame-specific (or even frustum-specific or command-specific) values such as transform matrices. See AutomaticUniforms. For example, the sky box vertex shader uses automatic uniforms to transform a 2x2x2 cube centered at the origin in the True Equator Mean Equinox (TEME) frame to a cube enclosing the scene in clip coordinates:
attribute vec3 position;
varying vec3 v_texCoord;
void main()
{
vec3 p = czm_viewRotation * (czm_temeToPseudoFixed * (czm_entireFrustum.y * position));
gl_Position = czm_projection * vec4(p, 1.0);
v_texCoord = position.xyz;
}
Shader programs are cached using the original GLSL source as the key to reduce the number of WebGL calls made for initializing and using shaders. See ShaderCache.
Executing a Command
A command is executed with Context.draw, which
- Binds the framebuffer if it is different than the previous command.
- Applies the render states that are different than the previous command’s. Since render states are immutable and cached, an efficient lookup of the previous and current render state yields a function that sets just the state that changed.
- Bind the shader program (and compiles/links it if needed) and sets the uniforms that changed, including Cesium’s automatic uniforms.
- Binds the vertex array and issues drawElements or drawArrays.
At the end of each frame, Context.endFrame cleans up state by unbinding the shader program, framebuffer, drawbuffers, and textures. This helps reduce the amount of state the Renderer manages between each command execution.
The Renderer in the Cesium Stack
The Renderer is used by the Scene to create WebGL resources for Cesium primitives such as the globe and 3D models, and to execute commands referencing these resources to draw a frame.
Future Work
With WebGL 2 coming out, lots of improvements can be made to the Renderer. See #797. Highlights include:
Uniform Buffers
Like many engines, setting uniforms is often a bottleneck in Cesium. Uniform buffers in WebGL 2 will improve performance by backing uniforms by buffers, which will be organized by update frequency.
Instancing
Redesigning commands to support instancing will enabled Cesium, for example, to render a large number of trees, each with some different attributes like their position, height, etc.
Acknowledgments
Thanks to Greg Beatty and Scott Hunter who wrote the runtime generation of GLSL shaders.
References
[Cozzi11] Patrick Cozzi and Kevin Ring. 3D Engine Design for Virtual Globes. CRC Press. 2011.