I am writing a shader to render spheres on point sprites, by drawing shaded circles, and need to write a depth component as well as colour in order that spheres near each ot
I came up with a solution yesterday, which which works well and and produces the desired result of a sphere drawn on the sprite, with a correct depth value which intersects with other objects and spheres in the scene. It may be less efficient than it needs to be (it calculates and projects two vertices per sprite, for example) and is probably not fully correct mathematically (it takes shortcuts), but it produces visually good results.
In order to write out the depth of the 'sphere', you need to calculate the radius of the sphere in depth coordinates - i.e., how thick half the sphere is. This amount can then be scaled as you write out each pixel on the sphere by how far from the centre of the sphere you are.
To calculate the radius in depth coordinates:
Vertex shader: in unprojected scene coordinates cast a ray from the eye through the sphere centre (that is, the vertex that represents the point sprite) and add the radius of the sphere. This gives you a point lying on the surface of the sphere. Project both the sprite vertex and your new sphere surface vertex, and calculate depth (z/w) for each. The different is the depth value you need.
Pixel Shader: to draw a circle you already calculate a normalised distance from the centre of the sprite, using clip
to not draw pixels outside the circle. Since it's normalised (0-1), multiply this by the sphere depth (which is the depth value of the radius, i.e. the pixel at the centre of the sphere) and add to the depth of the flat sprite itself. This gives a depth thickest at the sphere centre to 0 and the edge, following the surface of the sphere. (Depending on how accurate you need it, use a cosine to get a curved thickness. I found linear gave perfectly fine-looking results.)
This is not full code since my effects are for my company, but the code here is rewritten from my actual effect file omitting unnecessary / proprietary stuff, and should be complete enough to demonstrate the technique.
Vertex shader
void SphereVS(float4 vPos // Input vertex,
float fPointRadius, // Radius of circle / sphere in world coords
out float fDXScale, // Result of DirectX algorithm to scale the sprite size
out float fDepth, // Flat sprite depth
out float4 oPos : POSITION0, // Projected sprite position
out float fDiameter : PSIZE, // Sprite size in pixels (DX point sprites are sized in px)
out float fSphereRadiusDepth : TEXCOORDn // Radius of the sphere in depth coords
{
...
// Normal projection
oPos = mul(vPos, g_mWorldViewProj);
// DX depth (of the flat billboarded point sprite)
fDepth = oPos.z / oPos.w;
// Also scale the sprite size - DX specifies a point sprite's size in pixels.
// One (old) algorithm is in http://msdn.microsoft.com/en-us/library/windows/desktop/bb147281(v=vs.85).aspx
fDXScale = ...;
fDiameter = fDXScale * fPointRadius;
// Finally, the key: what's the depth coord to use for the thickness of the sphere?
fSphereRadiusDepth = CalculateSphereDepth(vPos, fPointRadius, fDepth, fDXScale);
...
}
All standard stuff, but I include it to show how it's used.
The key method and the answer to the question is:
float CalculateSphereDepth(float4 vPos, float fPointRadius, float fSphereCenterDepth, float fDXScale) {
// Calculate sphere depth. Do this by calculating a point on the
// far side of the sphere, ie cast a ray from the eye, through the
// point sprite vertex (the sphere center) and extend it by the radius
// of the sphere
// The difference in depths between the sphere center and the sphere
// edge is then used to write out sphere 'depth' on the sprite.
float4 vRayDir = vPos - g_vecEyePos;
float fLength = length(vRayDir);
vRayDir = normalize(vRayDir);
fLength = fLength + vPointRadius; // Distance from eye through sphere center to edge of sphere
float4 oSphereEdgePos = g_vecEyePos + (fLength * vRayDir); // Point on the edge of the sphere
oSphereEdgePos.w = 1.0;
oSphereEdgePos = mul(oSphereEdgePos, g_mWorldViewProj); // Project it
// DX depth calculation of the projected sphere-edge point
const float fSphereEdgeDepth = oSphereEdgePos.z / oSphereEdgePos.w;
float fSphereRadiusDepth = fSphereCenterDepth - fSphereEdgeDepth; // Difference between center and edge of sphere
fSphereRadiusDepth *= fDXScale; // Account for sphere scaling
return fSphereRadiusDepth;
}
Pixel shader
void SpherePS(
...
float fSpriteDepth : TEXCOORD0,
float fSphereRadiusDepth : TEXCOORD1,
out float4 oFragment : COLOR0,
out float fSphereDepth : DEPTH0
)
{
float fCircleDist = ...; // See example code in the question
// 0-1 value from the center of the sprite, use clip to form the sprite into a circle
clip(fCircleDist);
fSphereDepth = fSpriteDepth + (fCircleDist * fSphereRadiusDepth);
// And calculate a pixel color
oFragment = ...; // Add lighting etc here
}
This code omits lighting etc. To calculate how far the pixel is from the centre of the sprite (to get fCircleDist
) see the example code in the question (calculates 'float dist = ...
') which already drew a circle.
The end result is...
Voila, point sprites drawing spheres.
The first big mistake is that a real 3d sphere will not project to a circle under perspective 3d projection.
This is very non intuitive, but look at some pictures, especially with a large field of view and off center spheres.
Second, I would recommend against using point sprites in the beginning, it might make things harder than necessary, especially considering the first point. Just draw a generous bounding quad around your sphere and go from there.
In your shader you should have the screen space position as an input. From that, the view transform, and your projection matrix you can get to a line in eye space. You need to intersect this line with the sphere in eye space (raytracing), get the eye space intersection point, and transform that back to screen space. Then output 1/w as depth. I am not doing the math for you here because I am a bit drunk and lazy and I don't think that's what you really want to do anyway. It's a great exercise in linear algebra though, so maybe you should try it. :)
The effect you are probably trying to do is called Depth Sprites and is usually used only with an orthographic projection and with the depth of a sprite stored in a texture. Just store the depth along with your color for example in the alpha channel and just output eye.z+(storeddepth-.5)*depthofsprite.
Sphere will not project into a circle in general case. Here is the solution.
This technique is called spherical billboards
. An in-depth description can be found in this paper:
Spherical Billboards and their Application to Rendering Explosions
You draw point sprites as quads and then sample a depth texture in order to find the distance between per-pixel Z-value and your current Z-coordinate. The distance between the sampled Z-value and current Z affects the opacity of the pixel to make it look like a sphere while intersecting underlying geometry. Authors of the paper suggest the following code to compute opacity:
float Opacity(float3 P, float3 Q, float r, float2 scr)
{
float alpha = 0;
float d = length(P.xy - Q.xy);
if(d < r) {
float w = sqrt(r*r - d*d);
float F = P.z - w;
float B = P.z + w;
float Zs = tex2D(Depth, scr);
float ds = min(Zs, B) - max(f, F);
alpha = 1 - exp(-tau * (1-d/r) * ds);
}
return alpha;
}
This will prevent sharp intersections of your billboards with the scene geometry.
In case point-sprites pipeline is difficult to control (i can say only about OpenGL and not DirectX) it is better to use GPU-accelerated billboarding: you supply 4 equal 3D vertices that match the center of the particle. Then you move them into the appropriate billboard corners in a vertex shader, i.e:
if ( idx == 0 ) ParticlePos += (-X - Y);
if ( idx == 1 ) ParticlePos += (+X - Y);
if ( idx == 2 ) ParticlePos += (+X + Y);
if ( idx == 3 ) ParticlePos += (-X + Y);
This is more oriented to the modern GPU pipeline and of coarse will work with any nondegenerate perspective projection.