Advertisement

Splitting rays. How to accumulate color?

Started by April 22, 2023 08:38 PM
8 comments, last by taby 1 year, 6 months ago

I am writing a ray tracer that splits rays upon collision with meshes. It's sort of working, except I can't figure out how to do that accumulation. Any ideas?

The ray generation shader looks like this:

#version 460
#extension GL_EXT_ray_tracing : require


struct ray
{
	vec4 direction;// = vec3(0.0);
	vec4 origin;// = vec3(0.0);
	bool in_use;// = false;

	int child_refract_id;
	int child_reflect_id;

	vec3 base_color;
	vec3 accumulated_color;

	float reflection_constant;
	float refraction_constant;

	bool reflected_ray;
};


const int buffer_size = 10;
ray rays[buffer_size];
int current_buffer_index = 0;


layout(binding = 0, set = 0) uniform accelerationStructureEXT topLevelAS;
layout(binding = 1, set = 0, rgba8) uniform image2D image;
layout(binding = 2, set = 0) uniform CameraProperties 
{
	mat4 viewInverse;
	mat4 projInverse;
	vec4 lightPos;
} cam;


struct RayPayload {
	vec3 color;
	vec3 pure_color;
	float distance;
	vec3 normal;
	float reflector;
	float opacity;
};

layout(location = 0) rayPayloadEXT RayPayload rayPayload;

// Max. number of recursion is passed via a specialization constant
layout (constant_id = 0) const int MAX_RECURSION = 0;

