Skip to content

KarlHajal/Raymarcher

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

94 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ray Marching and Ambient Occlusion Rendering

Abstract

We developed a ray marching engine in WebGL that allows users to specify scenes to be rendered in JSON format. Its architecture is based on the raytracing framework used in the course exercises, and it implements the following features:

  • Adaptive ray-marching algorithm (sphere-tracing)
  • Handles 16 different primitives with full control over their position and rotation.
  • Support for combinations of shapes (intersection, union, subtraction). Any two primitives can be specified in the JSON file to be combined.
  • Phong lighting and reflections.
  • Soft shadows can be enabled, and the factor can be specified.
  • Ambient Occlusion
  • Environment Mapping can be enabled and any desired cubemap specified with 3 examples provided.
  • We further added 4 scenes where noise is raymarched to achieve aesthetic results: 3D Perlin Noise, 3D Perlin Noise + FBM, Waves, and Clouds.

Technical Approach

Raymarching

To achieve adaptive ray-marching, we implemented the basic sphere tracing algorithm whereas at every iteration, we call the function that calculates the shortest distance to a surface in the scene and, if that distance is not small enough, the point along the ray is advanced by that distance so as not to penetrate any surface in the scene.

Sphere Tracing

This is implemented as follows:

float raymarch(vec3 ray_origin, vec3 marching_direction, out int material_id) {
    float depth = MIN_DISTANCE;
    for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
        float dist = scene_sdf(ray_origin + depth * marching_direction, material_id);
        
		if (dist < EPSILON) {
			return depth;
        }
        
		depth += dist;
        
		if (depth > MAX_DISTANCE) {
            return MAX_DISTANCE;
        }
    }
    return MAX_DISTANCE;
}

Basic Primitives



We added to ability to specify in the JSON file 16 different primitives which are the following: Plane, Sphere, Box (+ rounded edges), Box Frame, Cylinder, Capsule, Torus, Triangle, Triangular, Link, Cone, Pyramid, Hexagonal, Ellipsoid, Octahedron.

The Signed Distance Function (SDF) for each primitive was implemented with the help of the following resource: Inigo Quilez - SDFs.

However, the aforementioned SDFs assume that the primitives are centered at the origin. Therefore, before using each one, we have to transform the position of the point from which we're checking the distance to each primitive so that it would be at the same position relative to the corresponding shape if the latter was centered at the origin.

Note: Scenes are rendered incorrectly on Ubuntu machines, which is probably due to driver issues. This issue will be investigated.

Combinations





We added the ability to add smooth intersections, unions, or subtractions of any two primitives in JSON format directly as shown in the following example:

unions: [
    {	
        material: 'white',
        smooth_factor: 0.7,
        shapes: [
            {type: 'box', center: [-1, 2, 0.1], length: 2.5, width: 2.5, height: 0.7, rotation_x: 0, rotation_y: 0, rotation_z: 0, rounded_edges_radius: 0.1, is_frame: 0},
            {type: 'sphere', center: [-1, 2, 0.5], radius: 0.8}
        ]
    },
]

A ShapesCombination struct is used in the shader. It has information that allows us to locate each one of the two shapes:

struct ShapesCombination {
	int shape1_id;
	int shape1_index;
	int shape2_id;
	int shape2_index;
	int material_id;
	float smooth_factor;
};

Where the shape id tells us what type of primitive the shape belongs to (e.g. shape1_id == 1 means that it's a sphere), and the shape index tells us at what index of the array of that primitive this particular shape is contained. And to be able to access each shape using the index, we had to add for each primitive a get function such as the one shown below:

#if COMBINATION_NUM_SPHERES != 0
vec4 get_sphere(int sphere_index){
	for(int i = 0; i < COMBINATION_NUM_SPHERES; ++i){
		if(i == sphere_index){
			return combination_spheres_center_radius[i];
		}
	}
	return combination_spheres_center_radius[0];
}
#endif

While this implementation is nice in the sense that it works and allows us to set directly in the JSON format any two primitives to be combined, it has a major drawback, which is that rendering combinations is extremely slow. This is most probably due to the heavy cost of branching.

In conclusion, this implementation allowed us to specify combinations dynamically in the JSON, but had the drawback that we had to add a lot of code on top of what we had, making it quite hefty, and is very slow.

Lighting

Shading scene from the exercise sessions rendered in our engine

We implemented basic phong lighting and reflections. We elaborate on some aspects below:

Soft Shadows



Soft shadows with penumbra were implemented to add better looking and more realistic shadows. They can be enabled from the JSON by adding the option, and the soft shadows factor can be specified.

The implementation was done with the help of the following reference: Inigo Quilez - Soft Shadows.

It is a very straightforward implementation which we can benefit from since we're doing ray marching. Essentially, when computing sharp shadows, we check if the ray from a surface to the light source intersects with an object, and if it does then there's a shadow. For soft shadows, we use the fact that we are calculating distances to check how far the ray is from the object in case there's no intersection. Consequently, when the distance is very small, we want to put the point on the surface under penumbra, i.e. the smaller the distance from the surface, the darker the point should be. So with this slight modification to the code that allows us to modify the darkness of each point in this soft manner and to control it by a variable factor, we can very easily achieve nice effects such as the one seen in the image above.

Ambient Occlusion

We implemented Ambient Occlusion by casting from each surface point 32 rays in random directions along a hemisphere whose direction was based on the surface normal. The number of rays who intersect with surfaces are counted, and the ambient occlusion function returns the percentage of rays that have intersected. The higher that number is, the darker the spot is.

Ambient Occlusion Diagram

The above video gives an example of a scene rendered with the Ambient contribution only for lighting, which showcases the effects of Ambient Occlusion.

We further experimented with a "fake" Ambient Occlusion implementation which is more efficient and basically consists of casting 8 rays in fixed and predefined directions and returning a smoother result. This implementation yields similarly satisfactory results when rendering scenes with several types of lighting, essentially achieving the darkening of spots that are surrounded by several surfaces and improving the quality of the lighting in a scene. However, when the scene is rendered with the ambient contribution only, we can see that it yields wildly different results to the first implementation. While the original implementation yields a very nice and playdough-ish style, this one is only focused on heavily darkening surfaces such as edges and not affecting the rest. Both can have their uses when trying to achieve novel non-photorealistic rendering effects. A comparison is shown below with the "fake" implementation on the left, and the more involved one on the right:



_

Environment Mapping



Environment Mapping was implemented using cubemaps which were passed to the shader as a uniform samplerCube, and consequently, whenever a ray doesn't intersect with any surface, we set the color of the pixel depending on the texture's color at the corresponding position.

Environment Mapping can be enabled from in the JSON, and any cubemap can be specified. 3 examples are provided with the source code.

Results

All the scenes and features mentioned above can be tested and rendered by running the WebGL source code.

Further, the video below summarizes and shows off all the features described in this report, the results of which have mostly been shown in figures above.

<iframe width="420" height="315" src="https://www.youtube.com/embed/dxjDXLZMDC8"> </iframe>

References

About

Ray marching engine in WebGL

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published