CaffeineViking / vkhr

Real-Time Hybrid Hair Rendering using Vulkan™
MIT License
446 stars 34 forks source link

Implement Phone-Wire AA #29

Closed CaffeineViking closed 5 years ago

CaffeineViking commented 5 years ago

After implementing #28 we want to "fix" the aliasing in the rasterizer as well, which mostly happens in areas of low-density. We can solve this by using e.g. Phone-Wire AA, which essentially assumes a line is already a pixel-wide, and then seemingly reduces the thickness by assigning the alpha values of the fragment, relative to the thickness reduction ratio. Emil provides us with a sample implementation of it, which works like this:

// Compute view-space w
float w = dot(ViewProj[3], float4(In.Position.xyz, 1.0f));

// Compute what radius a pixel wide wire would have
float pixel_radius = w * PixelScale;

// Clamp radius to pixel size. Fade with reduction in radius vs original.
float radius = max(actual_radius, pixel_radius);
float fade = actual_radius / radius;

// Compute final position
float3 position = In.Position + radius * normalize(In.Normal);

Maybe we can also get away by just using one pixel clamped lines and then changing the alpha component based on the thickness (which we change in the data itself). i.e. we modulate the alpha of the strand as pre-processing step based on the thickness, where the last 10-15% are interpolated towards 0, for thinning out.

Otherwise, we can use the strand coverage calculation as done in TressFX/TressFXRendering.hls#172 as well:

float ComputeCoverage(float2 p0, float2 p1, float2 pixelLoc, float2 winSize)
{
    // p0, p1, pixelLoc are in d3d clip space (-1 to 1)x(-1 to 1)

    // Scale positions so 1.f = half pixel width
    p0 *= winSize;
    p1 *= winSize;
    pixelLoc *= winSize;

    float p0dist = length(p0 - pixelLoc);
    float p1dist = length(p1 - pixelLoc);
    float hairWidth = length(p0 - p1);

    // will be 1.f if pixel outside hair, 0.f if pixel inside hair
    float outside = any(float2(step(hairWidth, p0dist), step(hairWidth, p1dist)));

    // if outside, set sign to -1, else set sign to 1
    float sign = outside > 0.f ? -1.f : 1.f;

    // signed distance (positive if inside hair, negative if outside hair)
    float relDist = sign * saturate(min(p0dist, p1dist));

    // returns coverage based on the relative distance
    // 0, if completely outside hair edge
    // 1, if completely inside hair edge
    return (relDist + 1.f) * 0.5f;
}
CaffeineViking commented 5 years ago

Instead of Phone-Wire AA (which doesn't solve all the problems we're having), we use something like GPAA but simplified (since we're only rendering lines anyway). Trick is to use gl_FragCoord which tells us the point of the fragment and the interpolated position, which gives us the "center" on the thick line. We just need to make position be in screen-space instead of world-space, and the distance between those is pixel distance from the center of the thick line. I was confused at first, since I was expecting to get "real" fragment positions from position, but that's of course not true, since we're not doing any billboarding, and are using lines instead. The results are as expected, and remove all of the jaggies. I made the thickness configurable, and the opacity which we use for blending too. Increasing thickness is a bad idea in this case, since it will cause more fragments to be inserted into the PPLL, I've found that a value of 2 is good enough for the ponytail.

float gpaa(vec2 screen_fragment,
           vec4 world_line,
           mat4 view_projection,
           vec2 resolution,
           float line_thickness) {
    // Transforms: world -> clip -> ndc -> screen.
    vec4 clip_line = view_projection * world_line;
    vec3 ndc_line = (clip_line.xyz / clip_line.w);
    vec2 screen_line = ndc_line.xy; // really NDC.

    // Transform NDC to screen-space to compare with samples.
    screen_line = (screen_line + 1.0f) * (resolution / 2.0f);

    // Distance is measured in screen-space grids.
    float d = length(screen_line-screen_fragment);

    // Finally the coverage is based on thickness.
    return 1.00f - (d / (line_thickness / 2.00f));
}
Anteru commented 5 years ago

Excellent work, how does it look :) ?