void main() 
{
	const vec2 pixelCenter = vec2(gl_LaunchIDEXT.xy) + vec2(0.5);
	const vec2 inUV = pixelCenter/vec2(gl_LaunchSizeEXT.xy);
	vec2 d = inUV * 2.0 - 1.0;

	vec4 origin = cam.viewInverse * vec4(0,0,0,1);
	vec4 target = cam.projInverse * vec4(d.x, d.y, 1, 1) ;
	vec4 direction = cam.viewInverse*vec4(normalize(target.xyz / target.w), 0);

	uint rayFlags = gl_RayFlagsOpaqueEXT;
	uint cullMask = 0xff;
	float tmin = 0.001;
	float tmax = 10000.0;



	// Step one: make tree of ray segments
	for(int i = 0; i < buffer_size; i++)
	{
		rays[i].direction = vec4(0.0);
		rays[i].origin = vec4(0.0);
		rays[i].in_use = false;

		rays[i].child_reflect_id = -1;
		rays[i].child_refract_id = -1;

		rays[i].base_color = vec3(0.0);
		rays[i].accumulated_color = vec3(0.0);
		rays[i].reflection_constant = 0;
		rays[i].refraction_constant = 0;

		rays[i].reflected_ray = false;
	}

	rays[0].direction = direction;
	rays[0].origin = origin;
	rays[0].in_use = true;
	rays[0].reflected_ray = false;
	
	current_buffer_index++;

	traceRayEXT(topLevelAS, rayFlags, cullMask, 0, 0, 0, rays[0].origin.xyz, tmin, rays[0].direction.xyz, tmax, 0);
	vec4 hitPos = rays[0].origin + rays[0].direction * rayPayload.distance;

	// hit the wall
	if(rayPayload.distance == -1.0)
	{
		rays[0].base_color = rayPayload.color;
		rays[0].reflection_constant = rayPayload.reflector;
		rays[0].refraction_constant = rayPayload.opacity;
		rays[0].accumulated_color = rays[0].base_color;
	}
	else
	{
		// entering
		if(dot(rays[0].direction.xyz, rayPayload.normal) <= 0.0)
		{
			if(current_buffer_index < buffer_size)
			{
				rays[0].child_reflect_id = current_buffer_index;

				//generate new ray
				rays[current_buffer_index].origin.xyz = hitPos.xyz + rayPayload.normal * 0.01f;
				rays[current_buffer_index].direction.xyz = reflect(rays[0].direction.xyz, rayPayload.normal);
				rays[current_buffer_index].in_use = true;
				rays[current_buffer_index].reflected_ray = true;

				current_buffer_index++;
			}

			if(false)//current_buffer_index < buffer_size)
			{
				//generate new ray
				rays[current_buffer_index].origin.xyz = hitPos.xyz - rayPayload.normal * 0.01f;
				rays[current_buffer_index].direction.xyz = rays[0].direction.xyz;//, rayPayload.normal);
				rays[current_buffer_index].in_use = true;
				rays[current_buffer_index].reflected_ray = false;

				current_buffer_index++;
			}
		}
		// exiting
		else
		{
			if(false)//current_buffer_index < buffer_size)
			{
				//generate new ray
				rays[current_buffer_index].origin.xyz = hitPos.xyz + rayPayload.normal * 0.01f;
				rays[current_buffer_index].direction.xyz = rays[0].direction.xyz;
				rays[current_buffer_index].in_use = true;
				rays[current_buffer_index].reflected_ray = false;

				current_buffer_index++;
			}
		}
	}
	
	rays[0].in_use = false;

	while(true)
	{
		int used_count = 0;

		for(int i = 0; i < buffer_size; i++)
		{
			if(rays[i].in_use)
			{
				used_count++;

				rays[i].base_color = rayPayload.color;
				rays[i].reflection_constant = rayPayload.reflector;
				rays[i].refraction_constant = rayPayload.opacity;

				if(rayPayload.distance == -1.0)
				{
					rays[i].in_use = false;
					continue;
				}
				
				traceRayEXT(topLevelAS, rayFlags, cullMask, 0, 0, 0, rays[i].origin.xyz, tmin, rays[i].direction.xyz, tmax, 0);
				vec4 hitPos = rays[i].origin + rays[i].direction * rayPayload.distance;
				
				// entering
				if(dot(rays[i].direction.xyz, rayPayload.normal) <= 0.0)
				{
					if(current_buffer_index < buffer_size)
					{
						rays[i].child_reflect_id = current_buffer_index;

						//generate new ray
						rays[current_buffer_index].origin.xyz = hitPos.xyz + rayPayload.normal * 0.01f;
						rays[current_buffer_index].direction.xyz = reflect(rays[i].direction.xyz, rayPayload.normal);
						rays[current_buffer_index].in_use = true;
						rays[current_buffer_index].reflected_ray = true;

						current_buffer_index++;
					}

					if(false)//current_buffer_index < buffer_size)
					{
						//generate new ray
						rays[current_buffer_index].origin.xyz = hitPos.xyz - rayPayload.normal * 0.01f;
						rays[current_buffer_index].direction.xyz = rays[i].direction.xyz;//, rayPayload.normal);
						rays[current_buffer_index].in_use = true;
						rays[current_buffer_index].reflected_ray = false;

						current_buffer_index++;
					}	
				}
				// exiting
				else
				{
					if(false)//current_buffer_index < buffer_size)
					{
						//generate new ray
						rays[current_buffer_index].origin.xyz = hitPos.xyz + rayPayload.normal * 0.01f;
						rays[current_buffer_index].direction.xyz = rays[i].direction.xyz;
						rays[current_buffer_index].in_use = true;
						rays[current_buffer_index].reflected_ray = false;

						current_buffer_index++;
					}
				}

				rays[i].in_use = false;
			}
		}

		if(used_count == 0)
			break;
	}

	// Step two: accumulate colour
	for(int i = current_buffer_index - 1; i >= 0; i--)
	{
		vec3 accum = rays[i].base_color;

		if(rays[i].child_reflect_id != -1)
			accum = mix(accum, rays[rays[i].child_reflect_id].accumulated_color, rays[i].reflection_constant);

		if(rays[i].child_refract_id != -1)
			accum = mix(accum, rays[rays[i].child_refract_id].accumulated_color, 1.0 - rays[i].refraction_constant);

		rays[i].accumulated_color = accum;
	}

	imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(rays[0].accumulated_color, 0.0));
}
Advertisement

I would advise against splitting rays. This leads to an exponentially increasing number of rays with each bounce, and I can't imagine it plays well with the GPU due to divergent workloads.

For the most physically correct results, you should look in to the “path tracing” algorithm. Path tracing is a Monte Carlo method (i.e. based on a random walk through the scene by many rays). Rays are emitted from the camera (N jittered rays in each pixel), and these rays intersect the scene. The N rays then bounce again and again until the termination criteria is reached. There are never any new paths, and this avoids the exponential explosion problem.

