如何计算像素着色器深度以将在点精灵上绘制的圆渲染为与其他对象相交的球体? [英] 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?

查看:18
本文介绍了如何计算像素着色器深度以将在点精灵上绘制的圆渲染为与其他对象相交的球体?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在编写一个着色器,通过绘制阴影圆圈来在点精灵上渲染球体,并且需要编写深度分量和颜色,以便彼此靠近的球体正确相交.

我使用的代码类似于 ,您会看到它是如何成为一个有用的可视化工具.我不想渲染实际的球体网格,因为与简单的硬件生成的点精灵相比,三角形的数量很多.我真的很想使用点精灵的技术,我只是想扩展 现存的演示技术,以便计算正确的深度值,该值在演示中作为变量传入,但没有派生来源.

解决方案

我昨天想出了一个解决方案,它运行良好,并产生了在精灵上绘制的球体的期望结果,具有相交的正确深度值与场景中的其他物体和球体.它可能比它需要的效率低(例如,它计算并投影每个精灵的两个顶点)并且在数学上可能不完全正确(它需要捷径),但它产生了视觉上很好的结果.

技术

为了写出球体"的深度,您需要在深度坐标中计算球体的半径 - 即球体的一半厚.然后,当您在球体上写出每个像素时,可以根据距球体中心的距离来缩放该数量.

在深度坐标中计算半径:

  • 顶点着色器:在未投影的场景坐标中,从眼睛投射一条光线穿过球体中心(即表示点精灵的顶点)并添加球体的半径.这会给你一个位于球体表面的点.投影精灵顶点和新球体表面顶点,并计算每个顶点的深度 (z/w).不同的是您需要的深度值.

  • 像素着色器:要绘制一个圆,您已经计算了距精灵中心的标准化距离,使用 clip 不绘制圆外的像素.由于它已归一化 (0-1),因此将其乘以球体深度(即半径的深度值,即球体中心的像素),并加上平面精灵本身的深度.这使球体中心的深度最厚为 0,边缘跟随球体表面.(根据您需要的精确度,使用余弦来获得弯曲的厚度.我发现 linear 给出了非常漂亮的结果.)

代码

这不是完整的代码,因为我的效果是为我的公司准备的,但这里的代码是根据我的实际效果文件重写的,省略了不必要的/专有的东西,并且应该足够完整以演示该技术.

顶点着色器

