Advertisement

Cascaded Voxel Cone Tracing reflections - cones go through walls at coarser LoDs

Started by November 05, 2019 06:35 PM
13 comments, last by Anfaenger 5 years, 3 months ago

I'm trying to implement reflections using Cascaded Voxel Cone Tracing using volume textures (i.e. not an octree).

When a cone starts inside a cascade and hits an object within the same cascade, the reflections are mostly correct:

289826846_vctreflectionsinthesamecascade.png.64c7bde609f93dae1735ab10333025fb.png

But when the cone starts on the inner cascade and hits something in the next (coarser) cascade (I'm using only two nested cascades for now),

it goes through the walls of the next cascade and shows the inside of the hit object (here the cones hitting something in the coarser cascade pick the red color for debugging):

723737895_vctreflectionsacrosscascades.png.e018100985fdbd8e3bb877e9ac721c1c.png

 

1) Does it happen because of thin (surface) voxelization?

But cones that start and end in the same cascade (texture) do not go through the walls.

 

2) I trace cones in volume texture UVW space. When I detect that a cone exits the current cascade, I transform the cone's origin into the UVW space of the coarser cascade (and scale the travelled distance as needed). Should anything else be done to avoid missing intersections with geometry?

Just in case, here's my (ugly) cone tracing shader:

Spoiler


float4 coneTraceSpecularReflection_Cascaded(
	float3 surfacePositionUVW
	, float3 surfaceNormal
	, float3 eyeToSurfaceDirection

	, VCT_ConeConfig cone
	, VCT_Params vctParams

	, float roughness	// The (linear) roughness of the surface material.

	, Texture3D< float4 > volumeTextures[GPU_MAX_VXGI_CASCADES]
	, int cascadeCount	// [1..GPU_MAX_VXGI_CASCADES]
	, SamplerState volumeTextureSampler
	)
{
	//TODO: Find the tightest cascade enclosing the surface point.

	// Trace the cone through the cascades.

	int iCurrentCascade = 0;

	float3 color = 0.0f;
	float  alpha = 0.0f;	// 'opacity'/transmittance/density accumulator

	// Compute the initial distance to avoid sampling the voxel containing the cone's apex (self-occlusion).
	// Unfortunately, it will result in disconnection between nearby surfaces :(
	const float surfaceBias = vctParams.getStartOffsetDistance();
	float distanceTravelled = surfaceBias;

	// in UVW space
	const float borderWidth = vctParams.m_grid_inv_resolution * 2;	// the size of voxel at coarser LoD
	//const float borderWidth = 0;// 0.25f;

	while( alpha < 1.0f )
	{
		const float3 currPos = getPositionAt( distanceTravelled );
		const float diameter = vctParams.getDiameter( cone, distanceTravelled );
		const float mipLevel = vctParams.getMipLevel( diameter );

		// break if the ray exits the voxel grid, or we sample from the last mip:
		[branch]
		if(
			mipLevel >= vctParams.m_texture_max_mipLevel
			||
			(currPos.x < borderWidth) || (currPos.y < borderWidth) || (currPos.z < borderWidth)
			||
			(currPos.x > (1.0f-borderWidth)) || (currPos.y > (1.0f-borderWidth)) || (currPos.z > (1.0f-borderWidth))
			)
		{
			// prepare for tracing through the next cascade
			++iCurrentCascade;

			// If this is the last cascade...
			if( iCurrentCascade == cascadeCount )
			{
				// ...then we hit the sky.
				const float skyVisibility = 1.0f - alpha;
				color += float3(0,0,1) * (skyVisibility * skyVisibility);
				break;
			}
			else
			{
				GPU_VXGI_CascadeParams cascadeToTraceNext = g_vxgi_cascades[ iCurrentCascade ];

				cone.apex = cascadeToTraceNext.transformPositionFromFinerCascade( currPos );
				distanceTravelled = cascadeToTraceNext.transformLengthFromFinerCascade( distanceTravelled );

				continue;
			}
		}

		// NOTE: in D3D11, an array of textures can be indexed only with a literal constant!
		//const float4 sample = volumeTextures[ iCurrentCascade ].SampleLevel( volumeTextureSampler, currPos, mipLevel );
		float4 sample;

		[branch]
		if( iCurrentCascade == 0 )
		{
			sample = volumeTextures[ 0 ].SampleLevel( volumeTextureSampler, currPos, mipLevel );
		}
		else if( iCurrentCascade == 1 )
		{
			sample = volumeTextures[ 1 ].SampleLevel( volumeTextureSampler, currPos, mipLevel );
sample.rgb *= float3(1,0,0);//ZZZ
		}
		else
		{
			sample = (float4) 0;
		}

		const float weight = (1.0f - alpha) * sample.w;
		color += weight * sample.xyz;
		alpha += weight;

		distanceTravelled += diameter;
	}

	return float4( color, alpha );
}

 

 

