Wie schreibe ich am besten eine Voxel-Engine in C mit performance im Kopf

Ich bin ein Anker in OpenGl und aus diesem Grund suche ich nur moderne OpenGl der 4.x Zeug. Sobald ich grundlegende Tutorials (rotierende Würfel zum Beispiel) abgeschlossen hatte, entschied ich, dass ich versuchen würde, ein Voxel-basiertes Programm zu erstellen, das sich ausschließlich mit Cubes befasste. Die Ziele dieses Programms waren, schnell zu sein, begrenzte CPU-performance und Speicher zu verwenden und dynamisch zu sein, so dass sich die Kartengröße ändern kann und Blöcke nur dann gezeichnet werden, wenn in dem Feld angegeben ist, dass der Block gefüllt ist.

Ich habe einen VBO mit den Eckpunkten und Indizes eines Würfels aus Dreiecken. Zu Beginn, wenn die Render-function ich OpenGl die Shader zu verwenden und dann VBO binden, sobald das abgeschlossen ist, führe ich diese Schleife

Cube-Schleife zeichnen:

//The letter_max are the dimensions of the matrix created to store the voxel status in // The method I use for getting and setting entries in the map are very efficient so I have not included it in this example for(int z = -(z_max / 2); z < z_max - (z_max / 2); z++) { for(int y = -(y_max / 2); y < y_max - (y_max / 2); y++) { for(int x = -(x_max / 2); x < x_max - (x_max / 2); x++) { DrawCube(x, y, z); } } } 

Würfel.c

 #include "include/Project.h" void CreateCube() { const Vertex VERTICES[8] = { { { -.5f, -.5f, .5f, 1 }, { 0, 0, 1, 1 } }, { { -.5f, .5f, .5f, 1 }, { 1, 0, 0, 1 } }, { { .5f, .5f, .5f, 1 }, { 0, 1, 0, 1 } }, { { .5f, -.5f, .5f, 1 }, { 1, 1, 0, 1 } }, { { -.5f, -.5f, -.5f, 1 }, { 1, 1, 1, 1 } }, { { -.5f, .5f, -.5f, 1 }, { 1, 0, 0, 1 } }, { { .5f, .5f, -.5f, 1 }, { 1, 0, 1, 1 } }, { { .5f, -.5f, -.5f, 1 }, { 0, 0, 1, 1 } } }; const GLuint INDICES[36] = { 0,2,1, 0,3,2, 4,3,0, 4,7,3, 4,1,5, 4,0,1, 3,6,2, 3,7,6, 1,6,5, 1,2,6, 7,5,6, 7,4,5 }; ShaderIds[0] = glCreateProgram(); ExitOnGLError("ERROR: Could not create the shader program"); { ShaderIds[1] = LoadShader("FragmentShader.glsl", GL_FRAGMENT_SHADER); ShaderIds[2] = LoadShader("VertexShader.glsl", GL_VERTEX_SHADER); glAttachShader(ShaderIds[0], ShaderIds[1]); glAttachShader(ShaderIds[0], ShaderIds[2]); } glLinkProgram(ShaderIds[0]); ExitOnGLError("ERROR: Could not link the shader program"); ModelMatrixUniformLocation = glGetUniformLocation(ShaderIds[0], "ModelMatrix"); ViewMatrixUniformLocation = glGetUniformLocation(ShaderIds[0], "ViewMatrix"); ProjectionMatrixUniformLocation = glGetUniformLocation(ShaderIds[0], "ProjectionMatrix"); ExitOnGLError("ERROR: Could not get shader uniform locations"); glGenVertexArrays(1, &BufferIds[0]); ExitOnGLError("ERROR: Could not generate the VAO"); glBindVertexArray(BufferIds[0]); ExitOnGLError("ERROR: Could not bind the VAO"); glEnableVertexAttribArray(0); glEnableVertexAttribArray(1); ExitOnGLError("ERROR: Could not enable vertex attributes"); glGenBuffers(2, &BufferIds[1]); ExitOnGLError("ERROR: Could not generate the buffer objects"); glBindBuffer(GL_ARRAY_BUFFER, BufferIds[1]); glBufferData(GL_ARRAY_BUFFER, sizeof(VERTICES), VERTICES, GL_STATIC_DRAW); ExitOnGLError("ERROR: Could not bind the VBO to the VAO"); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(VERTICES[0]), (GLvoid*)0); glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(VERTICES[0]), (GLvoid*)sizeof(VERTICES[0].Position)); ExitOnGLError("ERROR: Could not set VAO attributes"); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, BufferIds[2]); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(INDICES), INDICES, GL_STATIC_DRAW); ExitOnGLError("ERROR: Could not bind the IBO to the VAO"); glBindVertexArray(0); } void DestroyCube() { glDetachShader(ShaderIds[0], ShaderIds[1]); glDetachShader(ShaderIds[0], ShaderIds[2]); glDeleteShader(ShaderIds[1]); glDeleteShader(ShaderIds[2]); glDeleteProgram(ShaderIds[0]); ExitOnGLError("ERROR: Could not destroy the shaders"); glDeleteBuffers(2, &BufferIds[1]); glDeleteVertexArrays(1, &BufferIds[0]); ExitOnGLError("ERROR: Could not destroy the buffer objects"); } void DrawCube(float x, float y, float z) { ModelMatrix = IDENTITY_MATRIX; TranslateMatrix(&ModelMatrix, x, y, z); TranslateMatrix(&ModelMatrix, MainCamera.x, MainCamera.y, MainCamera.z); glUniformMatrix4fv(ModelMatrixUniformLocation, 1, GL_FALSE, ModelMatrix.m); glUniformMatrix4fv(ViewMatrixUniformLocation, 1, GL_FALSE, ViewMatrix.m); ExitOnGLError("ERROR: Could not set the shader uniforms"); glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, (GLvoid*)0); ExitOnGLError("ERROR: Could not draw the cube"); } 

