Descrição geral da composição de volumes
Para fazer ressonâncias magnéticas médicas ou volumes de engenharia, veja Composição em Volume na Wikipédia. Estas "imagens volumetricas" contêm informações avançadas com opacidade e cor em todo o volume que não podem ser facilmente expressas como superfícies como malhas poligonais.
Principais soluções para melhorar o desempenho
- BAD: Abordagem Ingénua: Mostrar Volume Inteiro, geralmente é executado muito lentamente
- BOM: Plano de Corte: Mostrar apenas um único setor do volume
- GOOD: Cortar Sub-Volume: Mostrar apenas algumas camadas do volume
- GOOD: reduza a resolução da composição do volume (veja "Composição de Cenas de Resolução Mista")
Existe apenas uma determinada quantidade de informações que podem ser transferidas da aplicação para o ecrã em qualquer moldura específica, que é a largura de banda total da memória. Além disso, qualquer processamento (ou "sombreado") necessário para transformar esses dados para apresentação requer tempo. As principais considerações ao efetuar a composição de volume são como tal:
- Screen-Width * Screen-Height * Screen-Count * Volume-Layers-On-That-Pixel = Total-Volume-Samples-Per-Frame
- 1028 * 720 * 2 * 256 = 378961920 (100%) (volume total de res: demasiadas amostras)
- 1028 * 720 * 2 * 1 = 1480320 (0,3% do total) (setor fino: 1 exemplo por pixel, executado sem problemas)
- 1028 * 720 * 2 * 10 = 14803200 (3,9% do total) (setor de subvolume: 10 amostras por pixel, é executado de forma bastante suave, parece 3d)
- 200 * 200 * 2 * 256 = 20480000 (5% do volume de res mais baixo: menos pixéis, volume completo, parece 3d mas um pouco desfocado)
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 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 nesse 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
Corte de Volume num Shader
Um excelente primeiro passo é criar um "plano de corte" que pode mover-se através do volume, "cortando-o" e como a análise valoriza 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, 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 do 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:
- Configurar duas câmaras fora do ecrã, uma para seguir cada olho que atualiza cada moldura
- Configurar dois destinos de composição de baixa resolução (ou seja, 200x200 cada) que as câmaras compõem
- Configurar um quad que se move à frente do utilizador
Cada Moldura:
- Desenhe os destinos de composição para cada olho com baixa resolução (dados de volume, sombreados caros, etc.)
- Desenhe a cena normalmente como resolução completa (malhas, IU, etc.)
- Desenhe um quad à frente do utilizador, sobre a cena e projete as composiçãos de baixa resequisa para esse
- Resultado: combinação visual de elementos de resolução completa com dados de volume de baixa resolução, mas de alta densidade