I am trying to render an infinite world floor grid, similar to this question.
My project is using SceneKit with Metal Shading Language but the concepts are no doubt similar between GLSL/HLSL.
Here is an example project where I attempt to render a plane as a full screen quad with the grid lines drawn using a fragment shader.
Full shader code can be found here.
Grid Rendering
A simple vertex shader sets the position and grid coordinate for the plane:
f.position = scn_node.modelViewProjectionTransform * float4(v.position, 1.0);
f.ij = v.position.xy;
The fragment shader then determines which color to render the grid or floor:
fragment half4 floor_fragment(FragmentIn f [[stage_in]]) {
float2 fractional = abs(fract(f.ij; + 0.5));
float2 partial = fwidth(f.ij;);
float2 point = smoothstep(-partial, partial, fractional);
float saturation = 1.0 - saturate(point.x * point.y);
return half4(mix(backgroundColor.rgb, gridColor.rgb, saturation), 1.0);
}
I then remove any MVP transforms and simply set the vertex position as is in clip space to fill the screen. This does have the intended effect of filling the screen but mangles the grid lines.
//inside floor_vertex()
f.position = float4(v.position, 1.0);
f.coordinate = v.position.xy;
Ray Casting
As suggested in the comments, the fragment position should be projected into world space and intersected with the floor plane to find a new vector on the plane. I have an intersect(plane:ray)
method returns a hit with a distance and a point on the plane or otherwise false if the ray is orthogonal to the plane.
It is my understanding that by multiplying the position in clip space with the inverseViewProjectionTransform
the result will be in camera space. The plane is also converted to camera space with the modelViewTransform
and I then test my ray against this which appears to work but with incorrect results.
fragment half4 floor_fragment(Fragment f [[stage_in]], constant SCNSceneBuffer& scn_frame [[buffer(0)]], constant NodeBuffer& scn_node [[buffer(1)]]) {
//f.ij is in the vertex position in clip space (-1, -1) to (1, 1)
//convert position into camera space
float4 position = (scn_frame.inverseViewProjectionTransform * float4(f.ij.x, f.ij.y, 0.0, 1.0));
//create ray from camera with direction
Ray ray = Ray { .origin = float3(0.0), .direction = normalize(position.xyz) };
//convert floor plane from world space to camera space
float3 worldFloor = (scn_node.modelViewTransform * float4(float3(0.0, -5.0, 0.0), 1.0)).xyz;
//hit test ray against floor plane
Plane plane = Plane { .position = worldFloor, .normal = float3(0.0, 1.0, 0.0) };
RayHitTest hitTest = intersect(plane, ray);
if(!hitTest.hit) {
return f.backgroundColor;
}
//grab xz values to determine floor fragment color
float2 uv = hitTest.vector.xz;
float2 fractional = abs(fract(uv + 0.5));
float2 partial = fwidth(uv);
float2 point = smoothstep(-partial, partial, fractional);
float saturation = 1.0 - saturate(point.x * point.y);
return half4(mix(f.backgroundColor.rgb, f.gridColor.rgb, saturation), 1.0);
}
This video shows the final results where you can see that the grid is not correctly aligned with the world axis. The floor plane is positioned at 0.0, -5.0, 0.0
and the rectangle at 0.0, -4.5, 0.0
which should be aligned with each other but this is not the case. It looks to me that the perspective is off as the grid seems much larger than is should be.
Can anyone please point out the obvious mistake(s) I am making?