Der Vertex-Shader behandelt nur die Drehung und Transformation von Scheitelpunkten und der Fragment-Shader behandelt nur colors, deren Ausführung nicht teuer ist, so dass sie nicht den Engpass darstellen.

Wie kann dieser Code verbessert werden, um effizienter zu machen und die modernen OpenGL-functionen voll auszunutzen, um den Overhead zu verringern?

PS Ich bin nicht auf der Suche nach einem Buch oder einem Tool oder einer Off-Site-Ressource als Antwort Ich habe Backface Culling und den OpenGL-Tiefentest verwendet, um Geschwindigkeit zu verbessern, aber sie haben keinen dramatischen Unterschied gemacht, dauert es immer ~ 50ms einen Rahmen zu rendern und das ist zu viel für ein Voxelraster von 32 * 32 * 32.

Hier ein Screenshot von dem, was ich mache:

img

Und hier Link zum vollständigen Code:

  • GitHUB Voxel Viewer

Das liegt daran, dass du das falsch machst. Du DrawCube 32^3 mal eine function DrawCube die zu viel Overhead ist (besonders wenn sie die Matrizen verändert). Das dauert wahrscheinlich viel viel mehr Zeit als das Rendern selbst. Sie sollten alle Rendering-Sachen auf einmal weitergeben, zum Beispiel als Textur-Array oder VBO mit allen Cubes.

Du solltest alles in Shadern machen (sogar die Würfel …).

Sie haben nicht angegeben, welche Technik Sie zum Rendern Ihres Volumes verwenden möchten. Es gibt einige Optionen, die normalerweise verwendet werden:

  • Ray-Tracing
  • Kreuzung
  • Suboberflächenstreuung

Sind Ihre Würfel transparent oder fest? Wenn Solid, warum renderst du 32^3 Würfel statt nur die sichtbaren ~32^2 ? Es gibt Möglichkeiten, nur sichtbare Würfel vor dem Rendern auszuwählen …