The screenshots are confusing, I am not sure where I supposed to be looking for the error. I suggest to simplify the problem, for example put a red box on a reflective plane. Verify that the first cascade reflections are correct by itself, then verify the next cascade. Finally, verify that the fallback from the first cascade to the next works when the cone exits the first cascade. Sorry, I haven't read the code in detail.

Advertisement

Yes this is why people don't use cone tracing. Other than Godot engine, which hasn't published what the heck they're doing (it's not even in release yet), and Cryengine, which I think you can get the source code and try to dig through that whole thing.

But afaik there's no published, easily referable way get around leaks of some kind or other. There is an idea I've had for quite a while and haven't gotten around to doing, which is recursive split cone tracing. It would be something like everytime a solid voxel of size x is encountered in the trace split current cone into four child cones in a smaller cascade and keep going. The solid walls/etc. encountered end up stopping cones, and resolution ends up exactly where you need it. But hardware compatibility and the performance of such a loop are concerning just imagining it.

Sorry, here's the simplest case - a box on a shiny plane.

The first cascade reflections (and reflections that start and end in the same cascade) are correct:

1879808123_vctreflections1-boxonplane.png.0502d300c7d3960ab52047fa71e15586.png

After moving the viewer a bit so that the box straddles the both cascades, reflection cones go through the cube's bottom:

799583826_vctreflections1-boxonplane-fail.png.577dd1414129f8518d58b3f1bffa1779.png

Here's a gif demonstrating the problem:

@Frantic PonE

Godot uses so-called GI probes - compact radiance textures (I think, they are compressed on CPU). IIRC, for each object, up to two nearest GI probes can be cone-traced and blended.

CryEngine uses a giant SVO, but I don't know how they update it dynamically (scary and long shaders).

I believe, you could use Signed Distance Field Cone Tracing (similar Claybook / UE4 tech) to prevent light leaks.
 

Gah, ok, as a guess it seems like anything in both voxel cascades goes missing from rays that transition from the first cascade to the second. If the rays start in the second cascade it's fine (I think? not enough info there) but if rays start in the first cascade, and hit voxel only present in the second cascade, they show up.

Maybe *crosses fingers*

Did not read it all, but there are two reasons for leaks - the first is voxel resolution too large to model a proper approximation of the scene which can't be helped, and the second is missing walls becasue too large steps while tracing. Looks your issue is the second.

To fix it, you can trace in a way you sample every plane in the voxel grid so nothing can be missed, using a DDA algorithm.

Seems this paper talks about it, but did not read ;) http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.42.3443&rep=rep1&type=pdf

Advertisement

@JoeJ

Yes, you were right - the cone step size was too large when tracing.

