Come scrivere al meglio un motore Voxel in C con le prestazioni in mente

Sono un’armatura in OpenGl e per questo motivo sto cercando di imparare solo le moderne OpenGl 4.x. Una volta completate le esercitazioni di base (ad esempio, i cubi rotanti) ho deciso di provare a creare un programma basato su voxel che si occupasse esclusivamente di cubi. Gli obiettivi di questo programma dovevano essere veloci, utilizzare potenza e memoria limitate della CPU ed essere dinamici in modo che le dimensioni della mappa possano cambiare e i blocchi vengano disegnati solo se nell’array si dice che il blocco è pieno.

Ho un VBO con i vertici e gli indici di un cubo formato da triangoli. All’inizio, se la funzione di rendering indica a OpenGl gli shader da utilizzare e quindi associa il VBO una volta completato, eseguo questo ciclo

Draw Cube Loop:

//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); } } } 

Cube.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"); } 

Lo shader del vertice gestisce solo la rotazione e la trasformazione dei vertici e lo shader dei frammenti tratta solo del colore che non sono costosi da eseguire in modo che non siano il collo di bottiglia.

Come può essere migliorato questo codice per renderlo più efficiente e sfruttare appieno le moderne funzionalità di OpenGL per ridurre il sovraccarico?

PS Non sto cercando un libro o uno strumento o una risorsa fuori sito come risposta Ho usato il backface culling e il test di profondità OpenGL per cercare di migliorare la velocità, tuttavia non hanno fatto una differenza drammatica, ma continua a prendere ~ 50ms per rendere un frame e questo è troppo per una griglia voxel di 32 * 32 * 32.

Ecco uno screenshot di quello che sto facendo:

img

E qui link al codice completo:

  • GitHUB Voxel Viewer

Questo perché lo fai nel modo sbagliato. Si chiama 32^3 volte una funzione DrawCube che è troppo big overhead (soprattutto se cambia le matrici). Ciò richiede più probabilmente molto più tempo del rendering stesso. Dovresti passare tutta la roba di rendering in una volta sola se ansible, ad esempio come array di texture o VBO con tutti i cubi.

Dovresti fare tutte le cose all’interno degli shader (anche i cubi …).

Non hai specificato quale tecnica vuoi utilizzare per il rendering del tuo volume. Ci sono molte opzioni qui alcune che vengono solitamente utilizzate:

  • Ray tracing
  • Sezione trasversale
  • Dispersione della superficie secondaria

I tuoi cubi sono trasparenti o solidi? Se solido, perché stai riproducendo 32^3 cubi anziché solo il visibile ~32^2 ? Ci sono modi su come selezionare solo i cubi visibili prima del rendering …

La mia migliore scommessa sarebbe quella di utilizzare ray-tracing e rendering all’interno di framment shader (nessuna mesh cubica solo all’interno del test del cubo). Ma per i principianti il ​​più facile da implementare sarebbe utilizzare VBO con tutti i cubi all’interno come mesh. Puoi anche avere solo punti nel VBO ed emettere cubi nello shader della geometria quest’ultimo ….

Ecco alcune raccolte di QA correlati che potrebbero aiutare con ciascuna delle tecniche …

Ray tracing

  • Cubo LED: disegnare la sfera 3D in C / C ++ ignorare la roba GL 1.0 e concentrarsi sulla funzione sphere()
  • Dispersione atmosferica in GLSL (volume analitico raytrace)
  • raytrace attraverso la mesh 3D Vorrei usarlo basta rimuovere la mesh e le cose di intersezione con la semplice trasformazione di coordinate cubiche per ottenere coordinate del cubo nella tua matrice sarà molto più veloce …
  • Questo è per semitrasparenti

Tracciatore di volume è più semplice della mesh raytrace.

Sezione trasversale

  • Sezione trasversale 4D

Questa è anche una grandezza più semplice per il volume e in 3D

Se hai bisogno di un punto di partenza per GLSL, dai un’occhiata a questo:

  • semplice esempio completo GL + VAO / VBO + GLSL + shaders in C ++

[Edit1] Esempio GLSL

Bene, riesco a scovare un esempio molto semplice di tracciamento volumetrico dei raggi GLSL senza rifrazioni o riflessioni. L’idea è di lanciare un raggio per ogni pixel della telecamera in vertex shader e testare quale cella della griglia del volume e lato del cubo voxel ha colpito all’interno dello shader del frammento . Per passare il volume ho usato GL_TEXTURE_3D senza mipmaps e con GL_NEAREST per s,t,r . Ecco come appare:

immagine dello schermo

Ho incapsulato il codice lato CPU per questo codice C ++ / VCL :

 //--------------------------------------------------------------------------- //--- 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 //--------------------------------------------------------------------------- 

Il volume è iniziato e utilizzato in questo modo:

 // [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,... 

il vol.glsl_draw() rende le cose ... Non dimenticare di chiamare gl_exit prima di chiudere l'app.

Qui 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); } //------------------------------------------------------------------ 

E frammento:

 //------------------------------------------------------------------ #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; } //--------------------------------------------------------------------------- 

Come puoi vedere è molto simile al Mesh Raytracer che ho linkato sopra (è stato fatto da esso). Il ray tracer è semplicemente questa tecnica di Doom convertita in 3D .

Ho usato il mio motore e VCL, quindi è necessario portarlo sul tuo ambiente ( AnsiString stringhe e shader di caricamento / compilazione / collegamento e list<> ) per maggiori informazioni vedi il semplice GL ... link. Inoltre mischio il vecchio GL 1.0 e il core GLSL che non è raccomandato (volevo mantenerlo il più semplice ansible), quindi dovresti convertire il singolo Quad in VBO .

il glsl_draw() richiede che gli shader siano già collegati e ShaderProgram dove ShaderProgram è l'id degli shader.

Il volume è mappato da (0.0,0.0,0.0) a (1.0,1.0,1.0) . La fotocamera è in forma di matrice diretta tm_eye . La class reper è solo la mia matrice di trasformazioni 4x4 che reper sia la reper diretta che la matrice inversa inversa qualcosa come GLM .

La risoluzione del volume è impostata su gl_init() hardcoded su 32x32x32 quindi basta cambiare la riga i=32 a ciò che ti serve.

Il codice non è ottimizzato né fortemente testato, ma sembra che funzioni. I tempi nello screenshot non dicono molto perché c'è un sovraccarico enorme durante il runtime in quanto ho questo come parte di un'app più grande. Solo il valore tim è più o meno affidabile, ma non cambia molto con risoluzioni maggiori (probabilmente fino a che si colga un collo di bottiglia come la dimensione della memoria o la risoluzione dello schermo rispetto alla frequenza dei fotogrammi) Ecco lo screenshot dell'intera app (quindi hai un'idea di cos'altro è in esecuzione):

IDE

Se stai facendo call di estrazione separati e invocando l’esecuzione dello shader per ogni specifico cubo, si tratterà di un’enorme perdita di perf. Consiglierei sicuramente l’instancing: in questo modo il tuo codice può avere una sola chiamata e tutti i cubi saranno renderizzati.

Cerca documentazione per glDrawElementsInstanced, tuttavia questo approccio significa anche che devi avere un “buffer” di matrici, una per ogni cubo di voxel, e dovrai accedere a ciascuna di esse nello shader usando gl_InstanceID per indicizzare nella matrice corretta.

Per quanto riguarda il buffer di profondità, ci saranno risparmi sul rendering se le matrici dei cubi sono in qualche modo ordinate front-to-back dalla fotocamera, quindi c’è un vantaggio in termini di prestazioni di un test di profondità precoce per ogni ansible frammento che si nasconde dietro un -retro cubo voxel.