[3D][GPU] Voxel-Raycasting/Raytracing

    • Release
    • Open Source

    Es gibt 1 Antwort in diesem Thema. Der letzte Beitrag () ist von φConst.

      [3D][GPU] Voxel-Raycasting/Raytracing

      Name:
      nsVoxelRaytracer

      Beschreibung:
      Mit der Bibliothek ist es möglich, ein beliebiges dreidimensionales Array/Volumen der Größe 512x512x512 hardwarebeschleunigt zu visualisieren. Dabei stellt jedes Element des Arrays ein Volumenelement (Voxel) dar. Für jeden Pixel im Backbuffer wird ein Strahl erzeugt, der das Array traversiert, bis entweder das Ende des Arrays erreicht, oder ein Voxel getroffen wurde.
      Der Vorteil dieser Variante ist, dass das Array zur Laufzeit geändert und die Änderung direkt visualisiert werden können. Die Modifikation des Arrays kann exemplarisch mittels eines Compute-Shaders erfolgen.

      Zusätzlich bietet die Bibliothek dem Entwickler die Möglichkeit, kritische Stellen der Ausführungslogik selbst zu programmieren, etwa die Art, wie die Strahlen erzeugt werden, wie der Raytracer gestartet wird, oder wie der Pixel schattiert/koloriert werden soll.
      Die Programmierung erfolgt dabei in einer HLSL-ähnlichen Programmiersprache, genannt nHLsL. Der Quellcode wird intern in ein HLSL-Code transpiliert und ausgeführt.


      Beispiel:

      Soll ein dreidimensionales Voxel-Array visualisiert werden, muss erst ein Voxel überhaupt spezifiziert werden.
      Hierfür stellt die Bibliothek zwei Attribute bereit: VoxelDefinition sowie SizeInBits.

      Eine mögliche Voxel-Definition kann so aussehen:

      Spoiler anzeigen

      C#-Quellcode

      1. [VoxelDefinition]
      2. struct Voxel
      3. {
      4. [SizeInBits(8)]
      5. public int r;
      6. [SizeInBits(8)]
      7. public int g;
      8. [SizeInBits(8)]
      9. public int b;
      10. }


      Über das Attribut SizeInBits können wir spezifizieren, wie viele Bits eine Variable jeweils in Anspruch nimmt.
      Auf diese Voxel-Definition können wir dann später in nHLsL zugreifen.
      Im Moment ist die Gesamtgröße eines Voxels auf 32-bit beschränkt.

      Ist ein Voxel definiert, können wir den Raytracer initialisieren:

      Spoiler anzeigen

      C#-Quellcode

      1. public class Game1 : Game
      2. {
      3. [VoxelDefinition]
      4. struct Voxel
      5. {
      6. [SizeInBits(8)]
      7. public int r;
      8. [SizeInBits(8)]
      9. public int g;
      10. [SizeInBits(8)]
      11. public int b;
      12. }
      13. RTvoXelQuery<Voxel> rtx;
      14. protected override void LoadContent()
      15. {
      16. rtx = new RTvoXelQuery<Voxel>(GraphicsDevice, Content)
      17. {
      18. RTvoXelSource = <<SHADER_PROGRAMM>>
      19. };
      20. rtx.Compile(RTvoXelQuery<Voxel>.CompilerProfile.DirectX_11);
      21. base.LoadContent();
      22. }
      23. }


      Wir können dann ein Voxel-Array folgendergestalt erzeugen:

      Spoiler anzeigen

      C#-Quellcode

      1. public class Game1 : Game
      2. {
      3. private VoxelVolume3D<Voxel> createSphere(int radius)
      4. {
      5. VoxelVolume3D<Voxel> voxels = new VoxelVolume3D<Voxel>(rtx, radius * 2, radius * 2, radius * 2);
      6. Voxel[] data = new Voxel[voxels.Width * voxels.Height * voxels.Depth];
      7. Random random = new Random();
      8. Vector3 c = new Vector3(radius);
      9. for (int x = 0; x < voxels.Width; x++)
      10. {
      11. for (int z = 0; z < voxels.Depth; z++)
      12. {
      13. for (int y = 0; y < voxels.Height; y++)
      14. {
      15. float r = (new Vector3(x, y, z) - c).LengthSquared();
      16. if (r <= radius * radius)
      17. {
      18. data[x + z * voxels.Width + y * voxels.Width * voxels.Depth] = new Voxel()
      19. {
      20. r = random.Next(0, 255),
      21. g = random.Next(0, 255),
      22. b = random.Next(0, 255),
      23. };
      24. }
      25. }
      26. }
      27. }
      28. voxels.SetVoxelData(data);
      29. return voxels;
      30. }
      31. }


      Die Klasse VoxelVolume3D erbt dabei von Texture3D. Auf die GPU wird also in Wahrheit eine Texture3D<int> hochgeladen, wobei die Struktur, die ein Voxel definiert, in ein Int32 umgewandelt wird.

      Wir können nun die VoxelVolume3D-Instanz über die .Volume-Property des RTvoXelQuery dem Raytracer übergeben.

      Nachfolgend müssen wir den Shader-Code setzen, der jeweils a) einen Strahl generiert, b) die initiale Methode des Raycasters definiert und c) das Verhalten bei Kollision (oder Nicht-Kollision) festlegt.

      Ein möglicher Code kann dabei so aussehen:

      Spoiler anzeigen

      C-Quellcode

      1. @bismIllah
      2. uniform float4x4 cameraRotation;
      3. uniform float3 cameraPosition;
      4. uniform float screenWidth;
      5. uniform float screenHeight;
      6. uniform float2 aspectRatio;
      7. uniform float focalDistance;
      8. nsRay createRay(float2 globalID) {
      9. int screenCoordinateX = globalID.x;
      10. int screenCoordinateY = globalID.y;
      11. float2 texCoord = float2((float) screenCoordinateX * screenWidth, (float) screenCoordinateY * screenHeight);
      12. float2 uv = (texCoord * 2.0f) - float2(1, 1);
      13. uv *= aspectRatio;
      14. float3 newPosition = cameraPosition;
      15. float3 newDirection = mul(float4(normalize(float3(uv.x, -uv.y, focalDistance)), 0), cameraRotation).xyz;
      16. nsSetRayOrigin(newPosition);
      17. nsSetRayDirection(newDirection);
      18. return nsCreateRay();
      19. };
      20. struct RaycastingResult {
      21. Voxel voxel : VOXEL;
      22. float3 hitPoint : HITF32;
      23. bool hit : RESULT;
      24. int iterations : DBG_ITERATIONS;
      25. };
      26. RaycastingResult main(Ray ray) {
      27. RaycastingResult rslt = nsRayArrayCheck<<<RaycastingResult>>>(ray);
      28. return rslt;
      29. }
      30. struct ShadowRaycastResult {
      31. bool hit : RESULT;
      32. };
      33. float4 voxelShader(Ray ray, RaycastingResult result) {
      34. if(result.hit)
      35. return float4(result.voxel.r, result.voxel.g, result.voxel.b, 1) / 255.0f ;
      36. else return float4(0, 0, 0, 1);
      37. }
      38. voxelprogram Test
      39. {
      40. ray_gen = transpile createRay();
      41. entry_point = transpile main();
      42. voxel_shader = transpile voxelShader();
      43. }


      Die Sources beginnen mit @bismIllah. Es gibt drei Routinen die programmiert werden müssen:
      Die ray_gen Methode hat den Rückgabewert nsRay (intrinsisch) und legt fest, wie Strahlen erzeugt werden sollen. Der Parameter dieser Methode muss vom Typ float2 sein (und heißt im Beispiel globalID). Jemandem, der mit Compute-Shadern gearbeitet hat, sollte die Terminologie bekannt sein:
      Der Parameter hält schlicht die zweidimensionale ID des aktuellen Threads und stellt damit die Bildschirmkoordinate dar, wobei der Gültigkeitsbereich zwischen 0 und Bildschirmbreite/höhe liegt.

      Über die intrinsischen Methoden nsSetRayOrigin(float3), nsSetRayDir(float3) können jeweils der Ursprung und die Richtung des Strahls gesetzt und mit nsCreateRay(void) der Strahl erzeugt werden.

      Die entry_point Methode legt den Einstiegspunkt des Raytracers fest. Grundsätzlich wird dort besonders die Struktur definiert, die dem voxel_shader übergeben werden soll.
      Auffällig ist, das dort CUDA ähnlich die Struktur mit <<< >>> festgelegt wird. Die Methode nsRayArrayCheck ist dabei eine "intrinsische" Funktion die als Parameter lediglich ein Strahl erwartet, der über den Parameter der Einstiegsmethode übergeben wird (und mit dem in der ray_gen-Methode generierten Strahl korrespondiert). Eine mögliche Struktur, die der entry_point zurückgibt, kann exemplarisch so aussehen:

      Spoiler anzeigen

      C#-Quellcode

      1. struct RaycastingResult {
      2. Voxel voxel : VOXEL;
      3. float3 hitPoint : HITF32;
      4. bool hit : RESULT;
      5. int iterations : DBG_ITERATIONS;
      6. };


      Der Programmierer kann auf die Ergebnisse der Raytracing-Abfrage über die :-Notation (Slot) zugreifen. Es gibt insgesamt sieben Slots:
      • DEPTH: Gibt die Distanz vom Betrachter zum Voxel an
      • HITF32: Gibt den Punkt an, in dem der Strahl den Voxel schneidet.
      • HITI32: Gibt den Ursprung desjenigen Voxels an, der vom Strahl geschnitten wurde.
      • RESULT: Gibt an, ob der Voxel geschnitten wurde.
      • DBG_ITERATIONS: Gibt an, wie viele Iterationen notwendig waren, um den Voxel zu ermitteln (hauptsächlich für Debug-Zwecke).
      • VOXEL: Gibt den Voxel zurück, wobei dieser mit der Voxel-Definition in C# korrespondiert.
      • NORMAL: Gibt die Normale der Voxel-Fläche an.

      Die voxel_shader Methode spezifiziert dann die Farbe des Pixels, wenn ein Strahl ein Voxel getroffen hat. In der Methode kann wieder die "intrinsische" nsRayArrayCheck aufgerufen werden, etwa um Schatten, oder Reflexionen zu realisieren.

      Den Shader-Code übergeben wir abschließend dem Raytracer über die .RTvoXelSource-Property der RTvoXelQuery-Klasse.


      Screenshot(s):











      Verwendete Programmiersprache(n) und IDE(s):
      C# sowie Custom-Monogame-Fork: github.com/cpt-max/MonoGame von cpt-max.

      Installation:
      Im Projekt einfach ein Verweis auf VoxelRendererQuery.dll setzen.

      Nebstdem muss der Custom-Monogame-Fork installiert werden:
      github.com/cpt-max/Docs/blob/master/Build Requirements.md

      Systemanforderungen:
      DirectX fähige Grafikkarte die Compute-Shader unterstützt.

      Download(s):
      Die Bibliothek und ein Beispiel-Projekt im Anhang.

      Lizenz/Weitergabe:
      Closed Source, bei Nutzung Verweis auf diesen Thread

      Sonstiges:
      Der Raytracer ist bereits fertig implementiert (inklusive Beschleunigungsstruktur, war schließlich meine Bachelorarbeit), es sind aber in dieser Bibliothek nicht alle Features aus der Bachelorarbeit realisiert worden. Man kann - so Gott will - also mit Updates rechnen.

      Viel Spaß.
      Dateien
      Und Gott alleine weiß alles am allerbesten und besser.

      Dieser Beitrag wurde bereits 15 mal editiert, zuletzt von „φConst“ ()