Pixel space depth offset in vertex shader

290 Views Asked by At

I'm trying to draw simple scaled points in my custom graphics engine. The points are scaled in pixel space, and the radius of the points are in pixels, but the position of the points fed to the draw function are in world coordinates.

So far, everything is working great, except for a depth clipping issue. The points are of constant size, regardless of how far away they are, which is done by offsetting the vertices in projected/clip space. However, when they are close to surfaces, they partially intersect them in the depth buffer.

Since these points represent world coordinates, I want them to use the depth buffer, and be hidden behind objects that are in front of them. However, when the point is close to a surface, I want to push it toward the camera, so it doesn't partially intersect it. I think it is easier to just always do this push, regardless of the point being close to a surface. What makes the most sense to me is to just push it by its radius, so that all of its vertices are exactly far enough away to avoid clipping into nearby surfaces.

The easiest way I've found to do this is to simply subtract from the Z value in the vertex shader, after transforming into view-projection space. However, I'm having some trouble converting my pixel radius into a depth offset. Regardless of the math I use, what works close up never seems to work far away. I'm thinking maybe this is due to how the z buffer is non-linear, but could be wrong.

Currently, the closest I've been to solving this is the following:

proj_vertex_pos.z -= point_pixel_radius / proj_vertex_pos.w * 100.0

I'm honestly not sure why 100.0 helps make this work yet. I added it simply because dividing the radius by w was too small of a value. Can anyone point me in the right direction? How do I convert my pixel distance into a depth distance? Especially if the depth distance changes scale depending on which depth you are at? Or am I just way off?

1

There are 1 best solutions below

0
Robert On

The solution was to convert my pixel space radius into world space units, since the z-buffer is still in world space, even after transforming by the view-projection transform. This can be done by converting pixels into a factor (factor = pixels / screen_size), then convert the factor into world space units, which was a little more involved - I had to calculate the world-space size of the screen at a given distance, then multiply the factor by that to get world units. I can post the related code if anyone needs it. There's probably a simpler way to calculate it, but my brain always goes straight for factors.

The reason I was getting different results at different distances was mainly because I was only offsetting the z component of the clip position by the result. It's also necessary to offset the w component, to make the depth offset work at any distance (linear). However, in order to offset the w component, you first have to scale xy by w, modify w as needed, then divide xy by the new w. This resulted in making the math pretty involved, so I changed the strategy to offset the vertex before clip space, which requires calculating the distance to the camera in Z space manually, but it honestly ended up being about the same amount of math either way.

Here is the final vertex shader at the moment. Hopefully the global values make sense. I did not modify this to post it, so please forgive any sillyness in my comments. EDIT: I had to make some edits to this, because I was accidentally moving the vertex along the camera-Z direction instead of directly toward the camera:

lerpPoint main(vinBake vin)
{
    // prepare output
    lerpPoint pin;
    
    // extract radius/size from input
    pin.InRadius = vin.TexCoord.y;

    // compute offset from vertex to camera
    float3 to_cam_offset = Scene.CamPos - vin.Position.xyz;
    
    // compute the Z distance of the camera from the vertex
    float cam_z_dist = -dot( Scene.CamZ, to_cam_offset );
    
    // compute the radius factor
    // + this describes what percentage of the screen is covered by our radius
    // + this removes it from pixel space into factor-space
    float radius_fac = Scene.InvScreenRes.x * pin.InRadius;
    
    // compute world-space radius by scaling with FieldFactor
    // + FieldFactor.x represents the world-space-width of the camera view at whatever distance we scale it by
    // + here, we scale FieldFactor.x by the camera z distance, which gives us the world radius, in world units
    // + we must multiply by 2 because FieldFactor.x only represents HALF of the screen
    float radius_world = radius_fac * Scene.FieldFactor.x * cam_z_dist * 2.0;
    
    // finally, push the vertex toward the camera by the world radius
    // + note: moving by radius will only work with surfaces facing the camera, since we are moving toward the camera, rather than away from the surface
    // + because of this, we also multiply by another 4, to compensate for nearby surface angles, but there is no scale that would work for every angle
    float3 offset = normalize(to_cam_offset) * (radius_world * -4.0);
    
    // generate projected position
    // + after this, x=-1 is left, x=+1 is right, y=-1 is bottom, and y=+1 is top of screen
    // + note that after this transform, w represents "distance from camera", and z represents "distance from near plane", both in world space
    pin.ClipPos = mul( Scene.ViewProj, float4( vin.Position.xyz + offset, 1.0) );
    
    // calculate radius of point, in clip space from our radius factor
    // + we scale by 2 to convert pixel radius into clip-radius
    float clip_radius = radius_fac * 2.0 * pin.ClipPos.w;
    
    // compute scaled clip-space offset and apply it to our clip-position
    // + vin.Prop.xy: -1,-1 = bottom-left, -1,1 = top left, 1,-1 = bottom right, 1,1 = top right (note: in clip-space, +1 = top, -1 = bottom)
    // + we scale by clipping depth (part of clip_radius) to retain constant scale, but this will give us a VERY LARGE result
    // + we scale by inverter resolution (clip_radius) to convert our input screen scale (eg, 1->1024) into a clip scale (eg, 0.001 to 1.0 )
    pin.ClipPos.x += vin.Prop.x * clip_radius;
    pin.ClipPos.y += vin.Prop.y * clip_radius * Scene.Aspect;

    // return result
    return pin;
}

Here is the other version that offsets z & w instead of changing things in world space. After edits above, this is probably the more optimal solution:

lerpPoint main(vinBake vin)
{
    // prepare output
    lerpPoint pin;
    
    // extract radius/size from input
    pin.InRadius = vin.TexCoord.y;
    
    // generate projected position
    // + after this, x=-1 is left, x=+1 is right, y=-1 is bottom, and y=+1 is top of screen
    // + note that after this transform, w represents "distance from camera", and z represents "distance from near plane", both in world space
    pin.ClipPos = mul( Scene.ViewProj, float4( vin.Position.xyz, 1.0) );
    
    // compute the radius factor
    // + this describes what percentage of the screen is covered by our radius
    // + this removes it from pixel space into factor-space
    float radius_fac = Scene.InvScreenRes.x * pin.InRadius;
    
    // compute world-space radius by scaling with FieldFactor
    // + FieldFactor.x represents the world-space-width of the camera view at whatever distance we scale it by
    // + here, we scale FieldFactor.x by the camera z distance, which gives us the world radius, in world units
    // + we must multiply by 2 because FieldFactor.x only represents HALF of the screen
    float radius_world = radius_fac * Scene.FieldFactor.x * pin.ClipPos.w * 2.0;
    
    // offset depth by our world radius
    // + we scale this extra to compensate for surfaces with high angles relative to the camera (since we are moving directly at it)
    // + notice we have to make the perspective divide before modifying w, then re-apply it after, or xy will be off
    pin.ClipPos.xy /= pin.ClipPos.w;
    pin.ClipPos.z -= radius_world * 10.0;
    pin.ClipPos.w -= radius_world * 10.0;
    pin.ClipPos.xy *= pin.ClipPos.w;
    
    // calculate radius of point, in clip space from our radius factor
    // + we scale by 2 to convert pixel radius into clip-radius
    float clip_radius = radius_fac * 2.0 * pin.ClipPos.w;
    
    // compute scaled clip-space offset and apply it to our clip-position
    // + vin.Prop.xy: -1,-1 = bottom-left, -1,1 = top left, 1,-1 = bottom right, 1,1 = top right (note: in clip-space, +1 = top, -1 = bottom)
    // + we scale by clipping depth (part of clip_radius) to retain constant scale, but this will give us a VERY LARGE result
    // + we scale by inverter resolution (clip_radius) to convert our input screen scale (eg, 1->1024) into a clip scale (eg, 0.001 to 1.0 )
    pin.ClipPos.x += vin.Prop.x * clip_radius;
    pin.ClipPos.y += vin.Prop.y * clip_radius * Scene.Aspect;
    
    // return result
    return pin;
}