Voxel Cone Tracing, more drama

Started by
17 comments, last by gboxentertainment 11 years, 2 months ago
Sorry to keep hammering on "Voxel Cone Tracing", with a looong post, but since I'm pretty close (I think) I want to finish it for once and for all.
It works more or less now. I can sample GI and specular light from a mipmapped octree. But, it just doesn't look good. Some parts do, some absolutely not. The 3 major issues (besides performance):
  • Light doesn't spread that far (1 bounce), especially not in narrow corridors that I have
  • Light spreads unequal. Incoming colors & strength vary too much.
  • Messy colors
  • Banding artifacts (see previous post)
  • Grainy result due low input resolution & jittering blur
However, when looking at Unreal4 or Crassins results, I believe better results should be possible. So I'm basically curious if someone with experience can point me to the cheats or critical implementation parts. Hence, if you have a interest & time, I would even invite you to help setting up VCT for our game "Tower22". I spend too much hours in GI the last year(s), the pain has to end!
--------
Let's walk through the issues. But first, there might be bugs in my implementation that contribute to these errors. Then again, ALL raymarch / 3D-texture related techniques I tried so far are showing the same problems, not VCT in particular. So maybe I'm always making the same mistakes.
1- Light doesn't spread that far
In Tower22, the environments are often narrow corridors. If you shine a light there, the opposite wall, floor or ceiling would catch light, making an "area" around a spotlight. But that's pretty much it. Not that I expect too much from a single bounce, but in Unreal4, the area is noticably affected by incoming light. Even if it's just a narrow beam falling through a ceiling gap. The light gradually fades in or not, nost just pops in a messy way on surfaces that suddenly catch a piece of light.
Or how about this:
Assuming the light only comes from the topleft corner, then how does the shadowed parts of the red-carpet compute light if they only use 1 bounce?? The first bounce would fire the light back into the air, explaining the ceiling, spheres and walls receiving some "red" from the carpet. But the floor itself should remain black mostly.
Unless they use 2 bounces, or simply another trick (AO / Skylight?) in addition. Going to 2 bounces sounds like a must to me, but first I want to make sure I'm doing everything right in the first bounce. And buy a much faster computer.
[attachment=12874:VCT_ObesitasCones.jpg]
2- Unequal light spread
This is killing the quality. It could be code errors, although I don't see bugs in particular when looking at the specular results. Meaning the rays collide at the correct points, also in the higher mipmaps.
[attachment=12875:VCT_Darkness.jpg]
With GI though, it just becomes messy. This has to do with banding errors (see #4), but I suspect more is going on. For the info, I'm using 9 rays for GI. The cone angle is adjustable. When using a different cone, the results are different as well, but not "worse" or "better". Just different. I think the cones shouldn't be too wide (in narrow environments, see pic above), or your rays will quickly stop half way the corridor as they already collide. Making them narrow on the other hand gives undersampling issues, so 9 rays becomes too little.
When you look at some of my shots, you can clearly recognize tiles. I mask that a bit by varying the sample directions randomly a bit, and by blurring afterwards. But still. you can pick them out easily (not good!). This has to do with mipmapping problems, see picture. The higher mipmapped levels aren't always smoothed, making them look MineCrafted. This is because when I sample from the brick corners in their child nodes, those locations may not be filled by geometry at that exact location (thus black pixels). Ifso, the
mipmapper samples from the brick center instead to prevent the result turning black. Well, difficult story.
[attachment=12876:VCT_TilesAndBanding.jpg]
The badness becomes far more visible when I show AO instead of colors. AO is then simply based on the average distance the 9 rays traveled. But it just varies for each location. I'm guessing some of the rays slip through walls, giving the maximum travel distance. It's nowhere close to the smooth result Crassin showed.
3- Messy colors
In addition to #2, I wonder if I inject the lighting right into the octree. If you look at the shots, you see the octree basically contains a blurry version of the actual scene. You also recognize the textures back in it (see the wood or wallpaper varying colors). This variation in color will also have its effect on the result, making it more messy. Plus in this particular case, the room will get very orange/brownish because most of the reflected color comes from that brown wood floor. Maybe its better to saturate the colors more towards grayish values, and maybe use 1 single color per texture. To reduce the variation.
[attachment=12877:VCT_OctreeContents.jpg]

4- Banding

My previous post explained it with pictures pretty well, and the conclusion was that you will always have banding errors more or less. Unless doing truly crazy tricks. In fact, user Jcabeleira showed me a picture of Unreal4 banding artifacts. However, those bands looked way less horrible than mines. Is it because they apply very strong blurring afterwards? I think their initial input already looks smoother. Also in the Crassin video, the glossy reflections look pretty smooth. Probably he uses a finer octree, more rays, more steps, and so on. Or is there something else I should know about?
My approach takes 4 steps per octree node. Earlier with only 1 step, I would per accident skip walls if the ray would sample somewhere at the edge of a node (stored as a brick in a volume texture). By taking multiple smaller steps, this problem was solved more or less, and the bands got finer (but still very noticable). Maybe I should take way more steps, but obviously, it will kill the performance as the amount of octree traversals and volume texture reads would increase insanely.
Yet another thing I might be doing wrong, is quadrilinear sampling (am I spelling that right?). In a mipmapped 3D texture, hardware does this for you. But in our case the 3D texture is sparse, meaning the bricks are scattered everywhere. So instead of really mipmapping the volume texture, I just generete extra bricks for the higher mipmap levels that are placed elsewhere. When travering the octree, it remembers the target node, but also the previous node from 1 level higher. This gives access to two different bricks,
so I can lerp between them. Not very subtle though. Each time I have to read the textures, I actually read 6 times. Positive or Negative X value, positive or negative Y value, pos/neg Z value. And that for 2 mipmap levels.
5- Grain
The grain is easy to explain, and probably the easiest to fix. I'm doing the sampling at 1/4 size of the screen. Then upscale it with a jitter blur. This produces big random blurry pixels. Using 1/2 of the screensize would save a lot, but again makes the technique a lot slower. No idea what Unreal4 does.
Apologees for the long post, but I just don't know how to explain it shorter. Complex stuff!!
Rick
Advertisement
Oh, one easier to answer little side question... I noticed the performance would suffer quite a lot when chosing a different struct size for my Octree nodes (each node is stored in a VBO). Not sure what I did, I believe reducing the size from 128 bytes to 64 or something. I thought that would make things a bit faster, but the performance actually dropped a lot. Maybe that was a bug elsewhere but anyway: what is a desired struct size for the GPU? Right now my voxels are 64 bytes, octree nodes 128 bytes (using some empty filling to make it 128b).
Let's walk through the issues. But first, there might be bugs in my implementation that contribute to these errors. Then again, ALL raymarch / 3D-texture related techniques I tried so far are showing the same problems, not VCT in particular. So maybe I'm always making the same mistakes.

Then you must be doing something wrong, I have an implementation working with 3D textures that gives flawless results for the diffuse GI (smooth and reallistic lighting, no artifacts nor banding).

1- Light doesn't spread that far
In Tower22, the environments are often narrow corridors. If you shine a light there, the opposite wall, floor or ceiling would catch light, making an "area" around a spotlight. But that's pretty much it. Not that I expect too much from a single bounce, but in Unreal4, the area is noticably affected by incoming light. Even if it's just a narrow beam falling through a ceiling gap. The light gradually fades in or not, nost just pops in a messy way on surfaces that suddenly catch a piece of light.

Try to remove the opacity calculation from the cone tracing and see if the light spreads further. I'm saying this because I've seen the voxel opacity causing too much blocking of the light and cause some of the simptoms you describe. In your case the light has to go through corridors which is problematic because in the mipmapped representation of the scene the opacity of the walls is propagated to the empty spaces of the corridor thus causing unwanted occlusion.

Assuming the light only comes from the topleft corner, then how does the shadowed parts of the red-carpet compute light if they only use 1 bounce?? The first bounce would fire the light back into the air, explaining the ceiling, spheres and walls receiving some "red" from the carpet. But the floor itself should remain black mostly.

The red carpet receives some reflected light from the object in the center of the room which is directly lit from the sun.

This has to do with mipmapping problems, see picture. The higher mipmapped levels aren't always smoothed, making them look MineCrafted. This is because when I sample from the brick corners in their child nodes, those locations may not be filled by geometry at that exact location (thus black pixels). Ifso, the
mipmapper samples from the brick center instead to prevent the result turning black. Well, difficult story.

You'll need to have your mipmapping working perfectly for the technique to work, taking shortcuts like this will hurt the quality badly. Make sure the octree mipmmapping gives the same results as a mipmapped 3D texture.

My previous post explained it with pictures pretty well, and the conclusion was that you will always have banding errors more or less. Unless doing truly crazy tricks. In fact, user Jcabeleira showed me a picture of Unreal4 banding artifacts. However, those bands looked way less horrible than mines. Is it because they apply very strong blurring afterwards? I think their initial input already looks smoother. Also in the Crassin video, the glossy reflections look pretty smooth. Probably he uses a finer octree, more rays, more steps, and so on. Or is there something else I should know about?

The banding artifacts from the Unreal 4 demo are smooth because their mipmapping is good, I'm convinced that most of your problems are caused by the fact that your mipmapping isn't right yet. And yes, Crassing doesn't show the banding probably because he used a high resolution (which is only possible because his test scene is really small).

Here's a good looking demo, with source code, of cone tracing. He's doing it through a 3d texture rather than an SVO, but the principles are there: http://www.geeks3d.com/20121214/voxel-cone-tracing-global-illumination-in-opengl-4-3/

And I don't believe EPIC is really planning on doing ONLY direct light and a single bounce. A very low level ambient term and HDAO is added in (at least some parts of) the Elemental demo as well. Should help with any "I can't see anything!" problems smile.png

The other thing you could try, as I assume you're already doing an ambient term, is something Pixar does. They inject an ambient term like amount of light into all points, replacing a normal ambient term with one that gives color bleed from all objects (and specular too!). But with the mipmapping errors you're getting it might look really weird.

Thanks again you both.

Indeed, the mipmapping issues are giving the tiled look I think. It will be hard to solve, but eventually I'll find something on that. I've also been thinking about scrapping the whole brick idea (which is already problematic on my somewhat older hardware) and to fall back on 3D textures covering the world. Like you described earlier. But, instead of raymarching through the textures (thus sampling 3 textures each step), I could still keep using the VCT octree to see if there is anything useful to sample at a certain point. So, a hybrid solution. I think sampling textures is actually faster than keep using a SVO, but it might be worth a try. Especially if the opacity calculation becomes more complicated (see below).

If I may ask, how do your textures cover the world (texture count, resolution, cubic cm coverage per pixel)? I suppose you use a cascaded approach like they do in LPV, thus having multiple sized textures following the camera. Do you really mipmap anything, or just interpolate between the multiple textures?

Anyhow, you said your solution didn't show banding errors. How did you manage that? Even with more steps and only using the finest mipmap level (which is smooth as you can see in the shots above), banding keeps occuring beceause of the sampling coordinate offsets. In the shots above, you'll see the banding on the wall. The finest brown bands on the wall are sampled from smoothed bricks (mipmap level 0). For the info:

* the smallest octree nodes are 25 cm3

* the ray takes about 4 steps in each node (slightly less, as the travel distance increases each step, depending on the cone angle)

Of course, there still could be a bug in the sample coordinates, but I'd say bandless sampling is just impossible. At least not in the way how I push the ray forwards.

>> Try to remove the opacity calculatuon

Good idea, and just did it. Didn't do any blocking at all, just to see if those dark T-junction corridor parts would catch light now. To exclude eventual other bugs. And... yes! A lot more light everywhere. As you say, the corridors quickly close in, blocking light on the higher mipmapped levels. But, just removing the opacity also leads to light leaks (and much longer rays = slower) of course.

Unless there is another smart trick for this, the only way to fix this is by providing more info to the voxels. In my case, the environment typically consists of multiple rooms and corridors close to each other. Voxels could tell from which room they are. So if the ray is suddenly sampling values coming from another room while the occlusion factor is already high, you know you probably skipped a wall. But yet, this sounds like one of those half-working solutions.

>> The red carpet receives some reflected light from the object in the center

True, but would that really result in that much light? Maybe my math is wrong, but each pixel in my case launches 9 rays, and the result is the average of them. In this particular scenario, the carpet further away may only hit the object with 1 or 2 rays, while the carpet beneath is hits it much more times. In my case the distant carpet would probably either be too dark, or the carpet below the object too bright. In the shot the light spreads more equally (realistically) though.

@Frenetic Pony

Thanks for the OpenGL demo, though it doesn't run on this computer hehe. Trying to dig out the shaders but asides from some common and mipmapping shader, I couldn't find the ray marching part.

Probably Unreal4 GI lighting solution isn't purely VCT indeed, but all in all, it seems to be good enough for true realtime graphics. One of the ideas I have is to make a 2 bounce system. Sure, my computer is too slow to even do 1 bounce properly, but I could make a quality setting that toggles between 0, 1 or 2 realtime bounces. In case I only pick 1, the first bounce is baked (using the static lights only) into the geometry. Not 100% realtime then, but hence none of the solutions in nowadays games are. A supercomputer could eventually toggle to 2 realtime bounces.

And yes, some extra's like SSAO, secundary pointlights or manually overriding the coloring of some parts should stay there. In the horror game I do, we don't always necessarily want realistic lights! Horror scenario's often have large contrasts between bright and dark.

Not sure if I get the Pixar method right... You mean they just add some color to all voxels in the scene?

Cheers

If I may ask, how do your textures cover the world (texture count, resolution, cubic cm coverage per pixel)? I suppose you use a cascaded approach like they do in LPV, thus having multiple sized textures following the camera. Do you really mipmap anything, or just interpolate between the multiple textures?

The biggest limitation of my implementation is precisely the world coverage which is currently very limited. I'm using a 128x128x128 volume represented by six 3D textures (6 textures for the anisotropic voxel representation that Crassin uses) that covers an area of 30x30x30 meters.
I'm planning on implementing the cascaded approach very soon which should only require small changes to the cone tracing algorithm. Essentially, when the cone exits the smaller volume it should start sampling immediately from the bigger volume, I think it's not necessary to interpolate between the two volumes when moving from one volume to the other, in particular for the diffuse GI effect which tends to smooth everything out, but if somekind of seam or artifacts appears then an interpolation scheme like the one used for LPVs can be used for this too.

Anyhow, you said your solution didn't show banding errors. How did you manage that? Even with more steps and only using the finest mipmap level (which is smooth as you can see in the shots above), banding keeps occuring beceause of the sampling coordinate offsets.

I didn't have to do anything, the only banding I get is in the specular reflection which is smooth as seen in the UE4 screenshot I showed you. For the diffuse GI you shouldn't get any banding whatsoever because the tracing with wide cone angles smooths everything out.
What do you mean with the banding being caused by the sampling coordinate offsets?

>> The red carpet receives some reflected light from the object in the center
True, but would that really result in that much light? Maybe my math is wrong, but each pixel in my case launches 9 rays, and the result is the average of them. In this particular scenario, the carpet further away may only hit the object with 1 or 2 rays, while the carpet beneath is hits it much more times. In my case the distant carpet would probably either be too dark, or the carpet below the object too bright. In the shot the light spreads more equally (realistically) though.

Now that you mention it, probably it shouldn't receive that much light. From what I've seen from my implementation, the light bleeding with VCT tends to be a bit excessive probably because no distance attenuation is applied to the cone tracing (another thing for my TODO list). I'm not sure if UE4 uses distance attenuation or not, their GDC presention doesn't mention anything about it and I believe Crassin's paper doesn't either. That's definitely something that we should investigate.

Probably Unreal4 GI lighting solution isn't purely VCT indeed, but all in all, it seems to be good enough for true realtime graphics. One of the ideas I have is to make a 2 bounce system. Sure, my computer is too slow to even do 1 bounce properly, but I could make a quality setting that toggles between 0, 1 or 2 realtime bounces. In case I only pick 1, the first bounce is baked (using the static lights only) into the geometry. Not 100% realtime then, but hence none of the solutions in nowadays games are. A supercomputer could eventually toggle to 2 realtime bounces.

A few days ago I implemented a 2 bounce VCT by voxelizing the scene once with direct lighting and then voxelizing the scene again with direct lighting and diffuse GI (generated by tracing the first voxel volume). The results are similar to the single bounce GI with the difference that surfaces that were previously too dark because they couldn't receive bounced light are now properly lit thus resulting in a more uniform lighting.
I've been making 3D textures with the world directly injected into them as well, using 3 grids. So I can compare with VCT. I did that a few times before, although I didn't really make use of "cone sampling" concept, leading to serious undersampling issues. Instead I just fired some rays on a fine grid, and repeated the whole thing on a more coarse grid and lerped between the results based on distance.


However, I'm running into some old enemies again. Probably you recognize those (and hopefully fixed them as well :) ).

* MipMapping
Probably I should do it manually, because when simply calling "glGenerateMipmap( GL_TEXTURE_3D )", the framerate dies directly. Instead I could loop through all mipmap levels, and re-inject all voxels for each level. Injecting is more costly, but there are way less voxels than pixels in a 128^3 texture (times 6, and 2 or 3 grids).


* Injecting multiple voxels in the same pixel
The voxels are 25 cm3 in my case, so when inserting them in a bigger grid, or when thin walls/objects are close to each other, it happens that multiple voxels inject themselves in the same pixel. Additive Blending leads to too bright values. Max filtering works good for the finest grid, but does not allow to partially occlude a cell (for example, you want at least 16 voxels to let a 1m3 cell fully occlude).

I should be averaging, eventually by summing up the amount of voxels being inserted in a particular cell (thus additive blend first, then divide through its value). But there is a catch, the values are spread over 6 directional textures, so it could happen you only insert half the occlusion of a voxel into a cell for a particular side. How to average that?


* edit
Still superslow due my lazy mipmapping approach so far, but the results look much better than I had with VCT. Indeed no banding except for specular reflections using a very narrow cone. And it seems the light spreads further as well. Yet, fixing the problems stated above will become a bitch. As well as the occlusion problem. Making the walls occlude as they should, block light in narrow corridors. Reducing the occlusion on the other hand gives leaks. I guess the only true solution on that is using more, less wide rays. I'm curious what the framerate will do. If its higher than with VCT, I can spend a few more rays maybe, though I'm more interested in adding a bounce eventually.

As for the limited size, right now I'm making 2 grids. One 128^3 texture covering 32 m3 (thus 25cm3 per pixel), and a second grid covering 128 m3 (thus 1m3 per pixel). Far enough for my indoor scenes mostly. Outdoor scenes or really bigass indoor areas should switch over the coarser grids. Well, having flexible sizes is not impossible to implement, we could eventually fade over to a larger or smaller grid when walking from area into another. May lead to some weird flickers during transition though...


Merry Christmas btw!

[quote name='spek' timestamp='1356369029' post='5013972']
Probably I should do it manually, because when simply calling "glGenerateMipmap( GL_TEXTURE_3D )", the framerate dies directly. Instead I could loop through all mipmap levels, and re-inject all voxels for each level. Injecting is more costly, but there are way less voxels than pixels in a 128^3 texture (times 6, and 2 or 3 grids).
[/quote]

Yeah, I can confirm that glGenerateMipmap(GL_TEXTURE_3D) kills the framerate, not sure why but it seems the driver performs the mipmapping on the CPU.

I think the best alternative is to simply create a shader that computes each mipmap level based on the previous one because it runs fast and is fairly easy to do. Re-injecting the voxels into each mipmap level as you suggest seems overkill and may not give you the desired results because you need the information about the empty voxels of the previous mipmap level to obtain partially transparent voxels on the new level. Are you doing this?

[quote name='spek' timestamp='1356369029' post='5013972']
I should be averaging, eventually by summing up the amount of voxels being inserted in a particular cell (thus additive blend first, then divide through its value). But there is a catch, the values are spread over 6 directional textures, so it could happen you only insert half the occlusion of a voxel into a cell for a particular side. How to average that?
[/quote]

Yeah, averaging is the right thing to do. Regarding the directional textures, you should propably average them as usual too. Just ensure you don't increment the counter when the voxel does not contribute for the radiance (when the weight of the voxel for that particular directional texture is <= 0.0).

PS: Merry Christmas to you too and to everyone else reading this ;D

I think mipmapping is so damn slow because it goes through all pixels, several times, for 6 textures. Replaced it with a manual shader now. It injects the voxels again (as points), so those points perform a simple box filter only at the places where it should be. The results are slight different than mipmap (can't say worse or better, varies a bit), probably because my shader is a bit different and because the plotting & sampling coordinates aren't 100% the same. Getting those right is a bitch with Cg and 3D textures. I haven't tried to skip the voxel injection and perform a simplified mipmapping yet... I can render one long horizontal quad (as a 2D object) to catch all layers at once. Far less draw calls, but more useless pixels to filter.

Anyhow, the framerate raised from 3 to 15 fps, which is not bad at all for my old nVidia 9800M craptop card! For the info,

- framerate was already pretty low due lots of other effects (somewhere around ~24)

- GI effect includes a upscale filter that brings the 1/4 GI buffer back to full size, polishing the jagged edges

- Only 1 grid used so far (128 ^3 texture, each pixel covering 25 cm3)

- 9 diffuse rays, 1 specular ray

- With VCT, the framerate was ~5. Both the construction & the raymarching goes a lot faster with simple texturing

And more important, the results finally look sort of satisfying. Maybe I can show a Christmas shot today or tomorrow hehe. Yet I still think a second bounce is needed if you really want to let a single light illuminate a corridor "completely". But more important for now is to implement the second grid first. And to make some baking options so that the produced GI can be stored per vertex or in a lightmap for older videocards that can't run this technique realtime properly. That would also allow to bake a first bounce (with static lights only) and do a second bounce realtime...

Averaging

Right now some of the corners appear as brigther spots in the result. Probably because they got a double dose of light indeed. But summing & averaging... For example, I have 2 RED voxels being inserted in the same pixel. One faces exactly to the +Z direction, another only a little bit. The injection code would look like this:

<<enable additive blending>> ... float3 ambiCube; ambiCube.x = dot( float3( +1, 0,0 ), voxelNormal ); ambiCube.y = dot( float3( 0, +1,0 ), voxelNormal ); ambiCube.z = dot( float3( 0, 0,+1 ), voxelNormal ); ambiCube = abs( ambiCube ); // Insertion if ( voxelNormal.x > 0 ) { outputColor_PosX.rgba = ambiCube.xxxx * float4( voxelLittenColor.rgb, 1 ); ++outputCounter_PosX; } ...and so on for the 5 other directions

So, the result could be rgba{1,0,0,1} + rgba{ 0.1, 0,0, 0.1 } = rgba{ 1.1, 0,0, 1.1 }

When dividing through an integer count (2 in this case), I get a dark result. If the other voxel would have rotated slightly further (not contributing to +X axis), the result would have been bright red though. Dividing through its own occlusion sum (1.1) would give a correct result in this particular example, but not if I would have inserted only the second voxel. In that case it would get too bright as well. rgba{0.1, 0,0, 0.1} / 0.1 = rgba{1, 0, 0, 1}

That's why I couldn't find a good way yet. I had the same problem with plenty of other similiar GI techniques btw (LPV for example). In case the voxels are as big as your cells, I would just use Max filtering instead of averaging, But that doesn't work too well when inserting the voxels in a much coarser grid though.

Thanks for helping,

Ciao!

About the red carpet being too lit, I think some UE4 talk mentioned a system for a coarse multi bounce tracing based on some idea I couldn't grasp. So they might very well have that carpet lit by the ceiling, or even by volumetric scattering, the air in that sample looks very foggy so they can have some kind of light scattering technique (energy diffusor). Or some locals light probes to complement lightings coming from some large areas light the sky.

the ground lighting outside the borders of a direct light that lit the ground (and this the ceiling indirectly in 1 bounce solutions) was an artefact that was happening with my implementation of LPV, for an unknown reason, I always supposed incrementally amplified errors in the propagation due to strong blurring due to SH encoding.

in VCT it could come from the lowest resolutions voxels mipmaps that got everything too mixed up and finally are just emitting a vague blur of energy everywhere.

but when the sources are mostly the ground facing upward... I agree that this shot looks weird because of that.

About the distance attenuation, it should not be done. we always suppose air as a non participant media that does not attenuate energy with distance. So the question to that interrogation of earlier has to be answered with simple radiance and flux concepts. A voxel emits a given radiance for a given surface, this amounts to a given total energy, that is the flux. the resulting irradiance on a given distant surface patch is calculable thanks to the solid angle, and that only should be considered.

(along with the "virtual" surface of the patch if it were being rotated to be made facing the source of radiance, which is what the lambert term stands for and why all radiance formula has it.)

well, my 2 cents. I hope I am not missing the point too much :)

This topic is closed to new replies.

Advertisement