Forward path tracing, part 2 -- announcing results after months of tinkering LOL

Started by
9 comments, last by JoeJ 1 month, 2 weeks ago

After taking a few months away from the forward path tracer, I recently solved my biggest problem, that of flat colouring.

Now it works much better:

Now to add reflections and refractions.

The relevant code is:


float trace_path_forward2(const vec3 eye, const vec3 direction, const float hue, const float eta)
{
	const vec3 mask = hsv2rgb(vec3(hue, 1.0, 1.0));

	const vec3 light_o = get_random_light_pos(50, eye, direction, hue, eta);
	const vec3 light_d = RandomUnitVector(prng_state);

	const float energy = 1.0;

	int step_count = 0;
	vec3 step_locations[max_bounces + 3];
	vec3 step_directions[max_bounces + 3];

	traceRayEXT(topLevelAS, gl_RayFlagsOpaqueEXT, 0xff, 0, 0, 0, eye, 0.001, direction, 10000.0, 0);

	if(rayPayload.dist == -1)
		return 0.0;

	const vec3 first_hit_pos = eye + direction * rayPayload.dist;

	step_locations[step_count] = light_o;
	step_directions[step_count] = light_d;
	step_count++;

	if(false == is_clear_line_of_sight(first_hit_pos, light_o))
	{	
		vec3 o = light_o;
		vec3 d = light_d;

		for(int i = 0; i < max_bounces; i++)
		{
			traceRayEXT(topLevelAS, gl_RayFlagsOpaqueEXT, 0xff, 0, 0, 0, o, 0.001, d, 10000.0, 0);

			if(rayPayload.dist == -1)
				return 0.0;

			const vec3 hitPos = o + d * rayPayload.dist;

			o = hitPos + rayPayload.normal * 0.01;
			d = cosWeightedRandomHemisphereDirection(rayPayload.normal, prng_state);
			
			step_locations[step_count] = o;
			step_directions[step_count] = d;
			step_count++;

			if(true == is_clear_line_of_sight(first_hit_pos, step_locations[step_count - 1]))
				break;
		}
	}




	step_locations[step_count] = first_hit_pos;
	step_directions[step_count] = light_d;
	step_count++;

	step_locations[step_count] = eye;
	step_directions[step_count] = direction;
	step_count++;






	// Reverse the path
	uint start = 0;
	uint end = step_count - 1;

	while(start < end) 
	{ 
		vec3 temp = step_locations[start];  
		step_locations[start] = step_locations[end]; 
		step_locations[end] = temp; 

		temp = step_directions[start];
		step_directions[start] = step_directions[end]; 
		step_directions[end] = temp; 

		start++;
		end--; 
	}


	float ret_colour = 0;
	float local_colour = energy;
	float total = 0;

	for(int i = 0; i < step_count - 1; i++)
	{
		vec3 step_o = step_locations[i];
		vec3 step_d = step_directions[i];

		traceRayEXT(topLevelAS, gl_RayFlagsOpaqueEXT, 0xff, 0, 0, 0, step_o, 0.001, step_d, 10000.0, 0);

		local_colour *= (rayPayload.color.r*mask.r + rayPayload.color.g*mask.g + rayPayload.color.b*mask.b);

		total += mask.r;
		total += mask.g;
		total += mask.b;

		if(i == step_count - 2 || 
		(rayPayload.color.r > 1.0 || 
		rayPayload.color.g > 1.0 || 
		rayPayload.color.b > 1.0))
		{
			ret_colour += local_colour;
			break;
		}
	}

	return ret_colour / total;
}

Thanks again for all of the help.

Advertisement

OK. So I decided to take your guys' advice and take advantage of the reciprocity in path tracing. So, the forward path tracer is now a lot simpler. Unfortunately it's back to that flat lighting!

