如何计算像素着色器深度以将点精灵上绘制的圆渲染为与其他对象相交的球体?

Dav*_*vid 16 math directx 3d shader hlsl

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

我使用的代码类似于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
}
Run Code Online (Sandbox Code Playgroud)

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

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

我已经开了几个星期一直在研究这个问题,但还没想出来 - 我确信这很简单,但这是我的大脑没有扯过的东西.

Direct3D 9的点精灵大小以像素计算,我的精灵有几种尺寸 - 由于距离导致的衰减(我实现了相同的算法,用于我的顶点着色器中的点大小计算的旧固定功能管道)以及由于什么精灵代表.

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

我使用Direct3D 9和HLSL与着色器模型3作为SM上限.

在图片中

为了演示技术,以及我遇到麻烦的地方:

从两个点精灵开始,并在像素着色器中绘制一个圆圈,使用剪辑移除圆圈边界外的碎片:

在此输入图像描述

一个将在另一个之上呈现,因为它们毕竟是平坦的表面.

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

在此输入图像描述

(以上情况很简单;这是我遇到麻烦的最后一步,我正在问如何实现.)

现在,代替仅使用颜色值的像素着色器,它也应该写入深度:

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

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

在此输入图像描述

如何计算正确的深度值才能实现最后一步?

编辑/备注

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

Dav*_*vid 9

我昨天提出了一个解决方案,它可以很好地工作并产生在精灵上绘制的球体的所需结果,其正确的深度值与场景中的其他对象和球体相交.它的效率可能低于它需要的效果(例如,它计算并投影每个精灵的两个顶点)并且可能在数学上不完全正确(它需要快捷方式),但它会产生视觉上良好的结果.

技术

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

要计算深度坐标的半径:

  • 顶点着色器:在未投影的场景坐标中,通过球体中心(即表示点精灵的顶点)投射来自眼睛的光线,并添加球体的半径.这会给你一个位于球体表面的点.投影精灵顶点和新球体曲面顶点,并计算每个顶点和深度(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);

   ...
}
Run Code Online (Sandbox Code Playgroud)

所有标准的东西,但我包括它,以显示它是如何使用的.

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

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;
}
Run Code Online (Sandbox Code Playgroud)

像素着色器

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
}
Run Code Online (Sandbox Code Playgroud)

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

最终的结果是......

结果

使用点精灵相交球体

瞧,精灵画出球体.

笔记

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