Gerard's Blagoblag Projects/Articles Shaders Github Shadertoy OpenProcessing Resume

3D Floor Map

WebGL is fun beans. Since it is in essence just a JavaScript object it doesn't have all the idiosyncrasies that traditional OpenGL implementations. There aren't multiple editions of GLSL to contend with, relaxed worries about capabilities between cards and OpenGL revisions, and stifled clunkiness in loading shaders and textures. However, to accomplish such a great feat of platform independence it has to dip its capabilities to nearly the least common denominator of hardware. (and impose limitations that greatly affect this project, as spoken of later.)

What I've made here is a 3D floor map of my dormitory floor at RIT. You click on a room, and info about that room pops up in a small dialog. Some rooms and other entities have links in their info that take you to relevant information, right there in the map.

Rendering

There are two editions of the map geometry used:

  • One which is color-coded in greyscale based on the room. This is the ID model.
  • Another which is colored for the user to see. I'll call this the "diffuse" pass.

These two models are rendered multiple times. First, we directly render the ID model to a framebuffer with a yellow background, allowing us to differentiate between clicks that are on and off the model with ease.

WebGL does not allow for multiple render targets, so for each pass of a multiple-buffer rendering method must be done independently. This fact causes some friction in the drawing of the model. The final desired product is fully wire-framed, and the wire-frame and selected room are gently Gaussian blurred, giving an old-timey sci-fi look. I was going for (in part) the style that Eidos' cult-classic game Whiplash had with their map display, but with a bit more detail and interactivity.) To accomplish this the rendering methodology the map uses has several steps. (On part of WebGL's lack of MRT support.)

Render pass of the ID model
Render the ID model into a framebuffer, and store it until later.

Render pass of the diffuse model
Render the diffuse model into a framebuffer, and store it until later.

Render pass to store surface normals of the model
Use GLSL's dFdx() dFdy() functions to render the surface normals of the diffuse model, into another framebuffer.

Render pass to store the depth values of the model
Render the depth of the diffuse model into a framebuffer. Notice now that we have all of the viewport-visible data of the model in image-space. (Including the earlier rendering of the ID model.)

The creation of the wire-frame
Next we apply a Sobel filter to the ID, normal, and depth renders. If the value of any of these at a given texel is above a certain threshhold, it is stepped up to 1 and stored in its own framebuffer. This is how the wireframe is created. Also rendered into this buffer is the selected room, if it exists. When we combine the wireframe with the final pass, the selected room is highlighted.

Drawback of using the normal pass to detect edges
It is important to note that certain edges are not visible in either the ID, diffuse, or normal passes. To detect these edges, we must also use the depth pass when generating the wireframe.

Blurring the wire-frame
Render the wire-frame pass, horizontally Gaussian blurred, into yet another framebuffer, then Take the horizontally blurred rendering, blur it vertically, and store it into yet another framebuffer.

The final composited frame
Combine the original rendering of the diffuse model with the unadulterated wire-frame pass, and the blurred wire-frame pass. This final addition of buffers is what is displayed to the screen.

Yes, there is a whole lot being done here. It can even be further reduced as-is. One might criticize the fact that normals are calculated in their own pass, when they can simply be derived from the depth-pass framebuffer. There is a reason for doing it my way: The way dFdx(genType g) and dFdy(genType g) work is, well, 'effing cool. Fragments are processed often in groups, each with their own set of registers with the GPU. These two functions actually snoop the registers of adjacent fragments for the state of the variable passed to them, and returns the difference between the local and snooped copy of the variable. That's wicked.

Interaction

Compared to the rendering of the map, interaction is rather straightforward. The user clicks somewhere on the screen. The location of the click is taken and transformed by the view-frustum of the ID pass, and then the transformed coordinates are used to read directly out of the ID pass framebuffer. Since the background in this pass is yellow, we know that if the values of the red and blue channels don't match, the user clicked outside the model. If they do, we send the red value to the info-popup dialog callback as the ID of the room to display.

Info for the room is queried from an LDAP database maintained by members of the dormitory floor.