Jamey Johnson (Ray Rays Juke Joint) Down in the Hollar

Michael Walczyk

Logo

View My GitHub Profile

Ray Marching

If y'all've ever visited Shadertoy, y'all've probably seen ray marching in activity. Information technology'due south an interesting technique that can exist used to generate fully procedural environments entirely from a single fragment shader. Unfortunately, in that location aren't a ton of great resources for learning these techniques. So, I wanted to create a quick tutorial on setting upwardly a bones ray marching shader. I will be using Derivative's TouchDesigner for rendering, just you should be able to port this to whatever other 3D environment fairly easily. In future blog posts, we will build upon the topics covered in this tutorial to create more interesting scenes!

If you lot are working with TouchDesigner, all y'all will need for this tutorial is a single GLSL Peak. In the "Common" tab, I inverse the "Output Resolution" parameter from "Utilise Input" to "Custom Resolution," 720 10 720.

Signed distance Functions

As I hinted at already, when we use ray marching, we aren't dealing with polygons anymore. So, a lot of the things that nosotros're used to in a typical 3D toolkit - geometry, lights, cameras - are nonexistent. But if we don't have points, lines, triangles, and meshes, how do we return anything? The trick is to use signed distance functions or SDFs (I will use these two terms interchangeably throughout this post). These are mathematical functions that take a point in space and tell you how far that betoken is from a surface. This can be somewhat confusing to wrap your caput around, so maybe an example will help.

Let's say we are at a point p in 3D infinite. In GLSL, we can represent p as:

for some coordinates x, y, and z. Now, suppose nosotros have a sphere centered at the origin with unit of measurement radius. We want to respond the question, "what is the altitude from p to the closest point on the sphere?" It turns out that this distance corresponds to the length of the vector that points from p to the sphere's eye, minus the sphere's radius. The following GLSL function implements this:

                              // params:                // p: arbitrary point in 3D space                // c: the heart of our sphere                // r: the radius of our sphere                bladder                distance_from_sphere                (                in                vec3                p                ,                in                vec3                c                ,                float                r                )                {                return                length                (                p                -                c                )                -                r                ;                }                          

And if you aren't convinced, check out the diagram beneath. Here, ||p - c|| denotes the length or norm of the vector p - c, which points from the eye of the sphere c to p. Convince yourself that ||p - c|| - r does, in fact, give u.s.a. the distance from p to the closest point on the sphere. The code snippet to a higher place is our first signed distance role. This function is "signed" because information technology returns a negative value, zero, or a positive value, depending on whether we are inside, on the surface of, or outside the sphere, respectively. We tin can write these iii cases:

Instance 1

||p - c|| < r, which means we are somewhere inside the sphere. This implies that ||p - c|| - r is negative.

Case 2

||p - c|| = r, which ways we are somewhere on the surface of the sphere. This implies that ||p - c|| - r is zero.

Case 3

||p - c|| > r, which means we are somewhere outside the sphere. This implies that ||p - c|| - r is positive.

Ok, and then we have a basic SDF, and we know how to evaluate it at any signal in 3D infinite. How do we actually use this to return something?

Setting Upwardly the Ray Marching Loop

In order to render our scene, we are going to apply a technique chosen ray marching. At a high level, we will shoot out a bunch of imaginary rays from a virtual camera that is looking at our globe. For each of these rays, we are going to "march" forth the direction of the ray, and at each step, evaluate our SDF (the distance_from_sphere function that nosotros wrote above). This will tell the states: "from where I currently stand up, how far am I from the closest betoken on the surface of our sphere?"

If yous are familiar with ray tracing, you might wonder why we tin can't just direct compute the indicate of intersection between our ray and our sphere. For this simple scene, we really could do this! Still, every bit we will come across towards the finish of this tutorial, the real power of ray marching lies in its ability to render shapes where this verbal betoken of intersection is not as obvious.

