磁片區轉譯概觀
如需醫療 MRI 或工程磁片區,請參閱 Wikipedia 上的磁片區轉譯。 這些「磁片區影像」包含豐富的資訊,在整個磁片區中具有不透明度和色彩,無法輕易地以 多邊形網格等表面表示。
改善效能的重要解決方案
- BAD:貝氏方法:顯示整個磁片區,通常執行速度太慢
- 良好:切割平面:只顯示磁片區的單一配量
- 良好:剪下子磁片區:只顯示數層的磁片區
- 良好:降低磁片區轉譯的解析度 (請參閱「混合解析度場景轉譯」)
只有一定數量的資訊可以從應用程式傳輸到任何特定畫面的畫面,也就是總記憶體頻寬。 此外,轉換簡報資料所需的任何處理 (或「陰影」) 都需要時間。 執行磁片區轉譯時的主要考慮如下:
- 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' Cube,代表磁片區位於世界空間的位置,可用來做為放置點的參考:
// 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 等完整解析度 ()
- 在使用者前面繪製四邊形、在場景上,並投影低度轉譯到該位置
- 結果:具有低解析度但高密度磁片區資料的全解析度元素視覺組合