float trace_path_forward2(const vec3 eye, const vec3 direction, const float hue, const float eta)
{
	const vec3 mask = hsv2rgb(vec3(hue, 1.0, 1.0));
	
	float ret_colour = 0;
	float local_colour = 1;
	float total = 0;

	vec3 light_o, light_normal, light_colour;
	get_random_light_pos(light_o, light_normal, light_colour, 10, eye, direction, hue, eta);
	const vec3 light_d = RandomUnitVector(prng_state);

	const float energy = 1.0;

	traceRayEXT(topLevelAS, gl_RayFlagsOpaqueEXT, 0xff, 0, 0, 0, eye, 0.001, direction, 10000.0, 0);

	if(rayPayload.dist == -1)
		return 0.0;

	local_colour *= (rayPayload.color.r*mask.r + rayPayload.color.g*mask.g + rayPayload.color.b*mask.b);

	total += mask.r;
	total += mask.g;
	total += mask.b;

	const vec3 first_hit_pos = eye + direction * rayPayload.dist;

	vec3 last_location = light_o;
	vec3 last_direction = light_d;

	if(false == is_clear_line_of_sight(first_hit_pos, light_o))
	{	
		vec3 o = light_o;
		vec3 d = light_d;

		for(int i = 0; i < max_bounces; i++)
		{
			traceRayEXT(topLevelAS, gl_RayFlagsOpaqueEXT, 0xff, 0, 0, 0, last_location, 0.001, last_direction, 10000.0, 0);

			if(rayPayload.dist == -1)
				return 0.0;

			local_colour *= (rayPayload.color.r*mask.r + rayPayload.color.g*mask.g + rayPayload.color.b*mask.b);

			total += mask.r;
			total += mask.g;
			total += mask.b;

			const vec3 hitPos = o + d * rayPayload.dist;

			o = hitPos + rayPayload.normal * 0.01;
			d = cosWeightedRandomHemisphereDirection(rayPayload.normal, prng_state);
			
			last_location = o;
			last_direction = d;

			if(true == is_clear_line_of_sight(first_hit_pos, last_location))
				break;
		}
	}

	return local_colour / total;
}

This time I'm using a green sphere:

Any ideas?

So you want to get rid of array / recursion.

Found an example on shadertoy.

I did not work on this, and i'm not sure to understand it well.
But maybe this example helps:

Thinking only about the red color, we may form a path hitting 3 surfaces in order with these colors:
0.2, 1, 0.5

Then we hit a light.

To track back the contribution of the light, we can just multiply:

0.2 times 1 times 0.5 = 0.1

So the result is 0.1 * light emission.

I assume it is that simple, but not sure.
To carry out the multiplication, maybe the shadertoy code uses the variable ‘throughput’, initialized to one.
And maybe the second variable ‘accumulation’ is only needed as well to sum up next event estimation results as well for each vertex.
If we did not use next event estimation, one variable should be enough i guess, but that's actually the part which confuses me a bit.

EDIT:

Maybe the second variable is need also / or exclusively to implement russian roulette, which i don't know about yet either?

Maybe somebody can clarify…

Thanks for your feedback JoeJ.

I'm putting the forward path tracer aside, once again, to work on other things.

JoeJ said:
Found an example on shadertoy.

It's quite common approach (shameless self promotion with public one - https://www.shadertoy.com/view/MdKyRK

So a bit of math background - with path tracing we solve the rendering equation:

The Rendering Equation

