Partilhar via


Composição de volumes

Se não estiver familiarizado com a composição de volumes, recomendamos que leia a nossa descrição geral.

Representando Texturas 3D

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

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

Sombreado e Gradações

Como sombrear um volume, como a ressonância magnética, para uma visualização útil. O método principal é ter uma "janela de intensidade" (um mínimo e um máximo) que pretende ver intensidades dentro e simplesmente dimensionar para esse espaço para ver a intensidade a preto e branco. Uma "rampa de cores" pode então ser aplicada aos valores dentro desse intervalo e armazenada como uma textura, para que diferentes partes do espectro de intensidade possam ter cores diferentes:

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

Em muitas das nossas aplicações, armazenamos no nosso volume um valor de intensidade bruta e um "índice de segmentação" (para segmentar partes diferentes, como a pele e o osso; estes segmentos são criados por especialistas em ferramentas dedicadas). Isto pode ser combinado com a abordagem acima para colocar uma cor diferente ou até mesmo uma rampa de cores diferente para cada índice de segmento:

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

Segmentação de Volumes num Sombreado

Um excelente primeiro passo é criar um "plano de segmentação" que possa mover-se através do volume, "segmentá-lo" e como os valores de análise em cada ponto. Isto pressupõe que existe um cubo "VolumeSpace", que representa onde o volume está no espaço mundial, que pode ser utilizado como referência para colocar os pontos:

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

Rastreio de Volumes em Sombreados

Como utilizar a GPU para fazer o rastreio de subvolume (percorre alguns voxels de profundidade e, em seguida, coloca camadas nos dados de trás para a frente):

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

Composição de Volume Inteiro

Ao modificar o código de subvolume acima, obtemos:

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

Composição de Cenas de Resolução Mista

Como compor uma parte da cena com uma resolução baixa e colocá-la novamente no lugar:

  1. Configurar duas câmaras fora do ecrã, uma para seguir cada olho que atualiza cada frame
  2. Configure dois destinos de composição de baixa resolução (ou seja, 200x200 cada) que as câmaras compõem
  3. Configurar um quad que se move à frente do utilizador

Cada Moldura:

  1. Desenhe os destinos de composição para cada olho com baixa resolução (dados de volume, sombreados caros, etc.)
  2. Desenhe a cena normalmente como resolução completa (malhas, IU, etc.)
  3. Desenhe um quad à frente do utilizador, sobre a cena e projete as representações baixas para esse
  4. Resultado: combinação visual de elementos de resolução completa com dados de volume de baixa resolução, mas de alta densidade