Fundamentals of VRAD: Part 1

May 15, 2018

Continuing the topic of waging war on the Source Engine monolith, I’ll be looking at something a little larger for the forseeable future. I’ll preface this by saying that prior to investigation, I had practically 0 idea about how vrad worked, although I have used it extensively. It’s become clear very quickly that understanding the Input and Output data has 0 bearing on understanding just how vrad functions.

What is vrad?

Vrad (Valve RADiosity simulator) is, simply put, a lighting pre-calculation tool that runs a radiosity simulation on a compiled Source Engine .bsp. It calculates lighting for any given surface, that is then stored inside the .bsp, such that lighting does not need to be calculated at runtime.

A clean breakdown of how vrad works has been written by x6herbius, and can be seen here: How VRAD works.md. For today, we’re just going to dive into the very first line:

Load the BSP and other resources, eg. lights.rad

Very well summarised, but not as simple as it appears. It is important to understand that the end goal is to obtain all the information required to generate a collection of C struct winding_t along with associated face information, from which Patches can be generated. Essentially, all other required structures are available in their current state as lumps. My current understanding is that a winding_t is what anyone familiar with graphics API’s can probably guess. winding_t represents an ordered collection of points from which you can construct a plane, as it represents a (optionally: clipped) infinite plane. Vertex order is important, as reading order affects derivable information, such as face normal. Reversing read order will reverse the normal direction, therefore the direction of the face too. This is commonly referred to as Winding Order. Winding in this context, according to x6herbius, is a leftover from Quake days, that Source was based off of.

Written in golang, winding_t looks like this:

const MAX_POINTS_ON_WINDING = 64

type Winding struct {
	NumPoints int
	Points [MAX_POINTS_ON_WINDING]mgl32.Vec3
	MaxPoints int
	Next *Winding
}

This could actually be simplified nicely in golang, as NumPoints can be derived from Points, if a slice was used over an array. Of course, there would be a performance hit, although simply using golang for this will incur a noticeable reduction, especially before optimisation.

Discarding irrelevant information

Naturally the first thing to be done is to load in our bsp so relevant lump data can be obtained. Quite a few lumps are important: Entdata, Planes, TexData, Vertexes, Visibility, Nodes, TexInfo, Faces, Leafs, LeafBrushes, Edges, SurfEdges, Models, Brushes, BrushSides, Areas, AreaPortals, MapFlags, VertNormals VertNormalIndices, and both TexDataStringTable+TexDataStringData.

The relevance of a lot of these are quite basic. Since the more generic lumps that directly contain mesh data (e.g. vertexes, edges), many of the specialised lumped (AreaPortals, Leafs) are necessary to determine whether particular faces should be ignored. MapFlags and the Tex* lumps are useful for reducing our face count further by ignoring faces with flags/materials that should be fullbright (e.g. UnLitGeneric, nodraw etc). Calculating light on these surfaces doesn’t pose a problem, but the engine will ignore it so a bit of time is saved during calculation by testing during preparing the environment.

We can also discard faces with materials listed inside of the lights.rad files included by both the game we’re compiling against, and an optional custom file we can define as a command flag. Lights.rad just defines a list of materials that should be emissive, referred to by the source code as TexLights.

Lastly, its worth noting that brush entities will tend to be discarded, unless they have the property vrad_brush_cast_shadows, so we again need to ensure that some brush entities are included.

Summarisation

The implementation for obtaining and generating these structures isn’t particularly complex, but there are a large number of lookups across a lot of datastructures, so it can appear a little confusing. In the meantime though, I’ve done a near line-by-line port of this initialisation process. The code of my VRAD port can be found here: https://www.github.com/galaco/VRADiant, and the entrypoint to initialising the datastructures can be found here: https://github.com/Galaco/VRADiant/cmd/tasks/loadbsp