Lots of hours have been spent on shaping Playform's terrain, playing with noise functions, isosurface extraction, and brushes. Lately I've been realizing how much of an impact the finer visual details make, starting with the introduction of depth fog. They help break up the monotony of an otherwise pretty homogenous landscape. I plan to bring in landscape features like trees to help too.
Procedural texturing means not having hand-crafted textures ahead of time. Because the terrain can shift and change organically, mapping these textures seamlessly is a bit of a headache. It's not impossible by any stretch (e.g. via Wang tiles). But I'm not convinced I want preconstructed assets at all - I think eventually, I'd like the user to be able to paint the terrain, much like how they can sculpt it. I want some cheap, easy, and decent-looking texturing there as a base, but I don't want the user married to it.
Noise
So how do we make a cheap, easy, and decent-looking texture from nothing? The solution is to find some smooth noise function, and twist and turn it into the desired texture. Lots of noise functions exist (Perlin noise is probably the most well-known), and there are even some built right into GLSL, the OpenGL shader language, but I ended up using this one for performance reasons, and because I'm not too nitpicky about the details as long as it's smooth and kind of bubbly looking. Here's a little patch of noise:
I'm still figuring out how to transform this basic thing, and how to think about stacks of transformations, but I've adopted Miguel Cepero's approach of adding layers until it looks halfway-decent. Here's what I've got so far. I suggest clicking to see the full-size images.
Dirt
Grass
Bark
I'm still having trouble getting things to look more jagged. The bark texture is the closest I've gotten (and I wasn't even trying to make bark - I was going for a dirt texture like this). Maybe I should just be using a different noise function for more jagged things. I'm not well-read on noise functions, so to my smooth bubbly mind, everything looks like a sinusoidal nail.
The code is up at github.com/bfops/procedural-textures. I encourage you to play around and try and make some better-looking things!
Sunday, August 30, 2015
Saturday, August 29, 2015
Voxel brushes
Playform now comes equipped with voxel brushes! They are very rough and very buggy, and the only one exposed directly to the user is a sphere brush, but the core functionality is still there.
This has been slowly in the works for over a month, and things were crashing pretty consistently for most of that time.
One big source of crashes was the code that turns the terrain voxels into a surface mesh (see this post). One invariant of that code is that if a voxel edge crosses the terrain surface, all the voxels containing that edge must be aware that they too cross the terrain, and keep extra data around to help build the mesh. It turns out it's really easy to violate that invariant if you're not being careful, and I wasn't. Another source of problems was that sometimes the "eraser" brush would expose parts of the world that hadn't been evaluated yet, because they'd been buried underground, and therefore weren't relevant to creating the mesh. Yet another series of crashes came up when a brush ended exactly between two voxels, because then they would "disagree" about whether they were crossing the terrain surface. After lots of debug logs and code comments, a version was reached that didn't constantly crash. It's still far from perfect, but it kindasortamaybesometimes works enough that I'm considering it functional for now, and doing other things for a while.
I started with a cube brush, because it's easy to define a cube and test whether it intersects various other things (especially points and cube-shaped voxels). This ended up being great since it exercised a lot of edge cases (pun intended), because the numbers were "too perfect". The sphere brush gave me an unreasonable amount of headache, because I couldn't find a clever way to place points inside some voxel that rested on the surface of some sphere (said another way: it's easy to pick some point inside a given voxel that rests on the surface of a given sphere, but it's a lot harder to find a good point that will make the mesh look nice). In the end, I just approached it from the point of view of "you have some shape, find a point on it", and reworked the terrain generation code that has to deal with exactly the same problem.
This has been slowly in the works for over a month, and things were crashing pretty consistently for most of that time.
One big source of crashes was the code that turns the terrain voxels into a surface mesh (see this post). One invariant of that code is that if a voxel edge crosses the terrain surface, all the voxels containing that edge must be aware that they too cross the terrain, and keep extra data around to help build the mesh. It turns out it's really easy to violate that invariant if you're not being careful, and I wasn't. Another source of problems was that sometimes the "eraser" brush would expose parts of the world that hadn't been evaluated yet, because they'd been buried underground, and therefore weren't relevant to creating the mesh. Yet another series of crashes came up when a brush ended exactly between two voxels, because then they would "disagree" about whether they were crossing the terrain surface. After lots of debug logs and code comments, a version was reached that didn't constantly crash. It's still far from perfect, but it kindasortamaybesometimes works enough that I'm considering it functional for now, and doing other things for a while.
I started with a cube brush, because it's easy to define a cube and test whether it intersects various other things (especially points and cube-shaped voxels). This ended up being great since it exercised a lot of edge cases (pun intended), because the numbers were "too perfect". The sphere brush gave me an unreasonable amount of headache, because I couldn't find a clever way to place points inside some voxel that rested on the surface of some sphere (said another way: it's easy to pick some point inside a given voxel that rests on the surface of a given sphere, but it's a lot harder to find a good point that will make the mesh look nice). In the end, I just approached it from the point of view of "you have some shape, find a point on it", and reworked the terrain generation code that has to deal with exactly the same problem.
Terrain, voxels, and meshes
Defining the terrain
Playform's terrain is defined using some cheap and easy noise functions. The basis is Perlin noise, which is a cheap and easy way of generating smooth, bubbly-looking output, like hilly terrain. A fractional Brownian model is applied to this noise, which is a term I learned just now, and a term I probably won't need to use again for at least another year. It basically just means that I take my Perlin noise, generate it at several amplitudes and frequencies in a fractal pattern (e.g. repeatedly multiplying both by 1/2), and then adding all the results together.
This lets you roughly describe lots of different terrain features pretty easily - mountains are high amplitude and low frequency, where bumps and ditches are low amplitude and high frequency. A nice thing about these fancy-sounding noise functions is that they're popular and well-understood, so finding libraries to generate them (and generate them quickly) isn't hard. Perlin noise can also be used to generate things like textures, which is a thing I've been meaning to work on for a while.
Storing the terrain
Playform's world is divided into a big 3D grid, and each cell can be called a "voxel" (which really just means a 3D pixel). Minecraft and many other games use this idea, so often each cell is considered filled or empty (or equivalently, filled with air). This world representation lets you add and remove pieces "anywhere" in the world without a whole lot of work.
Voxels don't just have to contain simple "full or empty" data; Playform's voxels contain more data so that we know whether each vertex in the grid is inside or outside the terrain. This means that any given grid edge can be entirely inside or outside the terrain (if both its vertices are), or it can be crossing the terrain. The crossing edges are the interesting ones.
This same idea applies to entire grid cells: if all the corners of a cell are on the same side of the terrain, we can just say that the whole voxel is inside/outside, but if it has some corners inside and some outside (or equivalently, if any of its edges cross the terrain), then the voxel intersects the surface of the terrain. Each intersecting voxel stores some extra data to help build the mesh: it stores a point inside that voxel that lies on the surface of the terrain (it doesn't have to be perfect, just approximate). It also stores a surface normal at that point, for lighting and whatnot.
Lastly, this grid isn't stored as a big 3D array; it's stored as an octree (a 3D binary tree). This lets you have bigger cells where less detail is needed (e.g. huge open areas, or huge chunks of space that are entirely inside the terrain), and it gives you the option of having "arbitrarily" smaller cells where more detail is needed.
From voxels to polys
The bulk of the credit for Playform's voxel-to-mesh algorithm goes to this Siggraph 2002 paper and to Miguel Cepero's blog post about exactly this problem.
Whenever we find an edge that intersects the terrain, we generate a little bit of mesh to represent the part of the terrain that is being crossed. We do this by looking at the four grid cells that the edge is a part of. Since the edge is crossing the terrain surface, all of those grid cells must be crossing it too, so they all store a point. We use those four points to construct a little patch of mesh.
This choice of what data to store in each voxel is nice for several reasons. It's pretty simple - a big portion of the work is deciding whether or not a given point is inside the terrain. Deciding where to place points inside the voxels isn't that important algorithmically, it just affects the visual quality of the resulting mesh - you can just put every point in the very center of its "parent" voxel. Curves won't look nice, but it's really easy to calculate. You can calculate surface normals using cross products, since once you have a few points, you also have vectors between those points that are roughly "lying on" the surface. You can make them a little smoother by averaging adjacent normals together.
The mesh points can be placed roughly anywhere inside the voxel, which means you can faithfully represent all kinds of different surfaces. Flat, curvy, pointy, the algorithm really doesn't care, as long as it has some points to work with. One thing I'd like to do is have a mesh import feature (again, credit to Miguel Cepero at Voxel Farm); as long as the grid is high-resolution enough that every mesh vertex is inside its own grid cell, the mesh could be imported without any loss of data, and then made a part of the world!
Depth fog - it makes a difference
Depth fog is the obscuring of objects based on how far they are from the viewer, eventually blending them into the background entirely. Here's a picture of Playform in all its depth-foggy glory:
Here's a roughly side-by-side example of the change. I'm unreasonably proud of how this looks.
Depth fog adds a lot! The code change is incredibly shallow (heh), with only a few extra lines in the shader for a simple exponential drop-off in visibility. I spent much more time tweaking the constants and the curve than I spent writing the code, and that's a nice place to be working in.
It definitely helps me get a sense of distance, and ideally the fog would be tweaked such that the objects at the furthest edge of the viewing distance aren't visible at all. Then they would fade in gradually as you moved closer, instead of aggressively popping in because they've suddenly been loaded. Right now, the bottleneck on view distance is memory consumption - the GPU crunches through the whole mesh in less than a tenth of a millisecond, but the amount of mesh that fits in memory is a little lackluster.
Stay tuned! Coming up hopefully-soon is voxel brushes, better texturing, and voxel materials. I hear trees are also planning a comeback tour.
Ahh.. Playform at dawn
Here's a roughly side-by-side example of the change. I'm unreasonably proud of how this looks.
Without depth fog
With depth fog
Depth fog adds a lot! The code change is incredibly shallow (heh), with only a few extra lines in the shader for a simple exponential drop-off in visibility. I spent much more time tweaking the constants and the curve than I spent writing the code, and that's a nice place to be working in.
It definitely helps me get a sense of distance, and ideally the fog would be tweaked such that the objects at the furthest edge of the viewing distance aren't visible at all. Then they would fade in gradually as you moved closer, instead of aggressively popping in because they've suddenly been loaded. Right now, the bottleneck on view distance is memory consumption - the GPU crunches through the whole mesh in less than a tenth of a millisecond, but the amount of mesh that fits in memory is a little lackluster.
Stay tuned! Coming up hopefully-soon is voxel brushes, better texturing, and voxel materials. I hear trees are also planning a comeback tour.
Subscribe to:
Posts (Atom)