17

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

我正在使用类似于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
}

该链接上的视频很好地了解了我所追求的效果:在点精灵上绘制的那些球体正确相交。我在下面添加了图片来说明。

我可以很好地计算点精灵本身的深度。但是,我不确定要计算球体在像素处的厚度,以便将其添加到精灵的深度,从而给出最终的深度值。(上面的代码使用一个变量而不是计算它。)

我已经断断续续地研究了几个星期,但还没有弄明白——我敢肯定这很简单,但这是我的大脑还没有动过的东西。

Direct3D 9 的点精灵大小以像素为单位计算,我的精灵有几种大小 - 都是由于距离造成的衰减(我实现了与在我的顶点着色器中用于点大小计算的旧固定函数管道相同的算法)以及由于什么精灵代表。

如何从像素着色器中的数据(精灵位置、精灵深度、原始世界空间半径、屏幕上的像素半径、相关像素到精灵中心的标准化距离)转换为深度值?简单地将精灵大小与深度坐标中的球体厚度 的部分解决方案很好 - 可以通过与中心的归一化距离进行缩放以获得像素处的球体厚度。

我使用 Direct3D 9 和 HLSL 以及着色器模型 3 作为 SM 上限。

在图片中

为了演示该技术,以及我遇到问题的地方:

从两个点精灵开始,在像素着色器中在每个精灵上画一个圆圈,使用剪辑删除圆圈边界之外的片段:

在此处输入图像描述

一个将渲染在另一个之上,因为它们毕竟是平面。

现在,使着色器更高级,并绘制圆形,就好像它是一个球体一样,带有照明。请注意,即使平面精灵看起来是 3D 的,它们仍然会完全在另一个之前绘制,因为这是一种错觉:它们仍然是平面的。

在此处输入图像描述

(以上很简单;这是我遇到问题的最后一步,我正在询问如何实现。)

现在,像素着色器不再只写颜色值,它也应该写深度:

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

请注意,现在当球体之间的距离小于它们的半径时,球体相交:

在此处输入图像描述

如何计算正确的深度值以完成最后一步?

编辑/注释

一些人评论说,真实的球体会因透视而变形,这在屏幕边缘可能特别明显,所以我应该使用不同的技术。首先,感谢您指出这一点,这不一定是显而易见的,对未来的读者有好处!其次,我的目标不是渲染透视正确的球体,而是快速渲染数百万个数据点,并且在视觉上我认为类似球体的对象看起来比平面精灵更好,并且也更好地显示空间位置。轻微的失真或没有失真都没有关系。如果您观看演示视频,您可以看到它是一个有用的可视化工具。我不想渲染实际的球体网格,因为与简单的硬件生成的点精灵相比,三角形数量众多。我真的很想使用点精灵的技术,我只是想扩展现有的演示技术以计算正确的深度值,在演示中它是作为变量传入的,没有来源说明它是如何派生的。

4

3 回答 3

10

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

技术

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

要计算深度坐标中的半径:

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

  • 像素着色器:要绘制一个圆,您已经计算出到精灵中心的标准化距离,clip用于不绘制圆外的像素。由于它是标准化的 (0-1),因此将其乘以球体深度(即半径的深度值,即球体中心的像素)并添加到平面精灵本身的深度。这使得球体中心处的最厚深度为 0,而边缘则紧随球体表面。(根据你需要的精确度,使用余弦来获得弯曲的厚度。我发现线性给出了完美的外观。)

代码

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

顶点着色器

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

   ...
}

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

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

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;
}

像素着色器

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
}

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

最终的结果是……

结果

使用点精灵相交球体

瞧,点精灵绘制球体。

笔记

  • 精灵的缩放算法也可能需要缩放深度。我不确定那条线是否正确。
  • 它在数学上并不完全正确(走捷径),但您可以看到结果在视觉上是正确的
  • 在使用数百万个精灵时,我仍然获得了不错的渲染速度(在 VMWare Fusion 模拟 Direct3D 设备上,对于 300 万个精灵,每帧 <10 毫秒)
于 2012-09-18T13:47:30.610 回答
2

第一个大错误是真实的 3d 球体在透视 3d 投影下不会投影到圆形。

这是非常不直观的,但是看一些图片,尤其是大视野和偏离中心的球体。

其次,我建议不要在一开始就使用点精灵,这可能会使事情变得比必要的更难,尤其是考虑到第一点。只需在你的球体周围画一个宽大的边界四边形,然后从那里开始。

在您的着色器中,您应该将屏幕空间位置作为输入。由此,视图变换和投影矩阵可以得到眼睛空间中的一条线。您需要将这条线与眼睛空间中的球体相交(光线跟踪),获取眼睛空间交点,并将其转换回屏幕空间。然后输出 1/w 作为深度。我不是在这里为你做数学,因为我有点醉酒和懒惰,而且我认为这不是你真正想做的事情。虽然这是线性代数的一个很好的练习,所以也许你应该尝试一下。:)

您可能尝试做的效果称为深度精灵,通常仅与正交投影和存储在纹理中的精灵的深度一起使用。只需将深度与您的颜色一起存储在 Alpha 通道中,然后输出 eye.z+(storeddepth-.5)*depthofsprite。

于 2012-09-14T05:51:32.930 回答
1

一般情况下,球体不会投影成圆形。这是解决方案。

这种技术被称为spherical billboards。可以在本文中找到深入的描述: 球形广告牌及其在渲染爆炸中的应用

您将点精灵绘制为四边形,然后对深度纹理进行采样,以便找到每像素 Z 值与当前 Z 坐标之间的距离。采样 Z 值和当前 Z 之间的距离会影响像素的不透明度,使其在与基础几何体相交时看起来像一个球体。该论文的作者建议使用以下代码来计算不透明度:

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;
}

这将防止您的广告牌与场景几何体发生尖锐的交叉。

如果点精灵管道难以控制(我只能说关于 OpenGL 而不是 DirectX),最好使用 GPU 加速的广告牌:你提供 4 个与粒子中心匹配的相等的 3D 顶点。然后将它们移动到顶点着色器中适当的广告牌角落,即:

if ( idx == 0 ) ParticlePos += (-X - Y);
if ( idx == 1 ) ParticlePos += (+X - Y);
if ( idx == 2 ) ParticlePos += (+X + Y);
if ( idx == 3 ) ParticlePos += (-X + Y);

这更面向现代 GPU 管道,并且粗略适用于任何非退化透视投影。

于 2012-09-16T19:17:10.037 回答