At every intersection point, the ray bounces randomly according to the BSDF of the surface. To handle transmission through the material, you can randomly transmit or reflect the ray (one or the other, based on RNG). The probability to transmit the ray should be the average of R, G, B transmission coefficients of the material. Therefore, more rays will transmit the more transparent the material is. You need to compensate the energy carried by each ray by the probability that the ray was sampled. For instance, if the transmission coefficient is 0.1, 10% of rays transmit. The transmitted rays energy should be divided by 0.1 (the PDF) for energy conservation. Similarly, the reflected rays (90% of rays) should be divided by 0.9, so that the energy is correct. When the ray transmits, you also multiply the RGB energy of the ray by the material transmission coefficient. When it reflects, you multiply the ray energy by the albedo.

Lighting can be done by tracing shadow rays from each intersection point toward the light sources in the scene. This is called “next event estimation”. Pick a random light source, then pick a random point on the light source. Then, try to connect to that light source point from the current ray intersection point on a surface. If it is visible, then add the contribution of that light to the ray's energy. You can also do bidirectional tracing, where you emit a ray from the camera and a light source and try to connect the ray traversal paths. For best results, bidirectional tracing needs something called “multiple importance sampling”, which drastically improves convergence.

Not really looking for fancy lighting yet. I read the book https://www.amazon.ca/Focus-Photon-Mapping-Marlon-John/dp/1592000088​ before, but I'm not looking to implement something like that yet. I got reflections working now… I changed the code:

#version 460
#extension GL_EXT_ray_tracing : require


struct ray
{
	vec4 direction;// = vec3(0.0);
	vec4 origin;// = vec3(0.0);
	bool in_use;// = false;

	int child_refract_id;
	int child_reflect_id;

	vec3 base_color;
	vec3 accumulated_color;

	float reflection_constant;
	float refraction_constant;

	bool reflected_ray;
};


const int buffer_size = 10;
ray rays[buffer_size];
int current_buffer_index = 0;


layout(binding = 0, set = 0) uniform accelerationStructureEXT topLevelAS;
layout(binding = 1, set = 0, rgba8) uniform image2D image;
layout(binding = 2, set = 0) uniform CameraProperties 
{
	mat4 viewInverse;
	mat4 projInverse;
	vec4 lightPos;
} cam;


struct RayPayload {
	vec3 color;
	vec3 pure_color;
	float distance;
	vec3 normal;
	float reflector;
	float opacity;
};

layout(location = 0) rayPayloadEXT RayPayload rayPayload;

// Max. number of recursion is passed via a specialization constant
layout (constant_id = 0) const int MAX_RECURSION = 0;

