Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Jeśli dopiero zaczynasz renderowanie woluminów, zalecamy zapoznanie się z naszym omówieniem.
Reprezentowanie tekstur 3D
Na procesorze 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 procesorze 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];
}
Cieniowanie i gradienty
Jak zacienić wolumin, taki jak MRI, na potrzeby przydatnej wizualizacji. Podstawową metodą jest posiadanie "okna intensywności" (minimalna i maksymalna), w którym chcesz zobaczyć intensyfikacje, a po prostu przeprowadź skalowanie w tę przestrzeń, aby zobaczyć czarną i białą intensywność. Następnie można zastosować "rampę kolorów" do wartości w tym zakresie i przechowywać jako teksturę, aby różne części spektrum intensywności mogły być zacienione w różnych kolorach:
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 ) );
}
W wielu naszych aplikacjach przechowujemy zarówno wartość pierwotnej intensywności, jak i "indeks segmentacji" (w celu segmentowania różnych części, takich jak skóra i kość; te segmenty są tworzone przez ekspertów w dedykowanych narzędziach). Można to połączyć z powyższym podejściem, aby umieścić inny kolor, a nawet różne rampy kolorów dla każdego indeksu segmentu:
// Change color to match segment index (fade each segment towards black):
color.rgb = SegmentColors[ segment_index ] * color.a; // brighter alpha gives brighter color
Fragmentowanie woluminu w cieniowaniu
Doskonałym pierwszym krokiem jest utworzenie "płaszczyzny fragmentowania", która może przechodzić przez wolumin, "fragmentowanie go" i jak wartości skanowania w każdym punkcie. Przyjęto założenie, że istnieje moduł "VolumeSpace", który reprezentuje miejsce, w którym wolumin znajduje się w przestrzeni światowej, który może służyć jako odwołanie do umieszczania punktów:
// 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 ) );
Śledzenie woluminów w cieniowaniach
Jak używać procesora GPU do śledzenia podvolume (przechodzi kilka voxels głęboko, a następnie warstwy danych z tyłu do przodu):
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 );
Renderowanie całego woluminu
Modyfikując powyższy kod podvolume, uzyskujemy następujące informacje:
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
Renderowanie mieszanej sceny rozpoznawania
Jak renderować część sceny o niskiej rozdzielczości i umieścić ją z powrotem w miejscu:
- Skonfiguruj dwie kamery poza ekranem, jeden, aby śledzić każde oko, które aktualizują każdą ramkę
- Skonfiguruj dwa obiekty docelowe renderowania o niskiej rozdzielczości (czyli 200x200) renderowane przez aparaty
- Konfigurowanie czworokąta, który porusza się przed użytkownikiem
Każda ramka:
- Rysuj elementy docelowe renderowania dla każdego oka w niskiej rozdzielczości (dane woluminu, kosztowne cieniowania itd.)
- Rysuj scenę normalnie jako pełną rozdzielczość (siatki, interfejs użytkownika itd.)
- Rysuj czworokąt przed użytkownikiem, na scenie i projektuj renderowanie o niskiej rozdzielczości na tym
- Wynik: wizualna kombinacja elementów o pełnej rozdzielczości z danymi o niskiej rozdzielczości, ale o wysokiej gęstości