This is an integral equation (Fredholm's integral equation of the second kind - if you're really interested in definitions) which is often unsolvable (within finite time) - but we can approximate it with numeric approach, one of the fastest converging will be Monte Carlo random walk. Keep in mind - it expands into series (Liouville-Neumann series, if you're interested in definitions). At number of samples approaching infinity, we will get the solution (so we just approximate to get something ‘close enough’).

There are other properties for specific algorithms like unbiasedness, and possibility of progressive computation (with each additional step you get closer to actual solution), etc. Which play a bit with these properties.

Back to the topic though - because it is Fredholm's integral equation of the 2nd kind - the integration is recurrent (i.e. effectively infinite), which means that we have to: Terminate each single Monte Carlo random walk at some point. Additionally, by observing and expanding how such Monte Carlo random walk would look equation-wise:

First 3 iterations of Liouville Neumann expansion of rendering equation

Due to lack of space - I didn't resolve the L_o' and L_o'' into according L_e (also it would probably be more confusing).

From the expansion we can immediately observe that each further step is reduced by a factor. And because we accumulate the resulting value - it makes sense that we algorithmically only need to keep 2 variables during actual implementation:

  • factor - holds the current (for step within random walk) factor, by which current increment into accumulation is multiplied
  • accumulator - holds the (currently - and once random walk finishes also final) accumulated result for single random walk

Which is what you observe in the code.

Now - termination case. Each random walk has some contribution, more specifically each step of random walk has some contribution - which gradually decreases by a factor. But what is this factor? Well… exactly what equation defines - multiplication of subsequent B*DF functions between steps in random walk.

To keep the random walk unbiased and computation time finite - we do have to terminate. Statistically our chance to terminate path grows proportionally with how the factor is decreased (as the contribution of next step in given walk closely gets to 0 - the chance for termination gets higher). We can't terminate just like that though - not after Nth step or such. We have to terminate randomly to keep the equation unbiased.

Which is where Russian Roulette comes in.

My current blog on programming, linux and stuff - http://gameprogrammerdiary.blogspot.com

Vilem Otte said:
So a bit of math background

Thanks!
But even after reducing both shadertoys to just simple diffuse and playing around, i did not really get it.
I'll have to work on my own CPU PT, first removing recursion, then adding RR… : )

But i just see you have Linux in your sig, so i want to ask which IDE do you use?
I've tried Eclipse, knowing it from former mobile game and web dev. Seems fine so far.
I've also made a simple CMake project using GLFW on Windows, so i should be ready for cross platform.
But then i got stuck with transferring it to Linux on VirtualBox. Can't access shared host folders, so i would need to email it to myself, if i can't fix that…


JoeJ said:
But i just see you have Linux in your sig, so i want to ask which IDE do you use?

For command line editing I'm mostly using vim. Which is still quite often when I have to do something remotely … but code wise, I'm also enjoying luxury of IDEs.

On Windows it's mostly either VS (for C++/C# projects) or VS Code (for almost anything else). I still like platform specific ones for mobile dev (Kotlin → Android Studio (on both Windows, Linux and Mac OS), Swift → XCode (Mac OS only obviously)). For Linux development I use VS Code nowadays (previously Atom was quite popular, before it was phased away - I think there is fork from it named Pulsar).

I sometimes try something new, but mostly stay with the above mentioned ones. Still - for tiny changes I still use vim. It's often faster than opening projects in ide and working through that.

Note:: The sig is old - I intended to change it like dozen times already and point it to proper site, but hey… being too busy at work.

JoeJ said:
I've tried Eclipse, knowing it from former mobile game and web dev. Seems fine so far.

Eclipse is … well … fine from what I've heard. I have used it for Java at the university, but not since.

JoeJ said:
I've also made a simple CMake project using GLFW on Windows, so i should be ready for cross platform.

Ideally - yes. Practically … well, I generally ended up with CMake for Linux and VSProj for Windows.

JoeJ said:
But then i got stuck with transferring it to Linux on VirtualBox. Can't access shared host folders, so i would need to email it to myself, if i can't fix that…

You should be able to access shared host folders. Did you add on the virtual machine under Settings→Shared Folders? If not, it won't be visible!

My current blog on programming, linux and stuff - http://gameprogrammerdiary.blogspot.com

Vilem Otte said:
You should be able to access shared host folders. Did you add on the virtual machine under Settings→Shared Folders? If not, it won't be visible!

Yeah, but did not work on either of my two test Linuxes. Maybe i should update VirtualBox and try again…

JoeJ said:
Yeah, but did not work on either of my two test Linuxes. Maybe i should update VirtualBox and try again…

Do you have auto-mount enabled?

Also… your installed system may not support vboxsf (VirtualBox Shared Folder) format - which might be a problem. To get around that, you need to install guest-additions in VBox. Start your vm and on top under Devices select Insert Guest Additions CD Image…

This adds VBox guest additions into your cdrom. So, the next step is to mount it and install it, with (something like - typing it from memory, so not sure if I don't do a typo there - don't just copy/paste!):

sudo mkdir /media/cdrom
sudo mount -t iso9660 /dev/cdrom /media/cdrom
sudo apt-get update
sudo apt-get install -y build-essential linux-headers-`uname -r`
sudo /media/cdrom/./VBoxLinuxAdditions.run

You will probably need to reboot at this point (there might be some other way - but rebooting is probably easier). So, once you have VM rebooted - you should be able to mount the folder now even manually (if auto-mount doesn't work at this point), like:

sudo mkdir /media/shared
sudo mount -t vboxsf shared /media/shared

Under /media/shared you should have your shared folder now.

EDIT: If you want to make folder persistent - add this into fstab:

shared /media/shared vboxsf defaults 0 0 

add follow by adding vboxsf into /etc/modules. After reboot you should see the folder each time..

My current blog on programming, linux and stuff - http://gameprogrammerdiary.blogspot.com

Yay, some progress : )

Using the commands, i was able to mount the additions disc in Garuda Linux for the first time. I can not update this OS, it says something about missing keys. Trying to install additions anyway, i see compiler errors, probably related to the new Zen Kernel. And i do not really expect the additions can work at all with such specialized OS on VBox.

I also have a very old Ubuntu for those expected reasons. Here the additions work (GUI became responsive after installing). I also see the shared folder, but can't open. It complains about missing permissions to do so, and using your commands it says ‘Protocol error’.
So i tried the Linux update, but gives 404. I also can't view any webpage beside google, saying ‘Error code: ssl_error_protocol_version_alert’.

Did not try to fix this yet, but if i fail i'll download some newer Ubuntu Vbox image. I'm optimistic…

Thanks, those commands are useful to give some more clues.
Soon i will be free and claim back control over my computer… \:D/

This topic is closed to new replies.

Advertisement