解决方案是将我的像素空间半径转换为世界空间单位,因为 z 缓冲区仍在世界空间中,即使在通过视图投影变换进行变换之后也是如此。这可以通过将像素转换为一个因子 (factor = pixels / screen_size),然后将该因子转换为世界空间单位来完成,这有点复杂——我必须计算给定距离处屏幕的世界空间大小,然后将该系数乘以得到世界单位。如果有人需要,我可以发布相关代码。可能有一种更简单的方法来计算它,但我的大脑总是直接寻找因素。
我在不同距离得到不同结果的原因主要是因为我只是用结果偏移了剪辑位置的 z 分量。还需要偏移 w 组件,以使深度偏移在任何距离(线性)下都有效。但是,为了抵消 w 组件,您首先必须将 xy 缩放 w,根据需要修改 w,然后将 xy 除以新的 w。这导致数学变得非常复杂,所以我改变了在裁剪空间之前偏移顶点的策略,这需要手动计算 Z 空间中到相机的距离,但老实说,无论哪种方式,它最终的数学量大致相同。
这是目前最终的顶点着色器。希望全球价值观有意义。我没有修改它来发布它,所以请原谅我的 cmets 中的任何愚蠢行为:
lerpPoint main(vinBake vin)
{
// prepare output
lerpPoint pin;
// extract radius/size from input
pin.InRadius = vin.TexCoord.y;
// extract alpha falloff from input
pin.Feather = vin.TexCoord.z;
// compute the Z distance of the camera from the vertex
float cam_z_dist = dot( Scene.CamZ, vin.Position.xyz - Scene.CamPos );
// 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 = Scene.CamZ * (radius_world * -4.0);
// generate projected position
// + in this space, xy are sort of in pixel-space, but are undivided by depth
// + the more depth this point has from the camera, the higher xy will become as it travels away from the screen center
// + after this, dividing xy by w will give us clip space, where 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) );
// record clip xy position as center (before we offset it)
pin.Center = pin.ClipPos.xy;
// calculate radius of point, in clip space
// + we scale by inverted resolution (1/width) and 2 to convert our 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 to retain constant scale, but this will give us a VERY LARGE result
// + we scale by inverter resolution 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;
// copy diffuse color
pin.Diffuse = vin.Color;
// return pin data
return pin;
}