I'm a big fan of constructive solid geometry because it allows people without advanced modeling skills to design a game level that looks great. In fact, I originally got my start in game development using the Quake and Half-Life modding tools, making game maps.
One of the criticisms of CSG has been that it only allowed creation of faceted objects. (Valve's Hammer Editor has a workaround for this that lets you set smooth groups, but you can't see the results until they are run in the game.) This was preventing me from making some game level features I wanted, like curved arches and rounded corners, so I decided to do something about it.
Leadwerks supports smooth groups for CSG objects. To access the controls, switch to face editing mode and then select the objects tab in the right-hand sidepanel:
It was fairly easy to calculate vertex normals from smooth group information. However, that information still has to be fed to the lightmapper or the lighting will appear faceted across a curved surface:
To solve this, I needed to calculate the interpolated normal across the surface, and use that for the lighting equation for each luxel (lightmap pixel). Initially, I thought I could use a simple weighted average. Vertices near the luxel would have a high influence, and vertices further away would have less influence. However, it quickly became apparent this would not produce anything close to accurate results!
Gouraud ShadingThe problem I was facing is actually a very common calculation that is done in real-time on the GPU. This was the first time I ever had to calculate it myself. It turns out the problem was first solved before I was born by a clever fellow by the last name of Gouraud, and thus we call it Gouraud Shading or Gouraud Interpolation.
The algorithm works like this: draw a straight line in any direction from the point you want to interpolate. It doesn't matter what angle, as long as it's one straight line. Now find the two triangle edges the line intersects. Each of those edges is connected to two vertices, each with a normal. Use a linear interpolation to weight those two normals, for each point. Finally, use the distance of both these points from your original position to weight their interpolated normals:
More information on this technique can be found here.
ImplementationGetting this to work on a CSG lightmapper was difficult for two reasons. First, CSG objects consist of n-sided polygons, not triangles. Although they can be broken down into triangles, I was worried that visual artifacts might arise. Second, lightmaps have a user-defined bleed border, and the luxels of a lightmap extend beyond the edges of the polygon being lit. Gauroud shading requires the point being interpolated actually be inside the triangle. Our luxel positions could be inside any of the triangles that make up a polygon face, or they might not be on the face at all!
I decided to start by only worrying about the luxels that fell inside one or another triangles on the face, and solve the outliers later. Fortunately, the transform and math classes built into Leadwerks 3 made it fairly easy to convert all the points into flat 2D space to solve the problem. As expected, my first attempt identified the luxels that fit inside a particular triangle, but the luxels along the edges could not be processed, and appear dark:
I added an error threshold for the triangle intersection routine, which got rid of the black borders, but turned out to be a bad idea. Some of my values were being interpolated in the wrong direction, as you can see in the following images:
In this image, it's almost working, but the error threshold is causing luxels along the center seam to get lit incorrectly. Additionally, a few luxels in the top right are forming a dark border:
The final piece of this puzzle was to deal with luxels that didn't fit into a particular triangle, This was a pretty puzzling problem, and for a while I thought there might not be a "correct" solution. However, if you think about it intuitively, a luxel that lies just outside a triangle should use the same lighting as a luxel just inside that triangle, right next to it.
For the remaining unsolved luxels, I tested each of their distances to each triangle in the face they belong to. I found a nearest triangle to each, then found the nearest point on that triangle, and calculated the normal from that point's position.
ResultsThis technique produces beautiful smooth lightmapping on curved surfaces:
The algorithm works with curves, arches, sphere, any CSG objects that use smooth groups. So now you can make those castles, arches, and towers you've always wanted to build:
This feature will be available in the next update to Leadwerks 3.