Overzicht van volumerendering
Zie Volume Rendering op Wikipedia voor medische MRI- of technische volumes. Deze 'volumetrische afbeeldingen' bevatten rijke informatie met dekking en kleur in het hele volume die niet gemakkelijk kan worden uitgedrukt als oppervlakken zoals veelhoekige meshes.
Belangrijke oplossingen voor het verbeteren van de prestaties
- BAD: Naïve benadering: Het hele volume weergeven, wordt over het algemeen te langzaam uitgevoerd
- GOED: Snijvlak: slechts één segment van het volume weergeven
- GOED: Subvolume knippen: slechts enkele lagen van het volume weergeven
- GOED: Verlaag de resolutie van de volumerendering (zie 'Mixed Resolution Scene Rendering')
Er is slechts een bepaalde hoeveelheid informatie die kan worden overgebracht van de toepassing naar het scherm in een bepaald frame, wat de totale geheugenbandbreedte is. Daarnaast kost elke verwerking (of 'arcering') die nodig is om die gegevens te transformeren voor de presentatie tijd. De belangrijkste overwegingen bij het weergeven van volumes zijn als volgt:
- Screen-Width * Screen-Height * Screen-Count * Volume-Layers-On-That-Pixel = Total-Volume-Samples-Per-Frame
- 1028 * 720 * 2 * 256 = 378961920 (100%) (full res volume: te veel samples)
- 1028 * 720 * 2 * 1 = 1480320 (0,3% van vol) (dun segment: 1 monster per pixel, loopt soepel)
- 1028 * 720 * 2 * 10 = 14803200 (3,9% van vol) (subvolume slice: 10 samples per pixel, loopt redelijk soepel, ziet er 3d uit)
- 200 * 200 * 2 * 256 = 20480000 (5% van volledig) (lager formaat volume: minder pixels, volledig volume, ziet er 3d maar een beetje wazig uit)
3D-patronen vertegenwoordigen
Op de 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); }
}
/* ... */
}
Op de 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];
}
Arcering en kleurovergangen
Het arceringsscherm van een volume, zoals MRI, voor nuttige visualisatie. De primaire methode is om een 'intensiteitsvenster' (een min en max) te hebben waarbinnen u intensiteiten wilt zien en om eenvoudig in die ruimte te schalen om de zwart-witintensiteit te zien. Een 'kleurenplaat' kan vervolgens worden toegepast op de waarden binnen dat bereik en worden opgeslagen als een textuur, zodat verschillende delen van het intensiteitsspectrum verschillende kleuren kunnen worden gearceerd:
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 ) );
In veel van onze toepassingen slaan we in ons volume zowel een onbewerkte intensiteitswaarde als een 'segmentatie-index' op (om verschillende delen zoals huid en bot te segmenteren; deze segmenten worden gemaakt door experts in speciale hulpmiddelen). Dit kan worden gecombineerd met de bovenstaande benadering om een andere kleur of zelfs een andere kleur voor elk segmentindex te plaatsen:
// Change color to match segment index (fade each segment towards black):
color.rgb = SegmentColors[ segment_index ] * color.a; // brighter alpha gives brighter color
Volumeslicing in een shader
Een goede eerste stap is het maken van een 'segmenteervlak' dat door het volume kan worden verplaatst, 'segmenteren' en hoe de scanwaarden op elk punt worden weergegeven. Hierbij wordt ervan uitgegaan dat er een 'VolumeSpace'-kubus is, die aangeeft waar het volume zich in de wereldruimte bevindt, die kan worden gebruikt als referentie voor het plaatsen van de punten:
// 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 ) );
Volumetracering in shaders
De GPU gebruiken om subvolumetracering uit te voeren (doorloopt een paar voxels diep en vervolgens lagen op de gegevens van achter naar voren):
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 );
Volledige volumerendering
Als u de bovenstaande subvolumecode wijzigt, krijgen we het volgende te zien:
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
Scènerendering met gemengde resolutie
Een deel van de scène weergeven met een lage resolutie en het weer op zijn plaats plaatsen:
- Stel twee camera's buiten het scherm in, één om elk oog te volgen dat elk frame bijwerkt
- Stel twee renderdoelen met lage resolutie (dat wil gezegd, elk 200x200) in die door de camera's worden weergegeven
- Een quad instellen die voor de gebruiker wordt verplaatst
Elk frame:
- Teken de renderdoelen voor elk oog met een lage resolutie (volumegegevens, dure shaders, enzovoort)
- Teken de scène normaal als volledige resolutie (meshes, UI, enzovoort)
- Teken een quad voor de gebruiker, over de scène en projecteert de weergaven met lage res op die
- Resultaat: visuele combinatie van elementen met volledige resolutie met lage resolutie maar high-densityvolumegegevens