If, at each cone tracing step, I multiply the step size (that is equal to the cone's diameter by default) by, say, 0.1 - 0.5, the cones (almost) never miss geometry.

With the default cone step (=1), reflection cones may sometimes miss geometry (and show the inside of the reflected object) even if the cone starts and ends in the finest cascade.

The leaks also disappear if I reset the traveled distance to zero when the cone enters a coarser cascade.

Here's an ugly picture with buggy cascaded VCT reflections using 3 cascades:

1409988019_cascadedvctreflections.thumb.png.2b118a357b221b2c25f8f854408069c1.png

 

I'll try solid voxelization of blockers and see if it solves the problem of cones missing geometry.

I.e., I'll update the density/opacity texture on the CPU, then, probably, experiment with SDFs. (It should be easy in a voxel engine, and SDF is already available if the terrain is procedurally generated.)

 

I want to support large terrains, so I will be adding more cascades.

But I'm afraid that the cost of tracing the whole scene through several cascades would be prohibitively high,

so another GI solution would be needed, e.g. good old light probes.

 

@JoeJ

I'm gonna try baking spherical harmonic grids using VCT / ray marching (similar to Englisted GI).

Is this solution viable? What should I beware of?

I plan on using several cascades of SH light probes, each cascade - a 3 4-channel 16-bit volume texture.

The probes that lie inside the solid geometry (it's easy to find out in a voxel engine) will have zero weight.

I've never implemented light probes before, so I don't know what other problems can appear.

1 hour ago, Anfaenger said:

even if the cone starts and ends in the finest cascade.

I guess you have to match the transition to to coarser cascade. Likley the coraser always overlaps the finer, so fine data is always there but coarse is easier to miss? It should work somehow, even with constant step size per cascade. (But the step size should be one edge of a voxel, not the diagonl which is longer and so could miss one voxel. But otherwise i lack experience with constant step size. DDA surely works exactly, but is more expensive i guess.)

1 hour ago, Anfaenger said:

I'll try solid voxelization of blockers and see if it solves the problem of cones missing geometry.

I think it would be hard to get robust solid results eventually, and it is additional cost, also the internal vaxels could be unlit depending on what you do.

So i would not give up yet. Maybe you need to have more overlap in case the center of corse cascades is empty. Unfortunately most people do not implement cascades, so there is little help, but it should work...

1 hour ago, Anfaenger said:

I'm gonna try baking spherical harmonic grids using VCT / ray marching (similar to Englisted GI).

Is this solution viable? What should I beware of?

This is what i tried many years ago for realtime GI. I i gave it up in favor of surfels, but maybe i can say something about it...

What are your requirements, dynamic / static? Diffuse GI / specular reflections?

Ambient Dice is an overall better choice than spherical harmonics if you're going that route: https://www.ppsloan.org/publications/AmbientDice.pdf

You can choose two different ways of using it: Store in worldspace grid, and your solution ends up looking like The Division/RTX GI

Pros: Cheap runtime lookup with cost independent of scene complexity. Cons: Solid, static geometry highly preffered, dynamics hard/expensive/limited; updating good multilightbounces takes time/code complexity;  very limited specular resolution.

Or you can store a spherical basis at the object level as a set of vpls, then splat ala Square-Enix

Pros: Arbitrarily dynamic, excellent and arbitrary specular/diffuse resolution. Cons: Shadowing is separated, have to use signed distance field/conetraving/raytracing/whatever, expensive runtime lookup dependent on scene complexity; multilightbounce is expensive/difficult.

Those are the two major ones I can think of if you don't want to try split cone tracing, or waiting to see if Cryengine explains what they're doing in a readable manner (The Hunt: Showdown shows almost no leaking, somehow). The former one is the one shipped in The Division 1/2, and I suspect is coming to other titles, and is obviously more practical if you're not changing large scale geo a lot. If you wanted small doors you could probably hack it using RTX's probe influence channel thing ignore doors/windows, have doors/windows be analytic shapes, and trace against analytic shapes from probe location if there's a door close by,  then cutoff probe influence based on the trace. Something similar is done for Hitman, and it looks Ok-ish.

As for large scale terrain, just trace along a heightfield and call if good if you're far enough from the camera: https://slideplayer.com/slide/5173643/

@JoeJ

I don't know anything about surfels. I've recently seen this real-time GI demo, but its surfel-based GI requires heavy precomputation.

 

I'm making a building-terraforming game.

Dynamic (more precisely, semi-static) geometry with dynamic (slowly-changing) time of day, several km3 of terrain.

Mostly diffuse GI (e.g. for good-looking caves and buildings), but I also want specular reflections on water and shiny/wet materials.

I plan on supporting Direct3D 11 / Open GL 4 - level hardware (I don't have a GPU with RTX).

@Frantic PonE

Thanks for the references!

I didn't know about Ambient Dice. But it still takes more coefficients to store than 2nd order SH, and I'll be using volume textures to store light probes.

Unfortunately, my terrain can have non-heightfield topology.

 

This topic is closed to new replies.

Advertisement