卷呈现概述
有关医疗 MRI 或工程卷,请参阅 维基百科上的体积渲染。 这些“体积图像”包含丰富的信息,在整个体积中具有不透明度和颜色,这些信息不能轻易表示为 多边形网格等表面。
提高性能的关键解决方案
- BAD: Naïve 方法: 显示整个卷, 通常运行太慢
- 良好:剪切平面:仅显示卷的单个切片
- 良好:剪切子卷:仅显示卷的几层
- 良好:降低音量渲染的分辨率 (请参阅“混合分辨率场景渲染”)
在任何特定帧中,只能将一定数量的信息从应用程序传输到屏幕,即总内存带宽。 此外,转换呈现数据所需的任何处理 (或“着色”) 都需要时间。 执行卷呈现时的主要注意事项如下:
- Screen-Width * Screen-Height * Screen-Count * Volume-Layers-on-that-Pixel = Total-volume-samples-per-frame
- 1028 * 720 * 2 * 256 = 378961920 (100%) (完整卷:样本过多)
- 1028 * 720 * 2 * 1 = 1480320 (0.3% 的完整) (薄切片:每像素 1 个样本,运行平稳)
- 1028 * 720 * 2 * 10 = 14803200 (3.9% 的完整) (子卷切片:每个像素 10 个样本,运行相当顺利,看起来 3d)
- 200 * 200 * 2 * 256 = 20480000 (5% 的完整) (较低的分辨率:更少的像素,完整的音量,看起来 3d 但有点模糊)
表示 3D 纹理
在 CPU 上:
public struct Int3 { public int X, Y, Z; /* ... */ }
public class VolumeHeader {
public readonly Int3 Size;
public VolumeHeader(Int3 size) { this.Size = size; }
public int CubicToLinearIndex(Int3 index) {
return index.X + (index.Y * (Size.X)) + (index.Z * (Size.X * Size.Y));
}
public Int3 LinearToCubicIndex(int linearIndex)
{
return new Int3((linearIndex / 1) % Size.X,
(linearIndex / Size.X) % Size.Y,
(linearIndex / (Size.X * Size.Y)) % Size.Z);
}
/* ... */
}
public class VolumeBuffer<T> {
public readonly VolumeHeader Header;
public readonly T[] DataArray;
public T GetVoxel(Int3 pos) {
return this.DataArray[this.Header.CubicToLinearIndex(pos)];
}
public void SetVoxel(Int3 pos, T val) {
this.DataArray[this.Header.CubicToLinearIndex(pos)] = val;
}
public T this[Int3 pos] {
get { return this.GetVoxel(pos); }
set { this.SetVoxel(pos, value); }
}
/* ... */
}
在 GPU 上:
float3 _VolBufferSize;
int3 UnitVolumeToIntVolume(float3 coord) {
return (int3)( coord * _VolBufferSize.xyz );
}
int IntVolumeToLinearIndex(int3 coord, int3 size) {
return coord.x + ( coord.y * size.x ) + ( coord.z * ( size.x * size.y ) );
}
uniform StructuredBuffer<float> _VolBuffer;
float SampleVol(float3 coord3 ) {
int3 intIndex3 = UnitVolumeToIntVolume( coord3 );
int index1D = IntVolumeToLinearIndex( intIndex3, _VolBufferSize.xyz);
return __VolBuffer[index1D];
}
底纹和渐变
如何为卷(如 MRI)着色以获取有用的可视化效果。 主要方法是有一个“强度窗口” (你希望在其中查看强度的最小和最大) ,只需缩放到该空间即可查看黑白强度。 然后,可以将“色带”应用于该范围内的值,并存储为纹理,以便强度光谱的不同部分可以着色不同的颜色:
float4 ShadeVol( float intensity ) {
float unitIntensity = saturate( intensity - IntensityMin / ( IntensityMax - IntensityMin ) );
// Simple two point black and white intensity:
color.rgba = unitIntensity;
// Color ramp method:
color.rgba = tex2d( ColorRampTexture, float2( unitIntensity, 0 ) );
在我们的许多应用程序中,我们在卷中存储原始强度值和“分段索引” (来细分不同的部分,如皮肤和骨骼;这些细分由专家在专用工具) 中创建。 这可以与上述方法结合使用,为每个段索引放置不同的颜色,甚至不同的色带:
// Change color to match segment index (fade each segment towards black):
color.rgb = SegmentColors[ segment_index ] * color.a; // brighter alpha gives brighter color
着色器中的卷切片
第一步是创建一个“切片平面”,该平面可以在卷中移动,“切片它”,以及扫描在每个点的值。 这假定有一个“VolumeSpace”多维数据集,该多维数据集表示卷在世界空间中的位置,可用作放置点的引用:
// In the vertex shader:
float4 worldPos = mul(_Object2World, float4(input.vertex.xyz, 1));
float4 volSpace = mul(_WorldToVolume, float4(worldPos, 1));
// In the pixel shader:
float4 color = ShadeVol( SampleVol( volSpace ) );
着色器中的卷跟踪
如何使用 GPU 执行子卷跟踪 (深入几个体素,然后从后到前) 对数据进行分层:
float4 AlphaBlend(float4 dst, float4 src) {
float4 res = (src * src.a) + (dst - dst * src.a);
res.a = src.a + (dst.a - dst.a*src.a);
return res;
}
float4 volTraceSubVolume(float3 objPosStart, float3 cameraPosVolSpace) {
float maxDepth = 0.15; // depth in volume space, customize!!!
float numLoops = 10; // can be 400 on nice PC
float4 curColor = float4(0, 0, 0, 0);
// Figure out front and back volume coords to walk through:
float3 frontCoord = objPosStart;
float3 backCoord = frontPos + (normalize(cameraPosVolSpace - objPosStart) * maxDepth);
float3 stepCoord = (frontCoord - backCoord) / numLoops;
float3 curCoord = backCoord;
// Add per-pixel random offset, avoids layer aliasing:
curCoord += stepCoord * RandomFromPositionFast(objPosStart);
// Walk from back to front (to make front appear in-front of back):
for (float i = 0; i < numLoops; i++) {
float intensity = SampleVol(curCoord);
float4 shaded = ShadeVol(intensity);
curColor = AlphaBlend(curColor, shaded);
curCoord += stepCoord;
}
return curColor;
}
// In the vertex shader:
float4 worldPos = mul(_Object2World, float4(input.vertex.xyz, 1));
float4 volSpace = mul(_WorldToVolume, float4(worldPos.xyz, 1));
float4 cameraInVolSpace = mul(_WorldToVolume, float4(_WorldSpaceCameraPos.xyz, 1));
// In the pixel shader:
float4 color = volTraceSubVolume( volSpace, cameraInVolSpace );
整卷渲染
修改上面的子卷代码,我们得到:
float4 volTraceSubVolume(float3 objPosStart, float3 cameraPosVolSpace) {
float maxDepth = 1.73; // sqrt(3), max distance from point on cube to any other point on cube
int maxSamples = 400; // just in case, keep this value within bounds
// not shown: trim front and back positions to both be within the cube
int distanceInVoxels = length(UnitVolumeToIntVolume(frontPos - backPos)); // measure distance in voxels
int numLoops = min( distanceInVoxels, maxSamples ); // put a min on the voxels to sample
混合分辨率场景渲染
如何以低分辨率呈现场景的一部分并将其放回原位:
- 设置两个屏幕外摄像头,一个用于跟踪更新每个帧的每只眼睛
- (设置两个低分辨率渲染目标,即相机呈现到的每个) 200x200
- 设置在用户前面移动的象限
每个帧:
- 以低分辨率绘制每只眼睛的呈现目标 (卷数据、昂贵的着色器等)
- 通常将场景绘制为全分辨率 (网格、UI 等)
- 在用户面前、场景中绘制一个四边形,然后将低像素渲染投影到该图上
- 结果:全分辨率元素与低分辨率但高密度卷数据的直观组合