ボリュームレンダリングVolume rendering

医療 MRI またはエンジニアリングボリュームについては、 Wikipedia のボリュームレンダリングを参照してください。For medical MRI or engineering volumes, see Volume Rendering on Wikipedia. これらの ' 容量 images ' には、多角形メッシュなどの表面として簡単には表現できない、ボリューム全体の不透明度と色を含む豊富な情報が含まれています。These 'volumetric images' contain rich information with opacity and color throughout the volume that cannot be easily expressed as surfaces such as polygonal meshes.

パフォーマンスを向上させるための主要ソリューションKey solutions to improve performance

  1. 問題: 単純なアプローチ: ボリューム全体を表示します。一般に、実行速度が低下していますBAD: Naïve Approach: Show Whole Volume, generally runs too slowly
  2. 良好: カットプレーン: ボリュームの1つのスライスのみを表示するGOOD: Cutting Plane: Show only a single slice of the volume
  3. 良い: サブボリュームを切る: ボリュームのレイヤーをいくつか表示するGOOD: Cutting Sub-Volume: Show only a few layers of the volume
  4. 良好: ボリュームレンダリングの解像度を下げます (「混合解像度シーンのレンダリング」を参照してください)。GOOD: Lower the resolution of the volume rendering (see 'Mixed Resolution Scene Rendering')

アプリケーションから特定のフレームの画面に転送できる情報は一定量だけです。これは、メモリ帯域幅の合計です。There is only a certain amount of information that can be transferred from the application to the screen in any particular frame, which is the total memory bandwidth. また、プレゼンテーション用にデータを変換するために必要なすべての処理 (または "網掛け") には時間が必要です。Also, any processing (or 'shading') required to transform that data for presentation requires time. ボリュームレンダリングを行う際の主な考慮事項は次のとおりです。The primary considerations when doing volume rendering are as such:

  • 画面の幅 * 画面の高さ * 画面-カウント * ボリューム-ピクセルあたりのサイズ = 合計-ボリューム-サンプル-フレームあたりScreen-Width * Screen-Height * Screen-Count * Volume-Layers-On-That-Pixel = Total-Volume-Samples-Per-Frame
  • 1028 * 720 * 2 * 256 = 378961920 (100%)(フル res ボリューム: サンプル数が多すぎます)1028 * 720 * 2 * 256 = 378961920 (100%) (full res volume: too many samples)
  • 1028 * 720 * 2 * 1 = 1480320 (0.3% of full) (シンスライス: 1 ピクセルあたり1サンプル、スムーズに実行)1028 * 720 * 2 * 1 = 1480320 (0.3% of full) (thin slice: 1 sample per pixel, runs smoothly)
  • 1028 * 720 * 2 * 10 = 14803200 (3.9% of full) (サブボリュームスライス: 1 ピクセルあたり10個のサンプルが非常にスムーズに実行され、3d が見られます)1028 * 720 * 2 * 10 = 14803200 (3.9% of full) (sub-volume slice: 10 samples per pixel, runs fairly smoothly, looks 3d)
  • 200 * 200 * 2 * 256 = 2048万 (フルの 5%) (小さい方のボリューム: より少ないピクセル、フルボリューム、3d の外観は少しぼやけています)200 * 200 * 2 * 256 = 20480000 (5% of full) (lower res volume: fewer pixels, full volume, looks 3d but a bit blurry)

3D テクスチャの表現Representing 3D Textures

CPU:On the 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 の場合:On the 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];
 }

網掛けとグラデーションShading and Gradients

便利な視覚化のために、MRI などのボリュームを網掛けする方法。How to shade a volume, such as MRI, for useful visualization. 主要な方法としては、輝度を表示する "濃度ウィンドウ" (最小値と最大値) を設定し、その領域を拡大して、黒と白の輝度を表示します。The primary method is to have an 'intensity window' (a min and max) that you want to see intensities within, and simply scale into that space to see the black and white intensity. 次に、"カラーランプ" をその範囲内の値に適用し、テクスチャとして保存することで、照度スペクトルのさまざまな部分に影の異なる色を設定できます。A 'color ramp' can then be applied to the values within that range, and stored as a texture, so that different parts of the intensity spectrum can be shaded different colors:

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

多くのアプリケーションでは、未加工の輝度値と ' セグメンテーションインデックス ' の両方をボリュームに格納します (スキンやボーンなどのさまざまな部分をセグメント化するために、これらのセグメントは一般に専用ツールの専門家によって作成されています)。In many of our applications, we store in our volume both a raw intensity value and a 'segmentation index' (to segment different parts such as skin and bone; these segments are generally created by experts in dedicated tools). これを上記の方法と組み合わせて、セグメントインデックスごとに異なる色または異なるカラーランプを作成できます。This can be combined with the approach above to put a different color, or even different color ramp for each segment index:

// Change color to match segment index (fade each segment towards black):
 color.rgb = SegmentColors[ segment_index ] * color.a; // brighter alpha gives brighter color

シェーダーでのボリュームスライスVolume Slicing in a Shader

最初の優れた手順は、"スライス平面" を作成することです。これは、ボリューム間を移動し、"スライスする" と、各ポイントでのスキャン値を指定します。A great first step is to create a "slicing plane" that can move through the volume, 'slicing it', and how the scan values at each point. これは、ワールド空間におけるボリュームの場所を表す "VolumeSpace" キューブがあることを前提としています。これは、ポイントを配置するための参照として使用できます。This assumes that there is a 'VolumeSpace' cube, which represents where the volume is in world space, that can be used as a reference for placing the points:

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

シェーダーのボリュームトレースVolume Tracing in Shaders

GPU を使用してサブボリュームのトレースを実行する方法 (いくつかの方法について説明します。次に、データのレイヤーをバックエンドから順に見ていきます)。How to use the GPU to do sub-volume tracing (walks a few voxels deep, then layers on the data from back to front):

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

ボリューム全体のレンダリングWhole Volume Rendering

上記のサブボリュームコードを変更すると、次のことが得られます。Modifying the sub-volume code above, we get:

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

混合解像度シーンのレンダリングMixed Resolution Scene Rendering

低解像度でシーンの一部をレンダリングして、その場を戻す方法:How to render a part of the scene with a low resolution and put it back in place:

  1. 2つのスクリーンカメラ (各フレームを更新するために1つずつ) を設定します。Setup two off-screen cameras, one to follow each eye that update each frame
  2. カメラがレンダリングする2つの低解像度のレンダーターゲット (それぞれ 200 x 200) をセットアップします。Setup two low-resolution render targets (i.e. 200x200 each) that the cameras render into
  3. ユーザーの前に移動するクワッドをセットアップするSetup a quad that moves in front of the user

各フレーム:Each Frame:

  1. 各視点のレンダーターゲットを低解像度 (ボリュームデータ、負荷の高いシェーダーなど) で描画します。Draw the render targets for each eye at low-resolution (volume data, expensive shaders, etc.)
  2. シーンを完全な解像度 (メッシュや UI など) として通常どおり描画します。Draw the scene normally as full resolution (meshes, UI, etc.)
  3. ユーザーの前に四角形を描画し、そのシーンに対して低 res のレンダリングを射影します。Draw a quad in front of the user, over the scene, and project the low-res renders onto that
  4. 結果: 低解像度で高密度ボリュームデータを使用する完全解決要素のビジュアル組み合わせResult: visual combination of full-resolution elements with low-resolution but high-density volume data