Back when I was talking about making beveled edges, some people asked why I didn’t just lift up the corners of cubes to make slopes instead of mucking around with the soft / solid geometry business. At the time, I wanted to avoid a system where I would attempt to build a cube:
And wind up with this:
I actually had the program doing this when I was working on beveled edges, and it was messy and frustrating. After a while I got used to it, but that wasn’t really the same as having it be intuitive in the first place. To do this right, you’d have to move away from cube-based building and adopt some sort of point-based system. To the end user the difference is purely semantic, but internally a point-based system would have a big impact on how you generate geometry.
That sounds really interesting, but I can’t let myself be distracted from my important goals of… of… What was the point of this project again? Oh, right: Screwing around and working on whatever I find interesting, and damn having a plan. That goal is 100% compatible with tearing the guts out of the engine and starting over with a different system. So let’s do that.
It looks like marching cubes is the go-to solution for this kind of thing. There’s even some raw example code I could drop in and have it working. But it’s no fun to just use some other code without knowing how it works. I don’t just want to use it, I want to understand it.
Marching cubes is a system for taking a 3D point cloud of data and turning it into polygons. By “point cloud” I mean all you have is a 3D grid that says whether or not each space is occupied. I decide to start by implementing marching squares, which is the same idea, but implemented in only 2 dimensions.
(If the image doesn’t make it clear, if you’ve got two points off on the same side then you just cut the square in half.)
With four on / off points, there are a total of sixteen possible configurations for each square. You sort them out by assigning a power-of-two number to each corner. Say we make the upper-left point 1. Upper-right will be 2. Lower right is 4. Lower left is 8. Now add up all the active corners. In the example image above, this would result in 1 + 4 + 8 = 13. If all corners were off the number would be zero. (All empty space.) With all corners on you get 15, which is just a solid green square.
You can arrange the numbering any way you like. The point of this numbering system is that you can just make each and every one of your 16 combinations (although zero and fifteen are the easy ones) and then just have it draw the appropriate pre-made shape out of the 16.
I implement this in my program. Note that we’re using squares to make 3D volumes, which doesn’t really work. I knew this wouldn’t work when I started, I just wanted a quick way to teach myself how it worked, and see how it looked. I’m taking squares and extruding them into 3D, so these “cubes” don’t have tops or bottoms.
Hm. Kind of interesting. I did have to break everything to do this. Texturing, lighting, everything is screwed up. (Also note that the purple cross in the middle of the screen is supposed to be a plus sign. Yes, I even managed to break the crosshair.)
It’s not the most exciting thing in the world, but it has a certain charm. It’s interesting enough that I’m willing to take what I’ve learned and implement the 3D version of this. This is going to take a while, during which time everything will be hilariously broken. I mean even more broken than it is now.
The trick with marching cubes is that instead of the 4 corners of a square, you’ve got the 8 corners of a cube. Which means there are 256 possible combinations. 256 different ways to cut up a single section of cube-space to represent all of the different point patterns. Once you understand how it works the whole thing is trivially easy to implement, but it is very, very time-consuming. I get a few dozen configurations into it before I go nosing around for some code. Now that I get the idea, everything from here on is drudge work.
Luckily, someone has indeed done the painstaking part and put the result up online. Even better: It’s old-school ANSI C code. C++ has a lot of advantages when you’re building a large project and you need some way to manage complexity, but when you need a simple bit of code that does a simple thing, nothing beats C for clarity. Far too many programmers would have made this into some convoluted class interface that spans several modules and header files, but the stuff on the linked page is pretty much ready for copy & paste.
So how does it look?
Er. Yeah. Without lighting it’s pretty dang hard to see anything. I’ve got it painting some facets different colors, and I can make out the contour of the geometry when I’m moving the camera around, but without shading it’s pretty much impossible to make sense of a still image.
The problem is that I can’t light this thing properly. To do lighting, you need surface normals. Surface normals are numbers attached to your 3D geometry that tells you which way it’s pointing. Remember that 3D graphics are made of triangle, and triangles are made of points, and points are just dots in space. A dot doesn’t have a direction. Is this dot facing the light? Or away? Without normals, you don’t know.
Hm. I’ve got some code here that will analyze groups of triangles create normals for them. Mathematically, it’s pretty hairy and it’s one of those code snippets that I have to re-learn every time I need to do something with it. (If you’ve got the source for Project Frontier, look for CalculateNormals or CalculateNormalsSeamless in the Mesh module.) It’s crazy slow, but it should let me add lighting to the scene so I can see what I’m looking at.
Savor this image, because I’m not making more of them. It took the thing over a minute of chugging to come up with this scene because of the extreme expense of the normal-calculation code. Well, one more:
For comparison, here is the same location rendered with the old cube system.
I add some texture maps. These just use my old brain-dead mapping logic from part 6. Of course, the system breaks down now that I’m no longer using cubes. The situations where I used to have mirroring I now have a big ugly smear as a single column of pixels is stretched over several meters worth of geometry.
It’s sort of amazing how organic this looks. This is the same sort of data-set I’ve been using since part 5. Remember that these are just one-meter cubes with the corners knocked off, even though it looks like we’re dealing with high-polygon lumps.
I add some simple vertex coloring to the mix and have it generate a simple “maze”.
(That white thing in the middle of my screen is my edit icon. That’s where blocks appear or vanish when I hit the appropriate button.)
Building in this stuff is very, very strange. It’s not dealing with “blocks” anymore. Instead I’m building with… blobs. A single point in the air is shaped like a diamond. But once you get a few of them next to each other it feels more like building with clay.
I find I want to dig wider like this. Previously I was content to travel through tunnels that were 1 block wide and 2 tall, but with the beveled shapes the tunnel is sort of cramped like that. So I need to excavate 2 or 3 wide to be comfortable. But then I get impatient and start spamming the destroy button to clear the way. The result is a very lumpy tunnel.
Well, the project is now well and truly broken. I migrated from Qt back to Visual Studio, as I discussed in part 8. Now I’ve re-written the core of the program. The result is that I have pages and pages of disabled code and a dozen or so things that no longer work. Collision, texture mapping, lighting, building, the movement controls… almost every system has something annoying wrong with it. I’m going to clean up this mess and get everything working right again. After that I’ll come back and decide if I want to go back to cubes or keep working with blobs.
The Best of 2017
My picks for what was important, awesome, or worth talking about in 2017.
The Middle Ages
Would you have survived in the middle ages?
Shamus Plays LOTRO
As someone who loves Tolkein lore and despises silly MMO quests, this game left me deeply conflicted.
Resident Evil 4
Who is this imbecile and why is he wandering around Europe unsupervised?
A look at the main Borderlands games. What works, what doesn't, and where the series can go from here.