void SphereVS(float4 vPos//输入顶点,float fPointRadius,//世界坐标中圆/球体的半径out float fDXScale,//缩放精灵大小的 DirectX 算法结果out float fDepth,//平面精灵深度out float4 oPos : POSITION0,//投影精灵位置out float fDiameter : PSIZE,//以像素为单位的精灵大小(DX 点精灵以像素为单位)out float fSphereRadiusDepth : TEXCOORDn//深度坐标中球体的半径{...//普通投影oPos = mul(vPos, g_mWorldViewProj);//DX 深度(平面广告牌点精灵的)fDepth = oPos.z/oPos.w;//同时缩放精灵大小 - DX 以像素为单位指定点精灵的大小.//一种(旧)算法在 http://msdn.microsoft.com/en-us/library/windows/desktop/bb147281(v=vs.85).aspxfDXScale = ...;fDiameter = fDXScale * fPointRadius;//最后,关键是:用于球体厚度的深度坐标是多少?fSphereRadiusDepth = CalculateSphereDepth(vPos, fPointRadius, fDepth, fDXScale);...}

所有标准的东西,但我把它包括在内是为了展示它是如何使用的.

关键方法和问题的答案是:

float CalculateSphereDepth(float4 vPos, float fPointRadius, float fSphereCenterDepth, float fDXScale) {//计算球体深度.通过计算一个点来做到这一点//球体的远端,即从眼睛投射光线,通过//点精灵顶点(球体中心)并将其扩展半径//球体//球体中心与球体的深度差//然后使用边缘在精灵上写出球体深度".float4 vRayDir = vPos - g_vecEyePos;浮动 fLength = 长度(vRayDir);vRayDir = normalize(vRayDir);fLength = fLength + vPointRadius;//眼睛通过球体中心到球体边缘的距离float4 oSphereEdgePos = g_vecEyePos + (fLength * vRayDir);//指向球体的边缘oSphereEdgePos.w = 1.0;oSphereEdgePos = mul(oSphereEdgePos, g_mWorldViewProj);//投影它//投影球体边缘点的DX深度计算const float fSphereEdgeDepth = oSphereEdgePos.z/oSphereEdgePos.w;浮动 fSphereRadiusDepth = fSphereCenterDepth - fSphereEdgeDepth;//球体中心和边缘之间的差异fSphereRadiusDepth *= fDXScale;//考虑球体缩放返回 fSphereRadiusDepth;}

像素着色器

void SpherePS(...浮动 fSpriteDepth : TEXCOORD0,浮动 fSphereRadiusDepth : TEXCOORD1,出 float4 oFragment : COLOR0,出浮动 fSphereDepth : DEPTH0){浮动 fCircleDist = ...;//参见问题中的示例代码//距离精灵中心0-1值,使用clip将精灵形成一个圆形剪辑(fCircleDist);fSphereDepth = fSpriteDepth + (fCircleDist * fSphereRadiusDepth);//并计算一个像素颜色oFragment = ...;//在此处添加照明等}

此代码省略了光照等.要计算像素距精灵中心的距离(以获得 fCircleDist),请参阅问题中的示例代码(计算 'float dist =...') 已经画了一个圆圈.

最终结果是...

结果

瞧,点精灵绘制球体.

注意事项

  • 精灵的缩放算法可能需要深度为也缩放.我不确定该行是否正确.
  • 它在数学上并不完全正确(走捷径)但正如您所看到的,结果在视觉上是正确的
  • 当使用数百万个精灵时,我仍然获得了良好的渲染速度(在 VMWare Fusion 模拟 Direct3D 设备上,对于 300 万个精灵,每帧 <10 毫秒)

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 other will intersect correctly.

I am using code similar to that written by Johna Holwerda:

void PS_ShowDepth(VS_OUTPUT input, out float4 color: COLOR0,out float depth : DEPTH)
{
   float dist = length (input.uv - float2 (0.5f, 0.5f)); //get the distance form the center of the point-sprite
   float alpha = saturate(sign (0.5f - dist));
   sphereDepth = cos (dist * 3.14159) * sphereThickness * particleSize; //calculate how thick the sphere should be; sphereThickness is a variable.

   depth = saturate (sphereDepth + input.color.w); //input.color.w represents the depth value of the pixel on the point-sprite
   color = float4 (depth.xxx ,alpha ); //or anything else you might need in future passes
}

The video at that link gives a good idea of the effect I'm after: those spheres drawn on point sprites intersect correctly. I've added images below to illustrate too.

I can calculate the depth of the point sprite itself fine. However, I am not sure show to calculate the thickness of the sphere at a pixel in order to add it to the sprite's depth, to give a final depth value. (The above code uses a variable rather than calculating it.)

I've been working on this on and off for several weeks but haven't figured it out - I'm sure it's simple, but it's something my brain hasn't twigged.

Direct3D 9's point sprite sizes are calculated in pixels, and my sprites have several sizes - both by falloff due to distance (I implemented the same algorithm the old fixed-function pipeline used for point size computations in my vertex shader) and also due to what the sprite represents.

How do I go from the data I have in a pixel shader (sprite location, sprite depth, original world-space radius, radius in pixels onscreen, normalised distance of the pixel in question from the centre of the sprite) to a depth value? A partial solution simply of sprite size to sphere thickness in depth coordinates would be fine - that can be scaled by the normalised distance from the centre to get the thickness of the sphere at a pixel.

I am using Direct3D 9 and HLSL with shader model 3 as the upper SM limit.

In pictures

To demonstrate the technique, and the point at which I'm having trouble:

Start with two point sprites, and in the pixel shader draw a circle on each, using clip to remove fragments outside the circle's boundary:

One will render above the other, since after all they are flat surfaces.

Now, make the shader more advanced, and draw the circle as though it was a sphere, with lighting. Note that even though the flat sprites look 3D, they still draw with one fully in front of the other since it's an illusion: they are still flat.

(The above is easy; it's the final step I am having trouble with and am asking how to achieve.)

Now, instead of the pixel shader writing only colour values, it should write the depth as well:

void SpherePS (...any parameters...
    out float4 oBackBuffer : COLOR0,
    out float oDepth : DEPTH0 <- now also writing depth
   )
{

Note that now the spheres intersect when the distance between them is smaller than their radiuses:

How do I calculate the correct depth value in order to achieve this final step?

Edit / Notes

Several people have commented that a real sphere will distort due to perspective, which may be especially visible at the edges of the screen, and so I should use a different technique. First, thanks for pointing that out, it's not necessarily obvious and is good for future readers! Second, my aim is not to render a perspective-correct sphere, but to render millions of data points fast, and visually I think a sphere-like object looks nicer than a flat sprite, and shows the spatial position better too. Slight distortion or lack of distortion does not matter. If you watch the demo video, you can see how it is a useful visual tool. I don't want to render actual sphere meshes because of the large number of triangles compared to a simple hardware-generated point sprite. I really do want to use the technique of point sprites, and I simply want to extend the extant demo technique in order to calculate the correct depth value, which in the demo was passed in as a variable with no source for how it was derived.

解决方案

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

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)

这篇关于如何计算像素着色器深度以将在点精灵上绘制的圆渲染为与其他对象相交的球体?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