Cómo escribir mejor un motor de vóxel en C con el rendimiento en mente

Soy una armadura en OpenGl y por esta razón, estoy buscando aprender OpenGl moderno, las cosas 4.x. Una vez que había completado los tutoriales básicos (por ejemplo, cubos rotativos), decidí intentar crear un progtwig basado en voxel que tratara únicamente cubos. Los objectives de este progtwig eran ser rápidos, usar memoria y poder de CPU limitados, y ser dynamics para que el tamaño del mapa pueda cambiar y los bloques solo se dibujarán si en el conjunto indica que el bloque está lleno.

Tengo una VBO con los vértices e índices de un cubo construido con triangularjs. Al principio, si la función de renderización le digo a OpenGl que los sombreadores deben usar y luego unir el VBO una vez que esté completo, ejecuto este ciclo

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

El sombreador de vértices solo maneja la rotación y la transformación de los vértices, y el sombreador de fragmentos solo trata con el color, por lo que no son caros de ejecutar, por lo que no son el cuello de botella.

¿Cómo se puede mejorar este código para que sea más eficiente y aproveche al máximo las características modernas de OpenGL para disminuir la sobrecarga?

PD: No estoy buscando un libro o una herramienta o un recurso externo como respuesta. He usado la eliminación de la cara posterior y la prueba de profundidad de OpenGL para intentar mejorar la velocidad; sin embargo, no han hecho una diferencia dramática, todavía demoran ~ 50 ms. para renderizar un cuadro y eso es demasiado para una cuadrícula de 32 * 32 * 32 vóxeles.

Aquí captura de pantalla de lo que estoy haciendo:

img

Y aquí enlace al código completo:

  • Visor de Voxel de GitHUB

Eso es porque haces esto de la manera incorrecta. Estás llamando 32^3 veces a alguna función DrawCube que es demasiado grande por encima (especialmente si cambia las matrices). Eso lleva mucho más tiempo que el renderizado. Debería pasar todo el material de renderizado a la vez si es posible, por ejemplo, como un conjunto de texturas o VBO con todos los cubos.

Deberías hacer todas las cosas dentro de los sombreadores (incluso los cubos …).

No especificó qué técnica quiere usar para renderizar su volumen. Aquí hay muchas opciones que suelen usarse:

  • trazado de rayos
  • Sección transversal
  • Esparcimiento de la superficie secundaria

¿Son tus cubos transparentes o sólidos? Si es sólido, ¿por qué está procesando 32^3 cubos en lugar de solo los visibles ~32^2 ? Hay formas de seleccionar solo cubos visibles antes de renderizar …

Mi mejor opción sería usar el trazado de rayos y la representación dentro del sombreador de fragmentos (sin mallas de cubo solo dentro de la prueba de cubo). Pero para empezar, lo más fácil de implementar sería usar VBO con todos los cubos dentro como malla. También puede tener solo puntos en la VBO y emitir cubos en el sombreador de geometría más tarde ….

Aquí una colección de QAs relacionadas que podrían ayudar con cada una de las técnicas …

trazado de rayos

  • Cubo de LED: dibujar esfera 3D en C / C ++ ignorar las cosas de GL 1.0 y centrarse en la función sphere()
  • Dispersión atmosférica en GLSL (Raytrace de volumen analítico)
  • Raytrace a través de malla 3D Yo usaría esto simplemente elimine la malla y las intersecciones con la transformación de coordenadas de cubo simple para obtener coordenadas de cubo en su matriz será mucho más rápido …
  • Dispersión de superficie submarina SSS esto es para material semi transparente

El trazador de rayos de volumen es de una magnitud más simple que la traza de rayos de malla.

Sección transversal

  • Sección transversal 4D

Esta también es una magnitud más simple para el volumen y en 3D

Si necesita un punto de inicio para GLSL, eche un vistazo a esto:

  • ejemplo completo GL + VAO / VBO + GLSL + shaders completo en C ++

[Edit1] Ejemplo de GLSL

Bueno, logré reventar un ejemplo muy simplificado de trazado de rayos volumétricos GLSL sin refracciones ni reflections. La idea es lanzar un rayo para cada píxel de la cámara en el sombreador de vértices y probar qué celda de cuadrícula de volumen y lado del cubo de vóxel tocó dentro del sombreador de fragmentos . Para pasar el volumen, utilicé GL_TEXTURE_3D sin mipmaps y con GL_NEAREST para s,t,r . Así es como esto luce:

captura de pantalla

Encapsulé el código del lado de la CPU en este código de 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 //--------------------------------------------------------------------------- 

El volumen se inicia y usa así:

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

el vol.glsl_draw() representa el material ... No olvides llamar a gl_exit antes de cerrar la aplicación.

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

Y Fragmento:

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

Como puede ver, es muy similar a Mesh Raytracer que he vinculado anteriormente (se hizo a partir de él). El rayo trazador es simplemente esta técnica Doom portada a 3D .

Utilicé mi propio motor y VCL, por lo que debe AnsiString a su entorno (cadenas AnsiString y carga / comstackción / enlace shader y list<> ) para obtener más información, vea el enlace GL ... simple. También mezclo viejas GL 1.0 y núcleo GLSL que no son recomendables (quería mantenerlo lo más simple posible) por lo que debes convertir el Quad individual en VBO .

el glsl_draw() requiere que los shaders estén vinculados y vinculados ya que ShaderProgram es la identificación de los shaders.

El volumen se mapea desde (0.0,0.0,0.0) a (1.0,1.0,1.0) . La cámara está en forma de matriz directa tm_eye . La clase reper es solo la matriz de transformación de 4x4 que contiene tanto la rep directa como la rep inversa de la matriz algo así como GLM .

La resolución del volumen está configurada en gl_init() codificada a 32x32x32 así que simplemente cambie la línea i=32 a la que necesita.

El código no está optimizado ni está muy probado, pero parece que funciona. Los tiempos en la captura de pantalla no dicen mucho, ya que hay una sobrecarga enorme durante el tiempo de ejecución, ya que tengo esto como parte de una aplicación más grande. Solo el valor de tim es más o menos confiable pero no cambia mucho con resoluciones más grandes (probablemente hasta que se alcanza un cuello de botella como tamaño de memoria o resolución de pantalla vs. velocidad de cuadro) Aquí captura de pantalla de toda la aplicación (para que tenga una idea) Esta corriendo):

IDE

Si realiza llamadas de sorteo por separado e invoca la ejecución de sombreado para cada cubo específico que va a ser una pérdida de perfl masiva. Definitivamente recomendaría la creación de instancias, de esta forma su código puede tener una sola llamada al sorteo y todos los cubos serán procesados.

Buscar documentación para glDrawElementsInstanced, sin embargo, este enfoque también significa que tendrá que tener un “buffer” de matrices, uno para cada cubo de vóxel, y tendrá que acceder a cada uno en el sombreador utilizando gl_InstanceID para indexar en la matriz correcta.

Con respecto al búfer de profundidad, habrá ahorros en su renderizado si las matrices de cubos se clasifican de alguna manera frente a frente de la cámara, por lo que existe el beneficio de rendimiento de una prueba de profundidad de z inicial para cualquier posible fragmento que yace detrás de una Cubo de voxel entregado.