Rendu du volume

Si vous débutez dans le rendu de volume, nous vous recommandons de lire notre vue d’ensemble.

Représentation de textures 3D

Sur le processeur :

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

Sur le 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];
 }

Ombrage et dégradés

Comment ombrage d’un volume, tel que l’IRM, pour une visualisation utile. La méthode principale consiste à avoir une « fenêtre d’intensité » (un min et un maximum) dans laquelle vous souhaitez voir les intensités, et simplement effectuer une échelle dans cet espace pour voir l’intensité en noir et blanc. Une « rampe de couleurs » peut ensuite être appliquée aux valeurs de cette plage et stockée sous forme de texture, afin que différentes parties du spectre d’intensité puissent être ombrées de différentes couleurs :

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

Dans bon nombre de nos applications, nous stockons dans notre volume à la fois une valeur d’intensité brute et un « index de segmentation » (pour segmenter différentes parties telles que la peau et l’os ; ces segments sont créés par des experts en outils dédiés). Cela peut être combiné avec l’approche ci-dessus pour placer une couleur différente, ou même une rampe de couleurs différente pour chaque index de segment :

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

Découpage de volume dans un nuanceur

Une première étape importante consiste à créer un « plan de découpage » qui peut se déplacer dans le volume, le « découpage » et la façon dont les valeurs d’analyse à chaque point. Cela suppose qu’il existe un cube « VolumeSpace », qui représente l’emplacement du volume dans l’espace mondial, qui peut être utilisé comme référence pour placer les 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 ) );

Suivi de volume dans les nuanceurs

Comment utiliser le GPU pour effectuer un suivi subvolume (marche à la profondeur de quelques voxels, puis couches sur les données de l’arrière-plan) :

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

Rendu du volume entier

En modifiant le code subvolume ci-dessus, nous obtenons :

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

Rendu de scène de résolution mixte

Comment restituer une partie de la scène avec une faible résolution et la remettre en place :

  1. Configurer deux caméras hors écran, une pour suivre chaque œil qui met à jour chaque image
  2. Configurer deux cibles de rendu basse résolution (c’est-à-dire 200 x 200 chacune) que les caméras affichent dans
  3. Configurer un quad qui se déplace devant l’utilisateur

Chaque image :

  1. Dessiner les cibles de rendu pour chaque œil à basse résolution (données de volume, nuanceurs coûteux, et ainsi de suite)
  2. Dessinez la scène normalement en pleine résolution (maillages, interface utilisateur, et ainsi de suite)
  3. Dessinez un quad devant l’utilisateur, sur la scène, et projetez les rendus low-res sur celui-ci
  4. Résultat : combinaison visuelle d’éléments de résolution complète avec des données de volume à faible résolution mais à haute densité