Polygons & textures creeping in my raymarcher

This week I’ve been working on additional features in my 64k toolchain. None of this is yet viable for 64k executables but it enhances the tool quite a bit.

My first step was implementing vertex shader support. A cool thing about vertex shaders in openGL is that they are responsible for outputting the vertex data, nobody said anything about requiring input. So with a function like glDrawArraysInstanced, we have full reign in the vertex shader to generate points based on gl_VertexID and gl_InstanceID.

Here I’m generating a grid of 10×10 quads, added some barycentric coordinates as per This article

#version 420

uniform mat4 uV;
uniform mat4 uVi;
uniform mat4 uP;

out vec3 bary;
out vec2 uv;

void main()
{
    vec3 local = vec3(gl_VertexID % 2, gl_VertexID / 2, 0.5) - 0.5;
    vec3 global = vec3(gl_InstanceID % 10, gl_InstanceID / 10, 4.5) - 4.5;

    uv = (local + global).xy * vec2(0.1, 0.1 * 16 / 9) + 0.5;

    bary = vec3(0);
    bary[gl_VertexID % 3] = 1.0;

    gl_Position = uP * vec4(mat3(uVi) * ((local + global - uV[3].xyz) * vec3(1,1,-1)), 1);
}

This was surprisingly easy to implement. In the tool I scan a template definition XML to figure out which shader source files to stitch together and treat as 1 fragment shader. Adding the distinction between .frag and .vert files allowed me to compile the resulting program with a different vertex shader than the default one and it was up and running quite fast.

Next came a more interesting bit, mixing my raymarching things together with this polygonal grid.
There are 2 bits to this, one is matching the projection, two is depth testing, and thus matching the depth output from the raymarcher.

To project a vertex I subtract the ray origin from the vertex and then multiply it by the inverse rotation. Apparantly that flips the Z axis so I had to fix that too. Then I multiply that with the projection matrix. the “u” prefix means uniform variable.

vec4 viewCoord = vec4(uViewInverse * ((vertex - uRayOrigin) * vec3(1,1-1)), 1)

My ray direction is based on mixing the corners of a frustum these days, I used to rotate the ray to get a fisheye effect but that doesn’t fly with regular projection matrices. My frustum calculation looks something like this (before going into the shader as a mat4):

tanFov = tan(uniforms.get('uFovBias', 0.5))
horizontalFov = (tanFov * aspectRatio)
uniforms['uFrustum'] = (-horizontalFov, -tanFov, 1.0, 0.0,
                        horizontalFov, -tanFov, 1.0, 0.0,
                        -horizontalFov, tanFov, 1.0, 0.0,
                        horizontalFov, tanFov, 1.0, 0.0)

So I can get a projection matrix from that as well. Additionally I added a uniform for the clipRange so the raymarcher near/far planes match the polygonal ones.

uniforms['uClipRange'] = (0.01, 100.0)
near, far = uniforms['uClipRange']
projection = Mat44.frustum(-xfov * near, xfov * near, -tfov * near, tfov * near, near, far)

For reference my raymarching ray looks like this:

vec4 d = mix(mix(uFrustum[0], uFrustum[1], uv.x), mix(uFrustum[2], uFrustum[3],uv.x), uv.y);
Ray ray = Ray(uV[3].xyz, normalize(d.xyz * mat3(uV)));

With this raymarching a 10x10x0.01 box matches up perfectly with the polygonal plane on top! Then the next issue is depth testing. All my render targets are now equipped with a float32 depth buffer, depth testing is enabled and before every frame I clear all depth buffers. Now I find my grid on top of my test scene because the raymarcher does not yet write the depth.

Following this nice article I learned a laot about this topic.
So to get the distance along Z I first define the world-space view axis (0,0,-1). Dotting that with the (intersection – rayOrigin), which is the same as totalDistance * raydirection, yield the right eye-space Z distance. The rest is explained in the article. It is pretty straight forward to map the Z using the clipping planes previously defined to match gl_DepthRange. I first fit between a 01 range (ndcDepth) and then fit back to gl_depthRange. One final trick is to fade to the FAR depth if we have 100% fog.

    vec3 viewForward = vec3(0.0, 0.0, -1.0) * mat3(uV);
    float eyeHitZ = hit.totalDistance * dot(ray.direction, viewForward);
    float ndcDepth = ((uClipRange.y + uClipRange.x) + (2 * uClipRange.y * uClipRange.x) / eyeHitZ) / (uClipRange.y - uClipRange.x);
    float z = ((gl_DepthRange.diff * ndcDepth) + gl_DepthRange.near + gl_DepthRange.far) / 2.0;
    gl_FragDepth = mix(z, gl_DepthRange.far, step(0.999, outColor0.w));

Now as if that wasn’t enough cool stuff, I added the option to bind an image file to a shot. Whenever a shot gets drawn it’s texture list is queried, uploaded and bound to the user defined uniform names. Uploading is cached so every texture is loaded only once, I should probably add file watchers… The cool thing here is that not only can I now texture things, I can also enter storyboards and time them before working on actual 3D scenes!

Leave a Reply

Your email address will not be published. Required fields are marked *