Am besten wäre es, Ray-Tracing und Rendering innerhalb des Fragment-Shaders zu verwenden (keine Cube-Meshes nur innerhalb des Cube-Tests). Aber für Anfänger wäre es einfacher, VBO mit allen Würfeln als Mesh zu verwenden. Sie können auch nur Punkte in der VBO haben und Würfel im Geometrie-Shader ausgeben.

Hier einige meiner QA’s, die bei jeder Technik helfen könnten …

Ray-Tracing

  • LED-Würfel: Zeichne eine 3D-Kugel in C / C ++, ignoriere das GL 1.0- Zeug und konzentriere dich auf die sphere() -function
  • Atmosphärische Streuung in GLSL (analytic volume raytrace)
  • Raytrace durch 3D-Mesh Ich würde dies verwenden, entfernen Sie einfach die Mesh-und Kreuzung Sachen mit einfachen Cube-Koordinaten-Transformation, um Würfel-Koordinaten in Ihrer Matrix erhalten wird viel schneller …
  • SSS-Suboberflächenstreuung ist für halbtransparentes Material

Volume Ray Tracer ist Magnitude einfacher als Mesh Raytrace.

Kreuzung

  • 4D Querschnitt

Dies ist auch eine Größenordnung für Volumen und in 3D einfacher …

Wenn Sie einen Startpunkt für GLSL benötigen, casting Sie einen Blick darauf:

  • Einfaches vollständiges GL + VAO / VBO + GLSL + Shader Beispiel in C ++

[Edit1] GLSL-Beispiel

Nun, ich schaffe es, ein sehr vereinfachtes Beispiel der GLSL volumetrischen Ray-Tracing ohne Brechungen oder reflectionen zu brechen. Die Idee besteht darin, für jeden Pixel der Kamera im Vertex-Shader einen Strahl zu casting und zu testen, welche Volumenrasterzelle und welche Seite des Voxel-Würfels sie innerhalb des Fragment-Shaders trifft. Um das Volume zu übergeben, habe ich GL_TEXTURE_3D ohne Mipmaps und mit GL_NEAREST für s,t,r . So sieht es aus:

Bildschirmfoto