Now, our goal is to take steps along this ray until we are and so close to an object that we can safely cease. But how should we move along the ray? 1 approach would exist to take small, every bit spaced steps. In GLSL, this might look like:

                              const                float                step_size                =                0                .                1                ;                for                (                int                i                =                0                ;                i                <                NUMBER_OF_STEPS                ;                ++                i                )                {                // Assume that `ro` and `rd` are divers elsewhere                // and represent the ray's origin and management,                                // respectively                vec3                current_position                =                ro                +                (                i                *                step_size                )                *                rd                ;                // Some lawmaking to evaluate our SDF and determine whether or non                                // we've hit a surface based on our current position...                }                          

It turns out that a much better approach is to apply distance-aided ray marching (sometimes called sphere tracing for reasons that we volition see shortly). At each position forth our ray, nosotros evaluate our SDF, which, by definition, gives us the altitude to the closest object in our scene. If we care for this altitude as the radius of a sphere centered around our current position, we know that nosotros tin safely movement forward along our ray by that amount without overshooting or missing any objects. These "bounding spheres" are represented past the four gray, stroked circles in the diagram below. Note that in do, we might step many more than times before terminating the ray.

The diagram beneath shows this process in activeness. Our starting time scene will be much simpler than this, simply for illustration purposes, it'southward helpful to visualize a more complex shape. Here, we imagine that we accept a top-downwards view of our earth (we are viewing the XZ-plane). The dark gray box towards the bottom of the paradigm represents our virtual camera, and the cerise line emanating from its lens represents a single ray. The green, mountain-like structure surrounding the camera represents some circuitous object (we assume that we have a SDF that represents this shape) that we are trying to render. Each position where we evaluate our SDF is represented by a minor, blue circumvolve. These points would exist evaluated in the gild drawn below, lesser to meridian. Each red X represents the bespeak on the surface that is closest to the corresponding blue circle along the ray.

There are two large optimizations that nosotros tin add to this. The first was already mentioned: if our SDF returns a very small distance (on the order of 0.001 for example), we will consider this a "hit" and break out of the loop early. The 2nd involves rays that never pass through any objects in our scene. In our ray marching routine, we tin keep runway of the full altitude traveled thus far and pause out of the loop if we striking a certain threshold (say, 1000.0 units). And so, our complete ray marching role might look something similar:

                              vec3                ray_march                (                in                vec3                ro                ,                in                vec3                rd                )                {                float                total_distance_traveled                =                0                .                0                ;                const                int                NUMBER_OF_STEPS                =                32                ;                const                bladder                MINIMUM_HIT_DISTANCE                =                0                .                001                ;                const                float                MAXIMUM_TRACE_DISTANCE                =                k                .                0                ;                for                (                int                i                =                0                ;                i                <                NUMBER_OF_STEPS                ;                ++                i                )                {                // Summate our current position along the ray                vec3                current_position                =                ro                +                total_distance_traveled                *                rd                ;                // We wrote this function earlier in the tutorial -                // presume that the sphere is centered at the origin                // and has unit radius                float                distance_to_closest                =                distance_from_sphere                (                current_position                ,                vec3                (                0                .                0                ),                1                .                0                );                if                (                distance_to_closest                <                MINIMUM_HIT_DISTANCE                )                // hitting                {                // We hit something! Return ruddy for at present                render                vec3                (                1                .                0                ,                0                .                0                ,                0                .                0                );                }                if                (                total_distance_traveled                >                MAXIMUM_TRACE_DISTANCE                )                // miss                {                break                ;                }                // accumulate the distance traveled thus far                total_distance_traveled                +=                distance_to_closest                ;                }                // If we get here, we didn't hit annihilation and so just                // return a groundwork color (black)                return                vec3                (                0                .                0                );                }                          

So, nosotros have a function that performs ray marching along a given ray. The last thing nosotros need to figure out is, how practice nosotros generate our rays?

Generating Rays

We know that a ray has two components: an origin and a direction. We are going to imagine that each ray starts at the photographic camera and passes through an imaginary "image plane" that sits somewhere in front of our camera. Call back that all of our code will execute inside of a unmarried fragment shader, so at that place is a flake of a bound, where we somehow have to depict a 3D world from the 2D plane that our fragment shader executes over. A fragment shader executes in one case for each pixel that makes up our final, rendered image. At each pixel location, we tin derive a UV-coordinate in the range [0.0, 1.0]. In TouchDesigner, this is passed from the vertex shader to the fragment shader as the variable vUV.st.

If you are post-obit forth with this tutorial and not using TouchDesigner, the specifics of where your UV-coordinates come from volition be implementation divers. Regardless of what framework / toolkit yous're using, you merely need to exist able to draw a full-screen quad and summate UV-coordinates, either past normalizing the congenital-in variable gl_FragCoord.xy, or otherwise. Too, if you are rendering an image that is non foursquare, you will demand to accept into account your aspect ratio when deriving your UV-coordinates. For now, I'll get out this as an exercise…

Next, let'southward remap our UV-coordinates from the range [0.0, 1.0] to [-1.0, ane.0]. We do this largely for convenience, since it places the pixel at the center of our image at (0.0, 0.0). Now, in our imaginary 3D space, let'due south say that the camera is 5 units away from the origin, in the negative Z management. To go on things simple, we aren't going to deal with things like FOV, photographic camera rotation, etc., but nosotros volition likely cover some of these topics in the next tutorial.

We already know that each ray originates from the camera. Information technology's direction vector can be idea of equally tracing a line from the camera's position through a signal on the image plane. The post-obit GLSL code snippet shows this process in action:

                              // TouchDesigner provides this variable for us                vec2                uv                =                vUV                .                st                *                2                .                0                -                1                .                0                ;                vec3                camera_position                =                vec3                (                0                .                0                ,                0                .                0                ,                -                5                .                0                );                vec3                ro                =                camera_position                ;                vec3                rd                =                vec3                (                uv                ,                1                .                0                );                          

The Z-coordinate of rd acts sort of like the photographic camera'due south FOV, pushing the image plane closer to or further away from the camera (you can endeavour adjusting this after to see the outcome). For now, we will leave this at 1.0.

So, since each fragment has unique UV-coordinates, each execution of our fragment shader will generate a unique ray. Keep in mind that considering of the fashion shaders execute on your graphics card, all of these calculations will be happening in parallel!

At present that we have a style to generate a unique ray at each pixel, we are (finally) ready to ray march!

Putting It All Together

We now have all of the pieces nosotros need to write our beginning, complete ray marching shader! At each pixel location, nosotros will generate a ray, which nosotros volition use inside of our ray marching routine to map out our 3D environment. If you lot've been following along, the complete shader should look something like:

                              out                vec4                o_color                ;                float                distance_from_sphere                (                in                vec3                p                ,                in                vec3                c                ,                float                r                )                {                return                length                (                p                -                c                )                -                r                ;                }                vec3                ray_march                (                in                vec3                ro                ,                in                vec3                rd                )                {                float                total_distance_traveled                =                0                .                0                ;                const                int                NUMBER_OF_STEPS                =                32                ;                const                float                MINIMUM_HIT_DISTANCE                =                0                .                001                ;                const                float                MAXIMUM_TRACE_DISTANCE                =                1000                .                0                ;                for                (                int                i                =                0                ;                i                <                NUMBER_OF_STEPS                ;                ++                i                )                {                vec3                current_position                =                ro                +                total_distance_traveled                *                rd                ;                float                distance_to_closest                =                distance_from_sphere                (                current_position                ,                vec3                (                0                .                0                ),                1                .                0                );                if                (                distance_to_closest                <                MINIMUM_HIT_DISTANCE                )                {                return                vec3                (                1                .                0                ,                0                .                0                ,                0                .                0                );                }                if                (                total_distance_traveled                >                MAXIMUM_TRACE_DISTANCE                )                {                break                ;                }                total_distance_traveled                +=                distance_to_closest                ;                }                return                vec3                (                0                .                0                );                }                void                main                ()                {                vec2                uv                =                vUV                .                st                *                2                .                0                -                1                .                0                ;                vec3                camera_position                =                vec3                (                0                .                0                ,                0                .                0                ,                -                five                .                0                );                vec3                ro                =                camera_position                ;                vec3                rd                =                vec3                (                uv                ,                1                .                0                );                vec3                shaded_color                =                ray_march                (                ro                ,                rd                );                o_color                =                vec4                (                shaded_color                ,                1                .                0                );                }                          

Shading

At present that nosotros have our sphere, permit's try to calculate some basic shading so that we can confirm that it is, indeed, a 3D surface! If you are familiar with lengthened / specular lighting, y'all probably know that we demand normal vectors to calculate shading. For a sphere, the normal at any point on the surface tin can be calculated past simply normalizing the vector c - p, where as before, c is the sphere'due south centre, and p is a point on the surface of the sphere. However, this method is limiting (it certainly doesn't extend to other shapes), and as nosotros will come across in the adjacent department, when nosotros deform our SDF, we demand a more dynamic style of calculating normals.

The idea is, we can "nudge" our point p slightly in the positive and negative direction along each of the 10/Y/Z axes, recalculate our SDF, and see how the values alter. If you are familiar with vector calculus, nosotros are essentially computing the slope of the distance field at p. In 2nd, you might be familiar with the derivative, which gives the rate of change of a role with respect to its input. You might likewise have seen this visualized equally the slope of the line that lies tangent to the function at some indicate. The gradient is just the extension of this to functions of multiple dimensions (our SDF has 3 dimensions, 10/Y/Z). Normals should generally exist unit of measurement vectors, and then we'll normalize it too. This method lets united states of america to calculate normals for arbitrarily complex objects, provided nosotros have the appropriate SDF to correspond its surface.

I realize this explanation is a bit "hand-wavy" at the moment! It took me a while to understand how and why this manner of computing normals works. I recommend sitting with the lawmaking for a bit - afterward some reflection, it should get-go to make sense. I am working on a better way to explicate / illustrate this and volition hopefully update this postal service in the futurity with a more concrete explanation.

Before we summate normals, allow's apace write a function that volition allow us to accommodate more than than ane shape in preparation for time to come posts:

                              float                map_the_world                (                in                vec3                p                )                {                float                sphere_0                =                distance_from_sphere                (                p                ,                vec3                (                0                .                0                ),                ane                .                0                );                // Later nosotros might have sphere_1, sphere_2, cube_3, etc...                return                sphere_0                ;                }                          

And alter the ray marching loop to call this function instead of the distance_from_sphere function straight:

                              ...                float                distance_to_closest                =                map_the_world                (                current_position                );                ...                          

At present, we volition write our function to summate normals, which will call map_the_world six times:

                              vec3                calculate_normal                (                in                vec3                p                )                {                const                vec3                small_step                =                vec3                (                0                .                001                ,                0                .                0                ,                0                .                0                );                float                gradient_x                =                map_the_world                (                p                +                small_step                .                xyy                )                -                map_the_world                (                p                -                small_step                .                xyy                );                bladder                gradient_y                =                map_the_world                (                p                +                small_step                .                yxy                )                -                map_the_world                (                p                -                small_step                .                yxy                );                bladder                gradient_z                =                map_the_world                (                p                +                small_step                .                yyx                )                -                map_the_world                (                p                -                small_step                .                yyx                );                vec3                normal                =                vec3                (                gradient_x                ,                gradient_y                ,                gradient_z                );                return                normalize                (                normal                );                }                          

If y'all are unfamiliar with swizzling in GLSL, we are basically using some syntactic "sugar" to add and subtract the 10-coordinate of the variable small_step (which is 0.001) to each of the 10/Y/Z coordinates of our original point p in succession. So the value of gradient_y, for case, is calculated past adding and subtracting 0.001 from merely the Y-coordinate of p, then calling map_the_world at these two new points. Now, back inside of our ray marching loop, if we striking an object, we can calculate the normal at that point. Let's visualize our normals every bit RGB colors to verify that the code is working every bit expected:

                              ...                if                (                distance_to_closest                <                MINIMUM_HIT_DISTANCE                )                {                vec3                normal                =                calculate_normal                (                current_position                );                // Remember, each component of the normal will be in                                // the range -1..1, then for the purposes of visualizing                // it as an RGB colour, permit'south remap it to the range                // 0..1                return                normal                *                0                .                5                +                0                .                v                ;                }                ...                          

Nosotros can have this one step further and summate some simple diffuse lighting:

                              ...                if                (                distance_to_closest                <                MINIMUM_HIT_DISTANCE                )                {                vec3                normal                =                calculate_normal                (                current_position                );                // For now, hard-lawmaking the light's position in our scene                vec3                light_position                =                vec3                (                2                .                0                ,                -                5                .                0                ,                iii                .                0                );                // Calculate the unit management vector that points from                // the point of intersection to the lite source                vec3                direction_to_light                =                normalize                (                current_position                -                light_position                );                float                diffuse_intensity                =                max                (                0                .                0                ,                dot                (                normal                ,                direction_to_light                ));                return                vec3                (                1                .                0                ,                0                .                0                ,                0                .                0                )                *                diffuse_intensity                ;                }                ...                          

So, your consummate shader should wait something like:

                              out                vec4                o_color                ;                float                distance_from_sphere                (                in                vec3                p                ,                in                vec3                c                ,                float                r                )                {                return                length                (                p                -                c                )                -                r                ;                }                float                map_the_world                (                in                vec3                p                )                {                bladder                sphere_0                =                distance_from_sphere                (                p                ,                vec3                (                0                .                0                ),                1                .                0                );                render                sphere_0                ;                }                vec3                calculate_normal                (                in                vec3                p                )                {                const                vec3                small_step                =                vec3                (                0                .                001                ,                0                .                0                ,                0                .                0                );                bladder                gradient_x                =                map_the_world                (                p                +                small_step                .                xyy                )                -                map_the_world                (                p                -                small_step                .                xyy                );                bladder                gradient_y                =                map_the_world                (                p                +                small_step                .                yxy                )                -                map_the_world                (                p                -                small_step                .                yxy                );                float                gradient_z                =                map_the_world                (                p                +                small_step                .                yyx                )                -                map_the_world                (                p                -                small_step                .                yyx                );                vec3                normal                =                vec3                (                gradient_x                ,                gradient_y                ,                gradient_z                );                return                normalize                (                normal                );                }                vec3                ray_march                (                in                vec3                ro                ,                in                vec3                rd                )                {                bladder                total_distance_traveled                =                0                .                0                ;                const                int                NUMBER_OF_STEPS                =                32                ;                const                float                MINIMUM_HIT_DISTANCE                =                0                .                001                ;                const                float                MAXIMUM_TRACE_DISTANCE                =                m                .                0                ;                for                (                int                i                =                0                ;                i                <                NUMBER_OF_STEPS                ;                ++                i                )                {                vec3                current_position                =                ro                +                total_distance_traveled                *                rd                ;                bladder                distance_to_closest                =                map_the_world                (                current_position                );                if                (                distance_to_closest                <                MINIMUM_HIT_DISTANCE                )                {                vec3                normal                =                calculate_normal                (                current_position                );                vec3                light_position                =                vec3                (                two                .                0                ,                -                5                .                0                ,                3                .                0                );                vec3                direction_to_light                =                normalize                (                current_position                -                light_position                );                float                diffuse_intensity                =                max                (                0                .                0                ,                dot                (                normal                ,                direction_to_light                ));                return                vec3                (                1                .                0                ,                0                .                0                ,                0                .                0                )                *                diffuse_intensity                ;                }                if                (                total_distance_traveled                >                MAXIMUM_TRACE_DISTANCE                )                {                intermission                ;                }                total_distance_traveled                +=                distance_to_closest                ;                }                render                vec3                (                0                .                0                );                }                void                primary                ()                {                vec2                uv                =                vUV                .                st                *                two                .                0                -                i                .                0                ;                vec3                camera_position                =                vec3                (                0                .                0                ,                0                .                0                ,                -                v                .                0                );                vec3                ro                =                camera_position                ;                vec3                rd                =                vec3                (                uv                ,                1                .                0                );                vec3                shaded_color                =                ray_march                (                ro                ,                rd                );                o_color                =                vec4                (                shaded_color                ,                ane                .                0                );                }                          

Distorting the Altitude Function

At present, for the cool office! Once we have a basic ray marching setup, nosotros can "nudge" or perturb our distance functions to create more interesting shapes. For instance, we can add together a sinusoidal distortion to our sphere's SDF by modifying the map_the_world role similar so:

                              bladder                map_the_world                (                in                vec3                p                )                {                bladder                displacement                =                sin                (                5                .                0                *                p                .                x                )                *                sin                (                5                .                0                *                p                .                y                )                *                sin                (                5                .                0                *                p                .                z                )                *                0                .                25                ;                float                sphere_0                =                distance_from_sphere                (                p                ,                vec3                (                0                .                0                ),                one                .                0                );                return                sphere_0                +                displacement                ;                }                          

You tin play effectually with this effect: try using other combinations of sin and cos based on the coordinates of the position vector p to generate a displacement value. If you are working with a toolkit that implements GLSL noise functions (or yous want to implement them yourself), you lot tin can effort perturbing p with noise every bit well.

Since nosotros summate our normals dynamically, the lighting should still be right, regardless of how we deform our shape!

Conclusion

Anyways, I hope this helps you get started with ray marching! I realize that the end results aren't quite as stunning equally you might've hoped, but at present that we take a solid framework to build upon, it'south non too much more than work to add things like:

  • Multiple shapes
  • CSG operations (unions, intersections, subtractions, etc.)
  • Animated cameras
  • Shadows and/or ambient occlusion

These are all things that I hope to encompass in future posts! If you want to start exploring on your own, I recommend taking a wait at Inigo Quilez's website for inspiration.

back

waresefuldsider.blogspot.com

Source: https://michaelwalczyk.com/blog-ray-marching.html

0 Response to "Jamey Johnson (Ray Rays Juke Joint) Down in the Hollar"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel