Condividi tramite


Rendering del volume

Se non si ha familiarità con il rendering del volume, è consigliabile leggere la panoramica.

Rappresenta le trame 3D

Nella 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); }
   }
   /* ... */
 }

Nella 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];
 }

Ombreggiatura e sfumature

Come ombreggiaturare un volume, ad esempio mri, per una visualizzazione utile. Il metodo principale consiste nell'avere una "finestra di intensità" (un minimo e un massimo) che si desidera visualizzare le intensità all'interno e semplicemente scalare in tale spazio per vedere l'intensità del bianco e nero. Una "gamma di colori" può quindi essere applicata ai valori all'interno di tale intervallo e archiviata come trama, in modo che parti diverse dello spettro di intensità possano essere ombreggiate con colori diversi:

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 molte delle nostre applicazioni, archiviamo nel nostro volume sia un valore di intensità non elaborata che un "indice di segmentazione" (per segmentare diverse parti come pelle e ossa; questi segmenti vengono creati da esperti in strumenti dedicati). Questo può essere combinato con l'approccio precedente per inserire un colore diverso, o anche una diversa rampa di colore per ogni indice del segmento:

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

Sezionamento del volume in uno shader

Un primo grande passaggio consiste nel creare un "piano di sezionamento" che può spostarsi attraverso il volume, 'sezionarlo' e come i valori di analisi in ogni punto. Si presuppone che sia presente un cubo "VolumeSpace", che rappresenta la posizione del volume nello spazio globale, che può essere usato come riferimento per posizionare i punti:

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

Traccia del volume negli shader

Come usare la GPU per eseguire la traccia subvolume (vengono illustrati alcuni voxel in profondità e quindi i livelli sui dati da una parte all'altro):

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

Rendering dell'intero volume

Modificando il codice subvolume precedente, si ottiene:

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

Rendering della scena a risoluzione mista

Come eseguire il rendering di una parte della scena con una risoluzione bassa e riporla in posizione:

  1. Configurare due fotocamere fuori schermo, una per seguire ogni occhio che aggiorna ogni fotogramma
  2. Configurare due destinazioni di rendering a bassa risoluzione (ovvero 200x200 ciascuna) in cui le fotocamere eseguono il rendering
  3. Configurare un quad che si sposta davanti all'utente

Ogni fotogramma:

  1. Disegnare le destinazioni di rendering per ogni occhio a bassa risoluzione (dati del volume, shader costosi e così via)
  2. Disegnare la scena normalmente come risoluzione completa (mesh, interfaccia utente e così via)
  3. Disegnare un quad davanti all'utente, sopra la scena e proiettare il rendering a bassa ripetizione su tale
  4. Risultato: combinazione visiva di elementi a risoluzione completa con dati di volume a bassa risoluzione ma ad alta densità