Ich habe den CPU- Seitencode an diesen C ++ / VCL- Code gekapselt:

 //--------------------------------------------------------------------------- //--- GLSL Raytrace system ver: 1.000 --------------------------------------- //--------------------------------------------------------------------------- #ifndef _raytrace_volume_h #define _raytrace_volume_h //--------------------------------------------------------------------------- const GLuint _empty_voxel=0x00000000; class volume { public: bool _init; // has been initiated ? GLuint txrvol; // volume texture at GPU side GLuint size,size2,size3;// volume size [voxel] and its powers GLuint ***data,*pdata; // volume 3D texture at CPU side reper eye; float aspect,focal_length; volume() { _init=false; txrvol=-1; size=0; data=NULL; aspect=1.0; focal_length=1.0; } volume(volume& a) { *this=a; } ~volume() { gl_exit(); } volume* operator = (const volume *a) { *this=*a; return this; } //volume* operator = (const volume &a) { ...copy... return this; } // init/exit void gl_init(); void gl_exit(); // render void gl_draw(); // for debug void glsl_draw(GLint ShaderProgram,List &log); // geometry void beg(); void end(); void add_box(int x,int y,int z,int rx,int ry,int rz,GLuint col); void add_sphere(int x,int y,int z,int r,GLuint col); }; //--------------------------------------------------------------------------- void volume::gl_init() { if (_init) return; _init=true; int x,y,z; GLint i; glGetIntegerv(GL_MAX_TEXTURE_SIZE,&i); size=i; i=32; if (size>i) size=i; // force 32x32x32 resolution size2=size*size; size3=size*size2; pdata =new GLuint [size3]; data =new GLuint**[size]; for (z=0;z &log) { GLint ix,i; GLfloat n[16]; AnsiString nam; const int txru_vol=0; // uniforms nam="aspect"; ix=glGetUniformLocation(ShaderProgram,nam.c_str()); if (ix<0) log.add(nam); else glUniform1f(ix,aspect); nam="focal_length"; ix=glGetUniformLocation(ShaderProgram,nam.c_str()); if (ix<0) log.add(nam); else glUniform1f(ix,focal_length); nam="vol_siz"; ix=glGetUniformLocation(ShaderProgram,nam.c_str()); if (ix<0) log.add(nam); else glUniform1i(ix,size); nam="vol_txr"; ix=glGetUniformLocation(ShaderProgram,nam.c_str()); if (ix<0) log.add(nam); else glUniform1i(ix,txru_vol); nam="tm_eye"; ix=glGetUniformLocation(ShaderProgram,nam.c_str()); if (ix<0) log.add(nam); else{ eye.use_rep(); for (int i=0;i<16;i++) n[i]=eye.rep[i]; glUniformMatrix4fv(ix,1,false,n); } glActiveTexture(GL_TEXTURE0+txru_vol); glEnable(GL_TEXTURE_3D); glBindTexture(GL_TEXTURE_3D,txrvol); // this should be a VBO glColor4f(1.0,1.0,1.0,1.0); glBegin(GL_QUADS); glVertex2f(-1.0,-1.0); glVertex2f(-1.0,+1.0); glVertex2f(+1.0,+1.0); glVertex2f(+1.0,-1.0); glEnd(); glActiveTexture(GL_TEXTURE0+txru_vol); glBindTexture(GL_TEXTURE_3D,0); glDisable(GL_TEXTURE_3D); } //--------------------------------------------------------------------------- void volume::beg() { if (!_init) return; for (int i=0;i=size) x1=size; y1=y0+ry; y0-=ry; if (y0<0) y0=0; if (y1>=size) y1=size; z1=z0+rz; z0-=rz; if (z0<0) z0=0; if (z1>=size) z1=size; for (z=z0;z< =z1;z++) for (y=y0;y<=y1;y++) for (x=x0;x<=x1;x++) data[z][y][x]=col; } //--------------------------------------------------------------------------- void volume::add_sphere(int cx,int cy,int cz,int r,GLuint col) { if (!_init) return; int x0,y0,z0,x1,y1,z1,x,y,z,xx,yy,zz,rr=r*r; x0=cx-r; x1=cx+r; if (x0<0) x0=0; if (x1>=size) x1=size; y0=cy-r; y1=cy+r; if (y0<0) y0=0; if (y1>=size) y1=size; z0=cz-r; z1=cz+r; if (z0<0) z0=0; if (z1>=size) z1=size; for (z=z0;z< =z1;z++) for (zz=z-cz,zz*=zz,y=y0;y<=y1;y++) for (yy=y-cy,yy*=yy,x=x0;x<=x1;x++) { xx=x-cx;xx*=xx; if (xx+yy+zz<=rr) data[z][y][x]=col; } } //--------------------------------------------------------------------------- #endif //--------------------------------------------------------------------------- 

Volume wird wie folgt initiiert und verwendet:

 // [globals] volume vol; // [On init] // here init OpenGL and extentions (GLEW) // load/compile/link shaders // init of volume data vol.gl_init(); vol.beg(); vol.add_sphere(16,16,16,10,0x00FF8040); vol.add_sphere(23,16,16,8,0x004080FF); vol.add_box(16,24,16,2,6,2,0x0060FF60); vol.add_box(10,10,20,3,3,3,0x00FF2020); vol.add_box(20,10,10,3,3,3,0x002020FF); vol.end(); // this copies the CPU side volume array to 3D texture // [on render] // clear screen what ever // bind shader vol.glsl_draw(shader,log); // log is list of strings I use for errors you can ignore/remove it from code // unbind shader // add HUD or what ever // refresh buffers // [on exit] vol.gl_exit(); // free what ever you need to like GL,... 