void main() 
{
	const vec2 pixelCenter = vec2(gl_LaunchIDEXT.xy) + vec2(0.5);
	const vec2 inUV = pixelCenter/vec2(gl_LaunchSizeEXT.xy);
	vec2 d = inUV * 2.0 - 1.0;

	vec4 origin = cam.viewInverse * vec4(0,0,0,1);
	vec4 target = cam.projInverse * vec4(d.x, d.y, 1, 1) ;
	vec4 direction = cam.viewInverse*vec4(normalize(target.xyz / target.w), 0);

	uint rayFlags = gl_RayFlagsOpaqueEXT;
	uint cullMask = 0xff;
	float tmin = 0.001;
	float tmax = 10000.0;



	// Step one: make tree of ray segments
	for(int i = 0; i < buffer_size; i++)
	{
		rays[i].direction = vec4(0.0);
		rays[i].origin = vec4(0.0);
		rays[i].in_use = false;

		rays[i].child_reflect_id = -1;
		rays[i].child_refract_id = -1;

		rays[i].base_color = vec3(0.0);
		rays[i].accumulated_color = vec3(0.0);
		rays[i].reflection_constant = 0;
		rays[i].refraction_constant = 0;

		rays[i].reflected_ray = false;
	}

	rays[0].direction = direction;
	rays[0].origin = origin;
	rays[0].in_use = true;
	rays[0].reflected_ray = false;
	
	current_buffer_index++;

	traceRayEXT(topLevelAS, rayFlags, cullMask, 0, 0, 0, rays[0].origin.xyz, tmin, rays[0].direction.xyz, tmax, 0);
	vec4 hitPos = rays[0].origin + rays[0].direction * rayPayload.distance;

	// hit the wall right off the bat
	if(rayPayload.distance == -1.0)
	{
		rays[0].base_color = rayPayload.color;
		rays[0].reflection_constant = rayPayload.reflector;
		rays[0].refraction_constant = rayPayload.opacity;
		rays[0].in_use = false;
	}

	while(true)
	{
		int used_count = 0;

		for(int i = 0; i < buffer_size; i++)
		{
			if(rays[i].in_use)
			{
				used_count++;

				rays[i].base_color = rayPayload.color;
				rays[i].reflection_constant = rayPayload.reflector;
				rays[i].refraction_constant = rayPayload.opacity;

				if(rayPayload.distance == -1.0)
				{
					rays[i].in_use = false;
					continue;
				}
				
				traceRayEXT(topLevelAS, rayFlags, cullMask, 0, 0, 0, rays[i].origin.xyz, tmin, rays[i].direction.xyz, tmax, 0);
				vec4 hitPos = rays[i].origin + rays[i].direction * rayPayload.distance;
				
				// entering
				if(dot(rays[i].direction.xyz, rayPayload.normal) <= 0.0)
				{
					if(current_buffer_index < buffer_size)
					{
						rays[i].child_reflect_id = current_buffer_index;

						//generate new ray
						rays[current_buffer_index].origin.xyz = hitPos.xyz + rayPayload.normal * 0.01f;
						rays[current_buffer_index].direction.xyz = reflect(rays[i].direction.xyz, rayPayload.normal);
						rays[current_buffer_index].in_use = true;
						rays[current_buffer_index].reflected_ray = true;

						current_buffer_index++;
					}

					if(false)//current_buffer_index < buffer_size)
					{
						//generate new ray
						rays[current_buffer_index].origin.xyz = hitPos.xyz - rayPayload.normal * 0.01f;
						rays[current_buffer_index].direction.xyz = rays[i].direction.xyz;//, rayPayload.normal);
						rays[current_buffer_index].in_use = true;
						rays[current_buffer_index].reflected_ray = false;

						current_buffer_index++;
					}	
				}
				// exiting
				else
				{
					if(false)//current_buffer_index < buffer_size)
					{
						//generate new ray
						rays[current_buffer_index].origin.xyz = hitPos.xyz + rayPayload.normal * 0.01f;
						rays[current_buffer_index].direction.xyz = rays[i].direction.xyz;
						rays[current_buffer_index].in_use = true;
						rays[current_buffer_index].reflected_ray = false;

						current_buffer_index++;
					}
				}

				rays[i].in_use = false;
			}
		}

		if(used_count == 0)
			break;
	}

	// Step two: accumulate colour
	for(int i = current_buffer_index - 1; i >= 0; i--)
	{
		vec3 accum = rays[i].base_color;

		if(rays[i].child_reflect_id != -1)
			accum = mix(accum, rays[rays[i].child_reflect_id].accumulated_color, rays[i].reflection_constant);

		if(rays[i].child_refract_id != -1)
			accum = mix(accum, rays[rays[i].child_refract_id].accumulated_color, 1.0 - rays[i].refraction_constant);

		rays[i].accumulated_color = accum;
	}

	imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(rays[0].accumulated_color, 0.0));
}

The whole code is at:

https://github.com/sjhalayka/sw_partial_reflectivity

I got refraction working great, taking into account the cases where there is total internal reflection.

https://github.com/sjhalayka/sw_partial_reflectivity

Advertisement

Looks good, did you try using smooth normals on the geometry (although maybe flat normals are the purpose here)? I'd be quite interested in how the result looks like.

This being said - I noticed that the frame time is taking some 37ms per frame. Out of curiosity - did you try to profile what takes the most time? For me, the scene doesn't seem that complex or big to be taking this long.

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

Yes, I forgot to smooth the normals on that mesh. I made another mesh, and it works better.

The main GPU hog is the ray generation shader. I run the whole thing three times, once per colour channel.

I have chromatic aberration working too:

Nevermind… I got Fresnel refraction working:

This topic is closed to new replies.

Advertisement