How do I calculate pixel shader depth to render a circle drawn on a point sprite as a sphere that will intersect with other objects?

前端 未结 3 1937
野的像风
野的像风 2020-12-28 09:15

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

相关标签:
3条回答
  • 2020-12-28 09:34

    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.

    The technique

    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.)

    Code

    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...

    Result

    Intersecting spheres using point sprites

    Voila, point sprites drawing spheres.

    Notes

    • The scaling algorithm for the sprites may require the depth to be scaled, too. I am not sure that line is correct.
    • It is not fully mathematically correct (takes shortcuts) but as you can see the result is visually correct
    • When using millions of sprites, I still get a good rendering speed (<10ms per frame for 3 million sprites, on a VMWare Fusion emulated Direct3D device)
    0 讨论(0)
  • 2020-12-28 09:37

    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.

    0 讨论(0)
  • 2020-12-28 09:42

    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.

    0 讨论(0)
提交回复
热议问题