Das vol.glsl_draw() rendert das Zeug ... Vergessen Sie nicht, gl_exit vor dem Herunterfahren der App gl_exit .

Hier Vertex Shader:

 //------------------------------------------------------------------ #version 420 core //------------------------------------------------------------------ uniform float aspect; uniform float focal_length; uniform mat4x4 tm_eye; layout(location=0) in vec2 pos; out smooth vec3 ray_pos; // ray start position out smooth vec3 ray_dir; // ray start direction //------------------------------------------------------------------ void main(void) { vec4 p; // perspective projection p=tm_eye*vec4(pos.x/aspect,pos.y,0.0,1.0); ray_pos=p.xyz; p-=tm_eye*vec4(0.0,0.0,-focal_length,1.0); ray_dir=normalize(p.xyz); gl_Position=vec4(pos,0.0,1.0); } //------------------------------------------------------------------ 

Und Fragment:

 //------------------------------------------------------------------ #version 420 core //------------------------------------------------------------------ // Ray tracer ver: 1.000 //------------------------------------------------------------------ in smooth vec3 ray_pos; // ray start position in smooth vec3 ray_dir; // ray start direction uniform int vol_siz; // square texture x,y resolution size uniform sampler3D vol_txr; // scene mesh data texture out layout(location=0) vec4 frag_col; //--------------------------------------------------------------------------- void main(void) { const vec3 light_dir=normalize(vec3(0.1,0.1,-1.0)); const float light_amb=0.1; const float light_dif=0.5; const vec4 back_col=vec4(0.1,0.1,0.1,1.0); // background color const float _zero=1e-6; const vec4 _empty_voxel=vec4(0.0,0.0,0.0,0.0); vec4 col=back_col,c; const float n=vol_siz; const float _n=1.0/n; vec3 p,dp,dq,dir=normalize(ray_dir),nor=vec3(0.0,0.0,0.0),nnor=nor; float l=1e20,ll,dl; // Ray trace #define castray\ for (ll=length(p-ray_pos),dl=length(dp),p-=0.0*dp;;)\ {\ if (ll>l) break;\ if ((dp.x< -_zero)&&(px<0.0)) break;\ if ((dp.x>+_zero)&&(px>1.0)) break;\ if ((dp.y< -_zero)&&(py<0.0)) break;\ if ((dp.y>+_zero)&&(py>1.0)) break;\ if ((dp.z< -_zero)&&(pz<0.0)) break;\ if ((dp.z>+_zero)&&(pz>1.0)) break;\ if ((px>=0.0)&&(px< =1.0)\ &&(py>=0.0)&&(py< =1.0)\ &&(pz>=0.0)&&(pz< =1.0))\ {\ c=texture(vol_txr,p);\ if (c!=_empty_voxel){ col=c; l=ll; nor=nnor; break; }\ }\ p+=dp; ll+=dl;\ } // YZ plane voxels hits if (abs(dir.x)>_zero) { // compute start position aligned grid p=ray_pos; if (dir.x<0.0) { p+=dir*(((floor(px*n)-_zero)*_n)-ray_pos.x)/dir.x; nnor=vec3(+1.0,0.0,0.0); } if (dir.x>0.0) { p+=dir*((( ceil(px*n)+_zero)*_n)-ray_pos.x)/dir.x; nnor=vec3(-1.0,0.0,0.0); } // single voxel step dp=dir/abs(dir.x*n); // Ray trace castray; } // ZX plane voxels hits if (abs(dir.y)>_zero) { // compute start position aligned grid p=ray_pos; if (dir.y<0.0) { p+=dir*(((floor(py*n)-_zero)*_n)-ray_pos.y)/dir.y; nnor=vec3(0.0,+1.0,0.0); } if (dir.y>0.0) { p+=dir*((( ceil(py*n)+_zero)*_n)-ray_pos.y)/dir.y; nnor=vec3(0.0,-1.0,0.0); } // single voxel step dp=dir/abs(dir.y*n); // Ray trace castray; } // XY plane voxels hits if (abs(dir.z)>_zero) { // compute start position aligned grid p=ray_pos; if (dir.z<0.0) { p+=dir*(((floor(pz*n)-_zero)*_n)-ray_pos.z)/dir.z; nnor=vec3(0.0,0.0,+1.0); } if (dir.z>0.0) { p+=dir*((( ceil(pz*n)+_zero)*_n)-ray_pos.z)/dir.z; nnor=vec3(0.0,0.0,-1.0); } // single voxel step dp=dir/abs(dir.z*n); // Ray trace castray; } // final color and lighting output if (col!=back_col) col.rgb*=light_amb+light_dif*max(0.0,dot(light_dir,nor)); frag_col=col; } //--------------------------------------------------------------------------- 

