| CPSC 424 | Computer Graphics | Fall 2025 | 
A scene graph provides a more user-friendly and platform-independent way to define a scene — the programmer can create a data structure representing the contents of the scene in terms of shapes, materials, transformations, lights, and so forth instead of having to issue specific OpenGL commands. A scene graph data structure can also be changed on the fly and can be written to — and read from — a file, making it possible to create general-purpose modeling and viewing programs that can work with any scene.
In this project, you will implement a WebGL renderer for XML-based scene description files and consider some design decisions for the scene description format.
This is an individual project. You may get technical help from others, but the effort and ideas that go into producing solutions should be yours.
 You may use AI as outlined in
the Use of AI policy —
Copilot's inline coding suggestions, explain, fix, review and
comment features but not code generation from English prompts.
Also:
Hand in your writeup in class.
Hand in your code by copying your ~/cs424/scenegraph folder into your handin folder (/classes/cs424/handin/username, where username is your username). Make sure that your scenes and shader files are contained in scenes and shaders subdirectories, respectively.
Check that the result is that your files are contained in /classes/cs424/handin/username/scenegraph — if not, fix it!
Copy the contents of /classes/cs424/lib into your ~/cs424/workspace/lib directory. Make sure that you end up (only) with a bunch of files inside ~/cs424/workspace/lib instead of multiple nested lib directories.
Copy the directory /classes/cs424/scenegraph and its contents into your ~/cs424/workspace directory. You should end up with a folder scenegraph inside your workspace directory, with files and two subdirectories inside of it.
[10/28 update] spot.scene and attenuation.scene have been added to the scenes subdirectory — if you previously copied the whole scenegraph directory, now copy just those two scene files from /classes/cs424/scenegraph/scenes into your ~/cs424/workspace/scenegraph/scenes folder.
Make sure that your scenegraph and lib directories are named exactly like that and is at the same level in your workspace directory. This is important so that the relative path names used to access common files remain the same so your program doesn't break when you hand it in. Also make sure that the scenegraph directory contains two subdirectories scenes and shaders.
The lib directory contains one new file:
webgl-file-utils.js contains some utility routines for loading text content for files.
The scenegraph directory contains several things:
renderer.html contains a partial implementation of a renderer for scene description files. (This file format is described below.)
The scenes directory contains several scene description files.
The shaders directory contains two vertex shaders and a fragment shader.
We'll be using an XML-based file format for our scene description files. W3Schools provides a brief introduction to XML through their XML Tutorial — start with the XML Introduction and read through the XML Attributes section.
The root element for our scene descriptions is scene. Four kinds of elements are permitted as children of scene and only one instance of each is permitted.
See the provided scene files for the attributes for these elements, and for the elements defining lights, materials, transforms, and geometry. The provided files together cover everything the core renderer needs to support.
A document object model (DOM) provides a standard way to access and manipulate documents. You can read more about the XML DOM in the W3Schools XML DOM tutorial — start with the DOM Nodes section and read through the DOM Get Values section.
The provided renderer.html contains code to load XML content from a text file (in loadScene) and parse that content into a structure that can be accessed via the XML DOM (in initScene). draw renders the scene, extracting the top-level elements of the scene (the camera, the projection, the list of lights, and the root of the scene graph) and traversing the scene graph, doing the WebGL stuff corresponding to various nodes (the camera, the projection, etc).
Two things in particular to notice in renderer.html:
Helper functions getAsFloat and getAsFloatArray are provided to help with parsing attribute values. Given a DOM node and a default value, they return the node's value as a float or an array of floats if the node is defined and the specified default value otherwise. Use null for the default value for required attributes — in that case, an exception will be thrown if the node is not defined.
Selectors are utilized to locate particular nodes or kinds of nodes. node.querySelector(selector) finds the first descendant of node that matches selector, while node.querySelectorAll(selector) finds all matching descendants. A few key aspects of selector syntax:
For much more on selector syntax, see this tutorial.
To simplify testing new features, move the scene file you are testing with to the beginning of the list in SCENES — this will make it the default selection when the live preview reloads. If you want to default to the mouse rotator instead of the scene's view, add checked at the end of the input tag near the end of the HTML document.
There are four main tasks for this project:
As you add functionality to the scene description format and the renderer below, create additional scene(s) to demonstrate (and test!) those new features.
The core renderer should be able to handle scenes with basic shapes (sphere, cube, cylinder, etc) and the lighting model discussed in class and implemented in labs 4 and 5. The provided renderer.html contains a partial implementation.
The organization of renderer.html is a little different from the WebGL programs we've been working with. In particular, the initialization functions have been split up a bit more in order to support loading shader source and scenes from external files rather than text elements within the HTML document. (This structuring is necessary because JavaScript loads files asynchronously — initialization that occurs after a file is loaded has to be set up in a callback.)
Drawing a scene means doing the usual things: set up shader parameters, draw geometry, set up shader parameters, draw more geometry, etc. In the case of the renderer, the values for setting up the shader parameters and the geometry to be drawn come from traversing the scene graph. This is draw's job.
Review renderer.html to make sure you understand its organization in terms of the WebGL elements.
Review draw in renderer.html in conjunction with the information on the scene description file format and the XML DOM above to see how it traverses and works with the XML DOM structure parsed from a scene file. Make sure you understand what is going on — you won't need to change the core structure of the traversal, but you will need to add cases for different types of nodes and thus be able to access a node's name, the values of its attributes, and its children.
Complete the renderer to fully support the six provided scene files. In (mostly) any order:
Add cases for the other shapes in elements.scene (cube, cylinder, cone, torus). Consider which attributes, if any, should be required and provided reasonable default values for the rest.
Add cases for the modeling transforms in elements.scene (translate, rotate, scale). Note that the rotation angle is specified in degrees. Consider which attributes, if any, should be required and provided reasonable default values for the rest.
Add support for perspective projections (frustum). Consider which attributes, if any, should be required and provided reasonable default values for the rest.
Add lighting: implement vertex and fragment shaders to compute pixel colors based on lighting, add the necessary code to pass light and materials parameters to the shaders, and add cases for point, directional, and spot lights in draw. Consider which attributes, if any, should be required and provided reasonable default values for the rest. More about implementing lighting:
The direction attribute for directional and spot lights is intended to be the direction that the light is shining e.g. direction="0,0,-10" is a viewer light, illuminating the front side of objects in the scene (remember that lighting is in eye coordinates).
The cutoff angle for spotlights should be in degrees.
Shaders should go in separate files in the shaders directory rather than being contained within renderer.html. Use the .glsl extension for shader filenames and follow the naming convention of -vert for vertex shaders and -frag for fragment shaders.
For full credit your shaders should implement Phong shading, have structs for light and material properties, and support multiple lights as defined in labs 4 and 5. (If you used your Phong shaders for lab 5, you are all set — use those shaders here. Otherwise you can get started with the provided lab4-vert.glsl shader or your solutions from lab 5 — you can proceed with much of the rest of the project with either of these shaders and then come back to finish anything involving a specific lighting model later.)
The end result is that your renderer should be able to handle everything in the provided scene files.
There is a key omission from the scene files so far: the ability to support user-defined mesh objects. (Only certain predefined shapes — sphere, teapot, etc — are supported.) While in general one might wish to support both indexed face set (IFS) and list-of-vertices representations, you only need to support IFS representations here.
Supporting a new feature like this requires two things: designing additions/modifications to the scene description file in order to specify the thing, and updating the renderer to process the new/modified elements.
For the design: Add a new element mesh with attributes for the vertex coordinates, vertex normals, and face indices. For convenience, specifying vertex normals in the scene file should be optional — if they aren't supplied, compute them. A helper function computeNormals is provided in renderer.html for this.
Add support for user-defined mesh objects: implement renderer support for a mesh element as described, and create one or more scene files containing mesh objects to test and demonstrate the feature.
Mesh objects can get very large, and having to repeat the full definition for a mesh object that appears more than once in a scene quickly gets prohibitive. A solution is the ability to name objects: a list of object definitions can be specified at the top level of the scene description and the name referenced in the scene graph. (An even better solution is support for external libraries so that mesh object definitions can go in a separate file and be reused in multiple scenes, but that's an optional extension below.)
For the design: a list of mesh object definitions can be handled like the list of lights — add an objects element to contain a list of mesh elements. Make the mesh elements into definitions by adding a name attribute to each mesh element. Within the scene graph, a mesh element with a name attribute refers to the corresponding definition.
For the renderer: a name must be defined before it is encountered during the rendering traversal of the scene graph. One option is to use querySelector to locate the DOM node with the definition when the name is encountered during the scene graph traversal. More efficient, but involving more JavaScript syntax, is to preprocess the list of definitions into a JavaScript data structure before traversing the scene graph — the idea is to build a JavaScript object indexed by the name to store a parsed version of the mesh object's data, then look that up when the name is encountered during the traversal of the scene graph.
Add support for defining named objects.
There is a great deal that could be added to the scene description format beyond these core features — several such features are listed below.
Most of the extensions will involve additions to the scene description format, but some only involve the renderer. When adding new elements to the scene description format, consider which attributes, if any, should be required and provide reasonable default values for the rest.
Also create new scene files to test/demonstrate each feature.
WebGL supports two alternatives for specifying a perspective projection: by defining the view window (gl.frustum) and by defining the field of view and aspect ratio (gl.perspective). Only one is necessary, but both are convenient in different situations.
Add support for defining a perspective projection via the field of view angle and the aspect ratio.
When there are multiple lights in a scene, it is often necessary to turn down the brightness of each light to avoid things becoming too bright. However, since the light's RGB color captures both hue and brightness, this means adjusting the red, green, and blue components separately.
Add a "brightness" attribute to lights of all types that modulates the light's diffuse and specular colors.
Within a scene, the same material may be applied to multiple objects. Instead of having to repeat the material properties for every object, it would be nice to be able to define a set of materials and refer to them by name in the rest of the scene. This also makes it easier to change materials — only the definition needs to be updated, rather than every instance of that material.
The key pieces of supporting this feature are similar to handling named objects: there will need to be a section at the top level of the scene description for material definitions and a way to reference those materials within the scene graph.
Add support for named materials: design the additions/modifications to the scene description file and update the renderer to process these new/modified elements.
Use some real materials (ignore the ambient color but note that the shininess values need to be multiplied by 128) in your demo scenes.
Object definitions for user-defined mesh objects allow repeated instances of specific geometry, but scenes may contain larger repeated chunks — compound objects are built from multiple objects, along with transforms to position the component objects and possibly different materials for those component objects.
Since XML is a tree-structured format, the option of simply attaching a subgraph as the child of multiple other scene graph nodes (forming a directed acyclic graph) isn't available. Instead, the key pieces of supporting this feature will be similar to handling named objects and named materials: there needs to be a way to define names for subgraphs and a way to reference those names within the scene graph to include those subgraphs.
Note that only separator elements can be the root node of a subgraph — they are the only elements in the scene graph that aren't leaf nodes. Similarly, subgraphs can only be spliced in where separator elements can occur — at the top level of the scene description or as a child of another separator.
A name must be defined before it is encountered during the rendering traversal of the scene graph. Definitions can be handled in the same way as for mesh objects and materials (up front, with a definitions section at the top level of the scene description) or inline (definitions occur within the scene graph itself by attaching a name to a separator node as it occurs as part of the scene graph). In the latter case, querySelector can be used with an appropriate selector to locate the definition when a reference is encountered, or to find all of the nodes with definitions for processing before rendering the scene graph.
Add support for repeated subgraphs: design the additions/modifications to the scene description file (including deciding whether to support upfront or inline definitions) and update the renderer to process these new/modified elements.
Section 4.4.1 in the textbook introduces the OpenGL 1.1 attribute stack, which allows saving and restoring many aspects of the current state such as light and material properties. See the glPushAttrib API for a full list.
Only the current material is relevant for the scene descriptions, but with support for repeated subgraphs, the behavior of the current material carrying across subgraphs that are not direct descendants is potentially problematic.
As with the modelview stack, WebGL does not maintain an attribute stack — to support similar functionality, you will need to maintain a JavaScript variable for the stack and push/pop state accordingly. Note that implementing an attribute stack for the current material requires only a change to the renderer — no additions to the scene description format are required.
Add support for saving and restoring the current material at separator nodes.
The shaders get lights in eye coordinates, so accommodating WC and OC lights means needing to transform the light by the viewing or modeling transform, respectively. As discussed in class, it is convenient to define EC and WC lights after the camera has been set but before any modeling transforms — at that point, modelview is the viewing transform and so it is only necessary to know if a light is EC or WC to know how to transform the light.
For OC lights, however, the entire modeling transform is needed. Section 4.4.3 in the textbook discusses a solution: make lights a node in the scene graph, similar to materials, transforms, and objects. The wrinkle is also identified: lights need to be set before geometry is drawn in order for the light to affect the object. And the solution is identified: traverse the scene graph twice, the first time updating the current modelview whenever a transform is encountered and defining a light when a light is encountered and the second time rendering everything other than light nodes.
Add support for WC and OC lights in addition to EC lights: design the additions/modifications to the scene description file to be able to specify the coordinate system the light is being defined in and, for OC lights, the modeling transform and update the renderer to process these new/modified elements.
There is a great deal that could be added to the scene description format beyond the core features and the extensions described above; some more are described below. These extensions are all optional — they are not required and can be done for extra credit.
OpenGL materials include an emissive color in addition to ambient, diffuse, and specular colors. The emissive color represents a glowing object which emits light but does not itself illuminate anything else. (Something which both glows and illuminates other objects, such as a light bulb, can be modeled by an object with an emissive material combined with a light source in the same place.)
Add support for emissive materials. This will also require a change to the shader.
Add support for drawing objects as wireframe. Include three options: solid, wireframe, or both.
Polyhedra have sharp edges between faces and should have flat shading (polygon normals) instead of smooth shading (vertex normals). While polygon normals can be achieved using a mesh object by duplicating shared vertices, it would be more convenient to be able to specify the vertices as for any other mesh (i.e. unduplicated) and have the renderer do the vertex duplication needed for flat shading.
Add support for polyhedra. Specifying polygon normals in the scene file should be optional — if they aren't supplied, compute them.
Supporting named materials within a single scene is nice, but even better would be to also support libraries of materials defined in separate files which can be imported instead of having to define common materials in every scene file.
Even better than materials libraries would be object libraries, where definitions of mesh objects can be imported into scenes instead of requiring redefinition in every scene. And why stop with single objects — whole subgraphs could be imported.
Technical note: this is a little tricky because JavaScript loads files asynchronously. See how loading scene files and shaders is done in renderer.html.
Add support for importing materials definitions from files.
Add support for importing mesh definitions from files.
Add support for importing named subgraph definitions from files.
Add support for common shortcuts: rotation around the x, y, and z axes, respectively, and for uniform scales.
Add support for other transforms, for example shear.
Add support for being able to specify a transform directly by giving the matrix.
Add support for a global ambient term in the lighting equation, in addition to the per-light ambient terms. This requires changes to the shaders as well as additions to the scene description format.
Include a short writeup addressing the following:
Document the additions to the scene description format — identify the elements and their attributes, including which attributes are required and what the default values are for optional ones.
Explain the rationale for your decisions about which attributes are required and which are optional.
Explain the rationale for your decision regarding upfront or inline definitions for named subgraphs.