Sdílet prostřednictvím


Přehled vykreslování svazků

Informace o lékařských mri nebo technických objemech najdete v článku Vykreslování svazků na Wikipedii. Tyto "volumemetrické obrázky" obsahují bohaté informace s krytím a barvou v celém svazku, které nelze snadno vyjádřit jako povrchy, jako jsou polygonální sítě.

Klíčová řešení pro zvýšení výkonu

  1. ŠPATN: Naïve Přístup: Zobrazit celý objem, obecně běží příliš pomalu
  2. DOBRÉ: Řezání rovina: Zobrazit pouze jeden řez objemu
  3. DOBRÉ: Řezání Sub-Volume: Zobrazit pouze několik vrstev objemu
  4. DOBRÉ: Snížení rozlišení vykreslování svazku (viz "Vykreslování scény s smíšeným rozlišením"))

Existuje pouze určité množství informací, které lze přenést z aplikace na obrazovku v libovolném konkrétním snímku, což je celková šířka pásma paměti. Jakékoli zpracování (neboli stínování) potřebné k transformaci dat pro prezentaci navíc vyžaduje čas. Při vykreslování svazku je třeba vzít v úvahu tyto hlavní aspekty:

  • Screen-Width * Screen-Height * Screen-Count * Vrstvy svazku na pixelu = Total-Volume-Samples-Per-Frame
  • 1028 * 720 * 2 * 256 = 378961920 (100 %) (plný objem res: příliš mnoho vzorků)
  • 1028 × 720 × 2 × 1 = 1480320 (0,3 % plného) (tenká řez: 1 vzorek na pixel, běží hladce)
  • 1028 * 720 * 2 × 10 = 14803200 (3,9 % plného) (dílčí řez: 10 vzorků na pixel, běží poměrně hladce, vypadá 3d)
  • 200 × 200 × 2 × 256 = 20480000 (5 % plné velikosti) (nižší hlasitost: méně pixelů, plná hlasitost, vypadá 3D, ale trochu rozmazaně)

Reprezentace 3D textur

Na procesoru:

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

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

Stínování a přechody

Jak vystínovat svazek, například MRI, pro užitečnou vizualizaci. Primární metodou je mít okno intenzity (minimum a maximum), ve které chcete zobrazit intenzity uvnitř, a jednoduše škálovat do daného prostoru, abyste viděli černobílou intenzitu. Na hodnoty v daném rozsahu je pak možné použít 'barevnou rampu' a uložit jako texturu, aby různé části spektra intenzity mohly být stínovány různými barvami:

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

V mnoha aplikacích ukládáme do objemu nezpracovanou hodnotu intenzity i index segmentace (abychom segmentovali různé části, jako je kůže a kost; tyto segmenty vytvářejí odborníci na specializované nástroje). Můžete ho zkombinovat s výše uvedeným přístupem a použít pro každý index segmentu jinou barvu nebo dokonce jinou barevnou rampu:

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

Řezy hlasitosti ve shaderu

Skvělým prvním krokem je vytvoření "roviny řezů", která se může pohybovat mezi svazkem, jeho řezy a jak se v každém bodě prohledávají hodnoty. Předpokládá se, že existuje datová krychle VolumeSpace, která představuje umístění svazku ve světovém prostoru, která se dá použít jako odkaz pro umístění bodů:

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

Trasování svazků ve shaderech

Jak pomocí GPU provést trasování podvolu (projde několik voxelů hluboko a pak vrstvy na datech zezadu do popředí):

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

Vykreslování celého svazku

Úpravou výše uvedeného podvolumu kódu získáme:

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

Vykreslování scény s smíšeným rozlišením

Jak vykreslit část scény s nízkým rozlišením a dát ji zpátky na místo:

  1. Nastavte dvě kamery mimo obrazovku, jednu pro sledování každého oka, které aktualizují jednotlivé snímky.
  2. Nastavte dva cíle vykreslování s nízkým rozlišením (každý 200 × 200), které kamery vykreslují do
  3. Nastavení čtyřúhelníku, který se pohybuje před uživatelem

Každý snímek:

  1. Nakreslete cíle vykreslení pro každé oko při nízkém rozlišení (objemová data, drahé shadery atd.)
  2. Nakreslete scénu normálně v plném rozlišení (sítě, uživatelské rozhraní atd.)
  3. Nakreslete čtyřúhelník před uživatelem přes scénu a promítněte na danou scénu vykreslení nízkého výkonu.
  4. Výsledek: Vizuální kombinace prvků v plném rozlišení s daty o objemu s nízkým rozlišením, ale s vysokou hustotou

Viz také