Wie Sie sehen können, ist es dem Mesh Raytracer sehr ähnlich, den ich oben verlinkt habe (es wurde daraus gemacht). Der Ray Tracer ist einfach diese Doom-Technik, die auf 3D portiert wurde.

Ich habe meine eigene Engine und VCL verwendet, so dass Sie es in Ihre Umgebung AnsiString ( AnsiString Strings und Shader Loading / Compiling / Linking und list<> ) für weitere Informationen siehe den einfachen GL ... Link. Außerdem mische ich alte GL 1.0 und core- GLSL- Sachen, die nicht empfohlen werden (ich wollte es so einfach wie möglich halten), also sollten Sie das einzelne Quad zu VBO konvertieren .

Die glsl_draw() erfordert, dass die Shader bereits verknüpft und verknüpft sind, wobei ShaderProgram die ID der Shader ist.

Das Volume wird von (0.0,0.0,0.0) bis (1.0,1.0,1.0) . Kamera ist in Form von direkter Matrix tm_eye . Die reper ist nur meine 4x4-Transformationsmatrix, die sowohl eine direkte rep als auch eine inverse inv Matrix wie etwa GLM enthält .

Die Volumenauflösung wird in gl_init() auf 32x32x32 also ändern 32x32x32 einfach die Zeile i=32 zu dem, was Sie brauchen.

Der Code ist weder optimiert noch stark getestet, sieht aber so aus, als ob er funktioniert. Das Timing im Screenshot sagt nicht viel, da es während der Laufzeit einen großen Overhead gibt, da ich dies als Teil einer größeren App habe. Nur der tim Wert ist mehr oder weniger zuverlässig, ändert sich aber nicht mit größeren Auflösungen (wahrscheinlich bis ein Flaschenhals getroffen wird wie Speichergröße oder Bildschirmauflösung vs. Bildfrequenz) Hier Screenshot der gesamten App (damit Sie eine Idee haben was sonst noch läuft):

IDE

Wenn Sie separate Draw-Aufrufe durchführen und die Shader-Ausführung für jeden spezifischen Cube aufrufen, wird dies zu einem massiven Perf-Verlust. Ich würde definitiv Instancing empfehlen – auf diese Weise kann Ihr Code einen einzigen Draw-Aufruf haben und alle Würfel werden gerendert.

Sehen Sie sich die Dokumentation für glDrawElementsInstanced an, dieser Ansatz bedeutet aber auch, dass Sie einen “Puffer” für Matrizen haben müssen, einen für jeden Voxel-Würfel, und auf jeden im Shader zugreifen müssen, indem Sie gl_InstanceID verwenden, um in die richtige Matrix zu indizieren.

In Bezug auf den Tiefenpuffer wird es Einsparungen beim Rendern geben, wenn die Würfelmatrizen von vorne nach hinten von der Kamera sortiert sind, so dass der performancesvorteil eines frühzeitigen Tiefenerrorstests für jedes mögliche Fragment, das hinter einem bereits liegt, ist Voxelwürfel.