關於醫療MRI或工程體積,請參見 維基百科的體積渲染。 這些「體積影像」包含豐富資訊,包含整個體積的不透明度與色彩,這些資訊不容易用多 邊形網格等表面來表達。
提升效能的關鍵解決方案
- 壞:天真方法:顯示整卷,通常跑得太慢
- GOOD:切割平面:只顯示體積的單一切片
- GOOD:切割子體積:只顯示體積的幾層
- GOOD:降低體積渲染的解析度 (查看「混合解析度場景渲染」)
你只能在每個特定畫面中,將應用程式中有限量的資訊傳送到螢幕。 這個總記憶體頻寬限制了你能傳輸的資料量。 此外,任何處理 (或「著色」) 將資料轉換以呈現所需的工作都需要時間。 進行體積渲染時,主要考量如下:
- Screen-Width * Screen-Height * Screen-Count * 該像素的體積層數 = 每幀總體積取樣數
- 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 (,攝影機渲染成 200x200)
- 設置一個四人形,讓它在使用者前方移動
每一幀:
- 用低解析度 (體積資料、昂貴的著色器等方式,為每隻眼睛繪製渲染目標)
- 像全解析度一樣繪製場景, (網格、介面等等)
- 在使用者前方畫一個四邊形,覆蓋整個場景,然後把低解析度渲染投影到上面
- 結果:全解析度元素與低解析度但高密度體積資料的視覺結合