Somewhat surprisingly, even in an age of retina displays aliasing is still an issue. Writing the stripe light shader was easy enough but creating an anti-aliased version took some research. It’s actually quite easy once you get your head around the principle.
Basically the completed stripe shader is a combination of a reflection shader (as used e.g. in a cube map reflection) and the edge shader from GPU Gems Chapter 24 (example 24-11/12). For each fragment, not only the central value is considered but the integral of the function over the whole size of the fragment (i.e.. the pixel on screen). The example uses shader derivates to approximate the width of the interval and then calculate the start and end value. The result is not perfect but a nice boost in image quality:
Shader Source
Here is a rough version of a Metal-based stripe light shader version. I’ve annotated it but it’s not really optimized for rendering speed or anything. I just dumped it here in case anyone else needs a starting point.
struct VertexOutput
{
float4 position [[position]];
float3 positionModelSpace;
float3 normalModelSpace;
};
vertex VertexOutput VertexShader(device VertexInput * vertexData [[buffer(0)]],
constant TransformationUniforms &transformation [[buffer(1)]],
uint vid [[vertex_id]])
{
// The vertex shader calculates the clip space vertex position for the
// rasterizer but otherwise just passes the model/object space position
// and normal to the fragment shader.
VertexOutput output;
VertexInput vData = vertexData[vid];
output.position = transformation.MVP * float4(vData.position, 1);
output.positionModelSpace = vData.position;
output.normalModelSpace = vData.normal;
return output;
}
fragment float4 FragmentShader(VertexOutput in [[stage_in]],
constant TransformationUniforms &transformation [[buffer(0)]])
{
// Compute position/normal in world space. Then reflect the vector from the
// viewer to the surface point. This is typical cubemap reflection stuff.
float3 positionWorldspace = (transformation.M * float4(in.positionModelSpace, 1)).xyz;
float3 normalWorldspace = normalize((transformation.M_inverse_transposed * float4(in.normalModelSpace, 0)).xyz);
float3 eye = (transformation.V_inverse * float4(0, 0, 0, 1)).xyz;
float3 I = positionWorldspace - eye;
float3 R = reflect(I, normalWorldspace);
// Project the normal to the X/Y plane (we assume our stripe lights are
// oriented at the Z-direction), re-normalize and compute the angle to the
// y-axis (in [0,M_PI]).
float3 projectedNormal = normalize(float3(R.x, R.y, 0.0));
float angle = acos(projectedNormal.y);
// Scale such that we have 50 white stripes. This as well as the stripe
// orientation would usually be a shader parameter.
float scale = 25.0 / 3.141592653589793;
// Compute the start and end value of "angle" along the fragment. fwidth
// computes fabs(dfdx(angle)) + fabs(dfdy(angle)) and we simply go half of
// the interval back for the start and half forward for the end.
Shift by
// 0.25 such that we don't have a double-width black line at the top.
float intervalWidth = fwidth(angle) * scale;
float xStart = angle * scale - intervalWidth / 2.0 + 0.25;
float xEnd = xStart + intervalWidth;
// Calculate the amount of stripes that have been traveled coming from 0.
float iStart = 0.5 * floor(xStart) + max(0.0, fract(xStart) - 0.5);
float iEnd = 0.5 * floor(xEnd) + max(0.0, fract(xEnd) - 0.5);
// Our final value is the difference scaled by the inverse of the interval.
float value = (iEnd - iStart) / intervalWidth;
// Simply return as gray value.
return float4(value, value, value, 1.0);
}
One Comment
Matt Ostgard
Very helpful thank you.