|
Smooth voxel-based lighting |
So, like, how do you light a voxel world, anyway?
I'm using Unity. However, my terrain is procedurally generated at run time. The data can be saved and loaded, but it's not like I'm building a Quake level here. My Unity scene is
empty. Stark. Bare. I can't use all those fancy Unity tools for lighting. No lightmaps, no light samples, no baked radiosity, nothing.
And it's a sandbox voxel world. Voxels come and go, so I can't do anything fancy that requires precomputation. Whenever some user walks up and deletes a voxel, I've got to redo lighting. And if you've played Minecraft much, you know that you've got to kinda dump torches every 10 meters. All over the place. A distant view of your castle will have
hundreds of light sources.
From playing the game, one can guess what Minecraft does. You might be wrong. At some point, lighting for a voxel was calculated by shooting out rays from each voxel into the sky, and the number of those rays that made it determined the light value for that voxel. This only works outdoors, though; what happens when the sun sets? What do torches do?
So the next guess has two parts: (1) Anything directly exposed to sunlight gets lit at full brightness (during the day). (2) Everything else seems to be lit by light values in a voxel slowly dropping from voxel to voxel. A voxel "in the shade" (ie not directly exposed to sunlight) but that is
next to such a voxel will be lit just one tick darker. And so that's what I did.
The next trick is to figure out how to light each quad in the world. (With all my subvoxels, I've actually got to worry about WAY more planes, but that's a different story. Let's pretend we're doing strict cube-only worlds for now.) Lighting is there to see stuff, obviously, but we've got two main goals with the light algorithm: to model light occlusion, and to model light radiance.
The first means that something not facing a light source should be darker than something facing it. Further, if a quad faces a light source but is
occluded by an intervening object, then it should be darker. In a simple game engine, we could model the sun using a shadow map, perhaps rendered in screenspace, to indicate which parts of the world are exposed to a light. But, again: 100+ lights! This is madness. We need a solution.
The second term -- radiance -- means that, even if a quad isn't exposed to the sun, it will pick up light bounced off of other surfaces. Ideally, the surface should colorize the reflected light. This gets tricky with sunlight, however, since the sun itself isn't in the direction of sunlight. Sunlight comes from the whole sky, bouncing through the atmosphere, although
most brightly in a straight line. That bounced light is what makes outdoor shadows soft-edged and light. We kinda want an HDR solution here; a way of providing tons of resolution to the "how much light is here?" equation, as well as handling the fact that the human eye will adapt to the total amount of light in the environment.
But I get ahead of myself here.
So let's go back to the "simple" MineCraft-style solution. Any voxel directly exposed to sunlight gets the maximum light value. Any voxel with a torch in it gets the maximum light value. Other light-generating objects (jack'o'lanterns, ovens, glowstone) get the same treatment, although their light values are lower. Next, every other voxel gets assigned a light value that is one less than the most brightly-lit neighbor.
I'd love to colorize this light value. I'd love to directionalize it, so that we're actually modelling radiance; that would mean assigning 6 light values to a given voxel, with each value being not just a 4-bit value but an actual
color. Maybe a full 32 bits. And maybe rather than the 6 cardinal directions, we could model 18 directions (the 6 + the 12 diagonal directions along plane lines) or 26 directions (ie to all neighboring 26 voxels in a 3x3x3 cube around our source). Do we have processor power for that? Hurm. Dunno. There are real-time lighting solutions for complex-geometry games that compute light in six directions, eg using a clipmap, that can model complex interactions.
But... well do we really need that much detail?
Ok, so let's go back. Let's just get something working. We got a light value at a voxel. How do we light quads?
The goal
here, really, is to provide consistency. Take a 2x2 chunk of grass voxels, sitting in a plane under the sun. The intersection at the middle of the four top quads of these voxels should be seamless. Or not; that's a choice here. Minecraft Classic only lit quads by whether they were facing the sun or not. It was a simple directional one-light system with an ambient factor. You could tell which way a poly was facing by the lighting on its quads. At some point, MC introduced smooth lighting, and that's what I'm talking about here. Smooth lighting looks
sooo much better. So that's our goal. (When did MC add good support for both sunlight and moonlight, and torches, and the rest? I don't know. I'm not too interested in the history here.)
So the solution I've used is to come up with a clear definition for the lighting at any given vertex based upon neighboring voxel light values and the facing of the tri. (Remember, I'm dealing with subvoxels that have many faces not in any of the 3 axis planes.) In the simple case, at a corner such as our 2x2 chunk of grass voxels, lighting at the corner is the average of the light value of the four voxels directly above the grass. No matter which voxel you look at, that voxel's quad will have the same light value at each corner.
This gives us partial occlusion for free. If one of the four voxels above a vertex is solid, then that vertex will only be 75% lit. Two voxels, 50% lit. Three voxels, 75% lit. And we let the graphics card smoothly interpolate lighting across the quad.
Take a look at the image. The red area seems odd; why the bright contrast? Well that's cuz the bright bit faces out into the sunlight, while the dark bits are directly under the tree. (Sidewise light propagation is turned off in this image.) Inside the blue circle, you can see that corners get progressively darker.
There's algorithmic hoops to jump through to get this working fast, but do you really want to know about hashsets? I'll skip those details for now.