OpenGL.Shader: animación de textura de 3 GPU, reaprendizaje del sombreador de vértices / fragmentos

OpenGL.Shader: animación de textura de 3 GPU, reaprendizaje del sombreador de vértices / fragmentos

 

Primero ponga la dirección del proyecto: https://github.com/MrZhaozhirong/NativeCppApp   y las representaciones del contenido de este artículo

Al comienzo de este artículo, se lanza oficialmente el conocimiento de OpengGL.Shader. Desde el análisis del efecto hasta la teoría en profundidad para diseccionar GLSL paso a paso.

Siguiendo el último artículo de OpenGL.Shader: 2 , ya hemos completado una textura de cubo. Como se muestra en la imagen superior izquierda, los puntos de conocimiento básico son la práctica simple de OpenGL.ES en Android: 11-panorámico (prueba de profundidad de índice) Echemos un vistazo a la versión Cpp de CubeIndex

CubeIndex::CubeIndex() {
    modelMatrix = new float[16];
    CELL::Matrix::setIdentityM(modelMatrix, 0);

    CUBE_VERTEX_DATA = new int8_t[60];
    int8_t * p = CUBE_VERTEX_DATA;
    p[0]=-1;   p[1]=1;    p[2]=1;    p[3]=0;   p[4]=0;
    p[5]=1;    p[6]=1;    p[7]= 1;   p[8]=1;   p[9]=0;
    p[10]=-1;  p[11]=-1;  p[12]= 1;  p[13]=0;  p[14]=1;
    p[15]=1;   p[16]=-1;  p[17]= 1;  p[18]=1;  p[19]=1;
    p[20]=-1;  p[21]= 1;  p[22]=-1;  p[23]=1;  p[24]=0;
    p[25]=1;   p[26]=1;   p[27]=-1;  p[28]=0;  p[29]=0;
    p[30]=-1;  p[31]=-1;  p[32]=-1;  p[33]=1;  p[34]=1;
    p[35]=1;   p[36]=-1;  p[37]=-1;  p[38]=0;  p[39]=1;

    p[40]=-1;  p[41]= 1;  p[42]=-1;  p[43]=0;  p[44]=0;
    p[45]=1;   p[46]=1;   p[47]=-1;  p[48]=1;  p[49]=0;
    p[50]=-1;  p[51]=1;   p[52]=1;   p[53]=0;  p[54]=1;
    p[55]=1;   p[56]=1;   p[57]= 1;  p[58]=1;  p[59]=1;

    //{
    //        //x,   y,  z    s, t,
    //        -1,   1,   1,   0, 0,  // 0 left top near
    //        1,   1,   1,    0, 1,  // 1 right top near
    //        -1,  -1,   1,   1, 0,  // 2 left bottom near
    //        1,  -1,   1,    1, 1,  // 3 right bottom near
    //        -1,   1,  -1,   1, 0,  // 4 left top far
    //        1,   1,  -1,    0, 0,  // 5 right top far
    //        -1,  -1,  -1,   1, 1,  // 6 left bottom far
    //        1,  -1,  -1,    1, 0,  // 7 right bottom far
    //        这样安排的纹理坐标点,四周是正常的,但是顶底是不正常,
    //        所以顶底要重新安排一组
    //        -1,   1,  -1,   0, 0,  // 8  left top far
    //        1,   1,  -1,    1, 0,  // 9  right top far
    //        -1,   1,   1,   0, 1,  // 10 left top near
    //        1,   1,   1,    1, 1,  // 11 right top near
    //};

    CUBE_INDEX = new int8_t[24];
    CUBE_INDEX[0 ]= 8;  CUBE_INDEX[1 ]= 9;  CUBE_INDEX[2 ]=10;  CUBE_INDEX[3 ]=11;
    CUBE_INDEX[4 ]= 6;  CUBE_INDEX[5 ]= 7;  CUBE_INDEX[6 ]=2;   CUBE_INDEX[7 ]=3;
    CUBE_INDEX[8 ]= 0;  CUBE_INDEX[9 ]= 1;  CUBE_INDEX[10]=2;   CUBE_INDEX[11]=3;
    CUBE_INDEX[12]= 4;  CUBE_INDEX[13]= 5;  CUBE_INDEX[14]=6;   CUBE_INDEX[15]=7;
    CUBE_INDEX[16]= 4;  CUBE_INDEX[17]= 0;  CUBE_INDEX[18]=6;   CUBE_INDEX[19]=2;
    CUBE_INDEX[20]= 1;  CUBE_INDEX[21]= 5;  CUBE_INDEX[22]=3;   CUBE_INDEX[23]=7;
    //{
    //    //top
    //    8,9,10,11,
    //    //bottom
    //    6,7,2,3
    //    //front
    //    0,1,2,3,
    //    //back
    //    4,5,6,7,
    //    //left
    //    4,0,6,2,
    //    //right
    //    1,5,3,7,
    //};
}

CubeIndex::~CubeIndex() {
    delete [] CUBE_VERTEX_DATA;
    delete [] CUBE_INDEX;
    delete [] modelMatrix;
}

void CubeIndex::bindData(CubeShaderProgram* shaderProgram) {
    glVertexAttribPointer(static_cast<GLuint>(shaderProgram->aPositionLocation),
                          POSITION_COMPONENT_COUNT, GL_BYTE,
                          GL_FALSE, STRIDE,
                          CUBE_VERTEX_DATA);
    glEnableVertexAttribArray(static_cast<GLuint>(shaderProgram->aPositionLocation));

    glVertexAttribPointer(static_cast<GLuint>(shaderProgram->aTexUvLocation),
                          TEXTURE_COORDINATE_COMPONENT_COUNT, GL_BYTE,
                          GL_FALSE, STRIDE,
                          &CUBE_VERTEX_DATA[POSITION_COMPONENT_COUNT]);
    glEnableVertexAttribArray(static_cast<GLuint>(shaderProgram->aTexUvLocation));
}

void CubeIndex::draw() {
    // 正方体 六个面,每个面两个三角形,每个三角形三个点
    //glDrawElements(GL_TRIANGLES, 6*2*3, GL_UNSIGNED_BYTE, CUBE_INDEX );
    // 正方体 六个面,每个面四个点
    glDrawElements(GL_TRIANGLE_STRIP, 6*4, GL_UNSIGNED_BYTE, CUBE_INDEX );
}

Describe brevemente el código:

La matriz CUBE_VERTEX_DATA almacena las coordenadas de los puntos (x, y, z) y los datos de las coordenadas de textura (s, t) de 11 ubicaciones, donde 4 y 8 son la misma ubicación pero diferentes coordenadas de textura, de manera similar 5 y 9, 0 y 10 , 1 y 11. ¿Por qué la textura es diferente? Si no lo entiende, dibuje un boceto que coincida con la posición de las coordenadas de la textura. No lo discutiré aquí.

CUBE_INDEX almacena el índice de las 4 posiciones de puntos que componen cada superficie. Anteriormente dibujamos triángulos (GL_TRIANGLES), esta vez hicimos una optimización sutil y dibujamos tiras de triángulos (GL_TRIANGLE_STRIP), ahorrando 36-24 = 12 puntos.

No mire menos estos 12 puntos y luego comience a ingresar al primer conocimiento básico de Shader, el proceso de renderizado de sombreadores.

 

El flujo de ejecución de la canalización de renderizado

Renderizando un cubo, ¿sabes claramente cuál es el proceso de ejecución del renderizado? ¿Cuántas veces se ejecuta el sombreador de vértices (VertexShader)? ¿Cuántas veces se ejecutará FragmentShader? Primero, echemos un vistazo a la siguiente imagen:

Como se muestra en la figura, la API de OpengGL y el flujo de trabajo del sombreador: 1. Pasar varios datos de vértices a la memoria / memoria GPU a través de la API del cliente OpenGL (el código que escribimos); 2. Después de que el sombreador de vértices pasa por el ensamblado original , Asignado a los datos de vértice correspondientes; 3. Rasterización, es decir, después de que el cubo se mapea en la pantalla a través de la matriz MVP, se convierte en un área de dibujo con forma de diamante; 4. El sombreador de fragmentos calcula la operación de renderizado de cada fragmento y determina esto Qué valor de color debe mostrarse en el punto correspondiente del cubo: 5. Renderice la imagen y envíela al búfer de cuadros para su visualización.

Bueno, bip mucha teoría y teoría. En este ejemplo, ¿cuántas veces se ejecuta nuestro sombreador de vértices? ¡La respuesta es el parámetro de conteo de glDrawXXXXX! Al dibujar un triángulo (GL_TRIANGLES), el sombreador de vértices se ejecuta 36 veces; al dibujar una tira de triángulo (GL_TRIANGLE_STRIP), el sombreador de vértices se ejecuta 24 veces. Comprenda lo que se ha dicho anteriormente, no mire estas diferencias sutiles. Imagínese el cañón del rey de los pesticidas. Hay cientos o miles de objetos de renderizado. Cada objeto se puede reducir en 10 puntos de renderizado y se reducen 1k objetos. Tiempos de ejecución del sombreador de vértices 1w, ¿cuánto rendimiento se debe optimizar?

CubeShaderProgram::CubeShaderProgram()
{
    const char * vertexShaderResourceStr = const_cast<char *>(" uniform mat4    u_Matrix;\n\
                                                                attribute vec4  a_Position;\n\
                                                                attribute vec2  a_uv;\n\
                                                                varying vec2    out_uv;\n\
                                                                void main()\n\
                                                                {\n\
                                                                      out_uv = a_uv;\n\
                                                                      gl_Position = u_Matrix * a_Position;\n\
                                                                }");

    const char * fragmentShaderResourceStr= const_cast<char *>("precision mediump float;\n\
                                                                uniform sampler2D _texture;\n\
                                                                varying vec2      out_uv;\n\
                                                                void main()\n\
                                                                {\n\
                                                                   gl_FragColor = texture2D(_texture, out_uv);\n\
                                                                }");

    programId = ShaderHelper::buildProgram(vertexShaderResourceStr, fragmentShaderResourceStr);

    uMatrixLocation     = glGetUniformLocation(programId, "u_Matrix");
    aPositionLocation   = glGetAttribLocation(programId, "a_Position");
    aTexUvLocation      = glGetAttribLocation(programId, "a_uv");
    uTextureUnit        = glGetUniformLocation(programId, "_texture");
}

void CubeShaderProgram::setUniforms(float* matrix){
    glUniformMatrix4fv(uMatrixLocation, 1, GL_FALSE, matrix);
}
CubeShaderProgram::~CubeShaderProgram() {

}

Coopere con vertexShaderResourceStr para continuar profundizando la comprensión del párrafo anterior. glDrawElements (GL_TRIANGLE_STRIP, 6 * 4, GL_UNSIGNED_BYTE, CUBE_INDEX); Desencadena la transferencia de datos de vértice al programa de sombreado de vértice, el primer atributo de vértice (índice 0) vec4 a_Position = {-1,1,1} atributo vec2 a_uv = {0, 0}; Después del cálculo lógico personalizado, los datos relevantes se transmiten al sombreador de fragmentos correspondiente a través de las variables integradas. El atributo vec4 del segundo vértice (índice 1) a_Position = {1,1,1} atributo vec2 a_uv = {0,1}; Cuando se ejecutan el tercer y cuarto vértices, el segmento se activará si se encuentran para formar una banda triangular Colorear, ¡pero no significa ejecutar el programa de sombreado de fragmentos una vez!

Entonces, cuando se trata de sombreado de fragmentos, ¿cuántas veces se ejecuta el programa de sombreado de fragmentos? Esto es realmente inexacto. ¿Qué? ! Se acabaron los pantalones, ¿me dices esto? No estoy seguro, pero puedo usar una imagen para mostrar eso.

Para la coloración de fragmentos desencadenada por el primer triángulo, el número de ejecuciones del programa de sombreado de fragmentos depende de cuántos puntos de coloración hay en el área amarilla de la figura anterior. Los puntos de sombreado son similares a los píxeles, pero hay una diferencia. Los píxeles son para la pantalla y los puntos de sombreado son para la canalización de renderizado de GPU. Un píxel puede contener más de un punto de sombreado.

Si la matriz del modelo de este cubo se escala a una cierta proporción, su visualización en la pantalla se reducirá y el número de ejecuciones del sombreador de fragmentos renderizado en el fotograma actual se reducirá; con la matriz de vista de la posición actual de la cámara, después de activar la detección de profundidad, la parte inferior La superficie y la superficie posterior no se renderizarán, por lo que la franja triangular correspondiente no activará la coloración y, naturalmente, no se ejecutará el programa de coloración de fragmentos correspondiente.

 

Animación de textura de GPU

Después de la introducción de los conocimientos teóricos, luego ingrese a la práctica de combate real, ¿cómo lograr el efecto en el lado derecho de la apertura? Algunos estudiantes propondrán una solución de este tipo, a medida que cambia el tiempo, actualizan constantemente la textura para traer el efecto de animación. De hecho, esta es una solución factible, pero las desventajas también son obvias. Si hay demasiados fotogramas de animación periódicos, el espacio físico ocupado por el paquete de recursos aumentará y los recursos para la memoria operativa -> memoria de la GPU también aumentarán. Aquí hay otra forma más eficiente de manipular la reproducción de la animación de textura en el sombreador. Los juegos normales en 2D y 2.5D utilizan este método para lograr la animación de personajes.

Primero, con la ayuda de la función gettimeofday de Linux, se puede obtener el tiempo de ejecución de la aplicación preparada y simplemente se puede empaquetar en CELL :: TimeCounter. Luego, aumente el parámetro de tiempo de ejecución en la devolución de llamada renderOnDraw de GLThread antes. El código relevante es el siguiente:

void *glThreadImpl(void *context)
{
    GLThread *glThread = static_cast<GLThread *>(context);

    CELL::TimeCounter tm;
    while(true)
    {
        // ... ...
        double  second  =   tm.getElapsedTimeInMilliSec();
        if(glThread->isStart)
        {
            //LOGD("GLThread onDraw.");
            glThread->mRender->renderOnDraw(second);
        }
        //tm.update();
        // 不update就是计算整个应用的运行时长
        // update之后,计算清零,用于获取代码间执行的时长
    }
    return 0;
}

Luego cargue la siguiente imagen de recurso en el búfer de textura.

Esta es una imagen compuesta, que organiza cuidadosamente todas las imágenes de marco necesarias para un ciclo de animación. El número de filas no es necesariamente el mismo, pero debe ser el número de filas y columnas. Al ver esta imagen, creo que todos deberían comprender el método que presentaré a continuación, que consiste en cambiar las coordenadas de textura con el tiempo para mostrar las áreas de textura actuales de diferentes filas y columnas. Sin reemplazar el ID de textura, Logra el efecto de mostrar animación.

En primer lugar, la primera pregunta, con el tiempo, ¿cómo determinar en qué marco se encuentra actualmente?

void NativeGLRender::renderOnDraw(double elpasedInMilliSec)
{
    if (mEglCore==NULL || mWindowSurface==NULL) {
        LOGW("Skipping drawFrame after shutdown");
        return;
    }
    mWindowSurface->makeCurrent();
    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

    double elpasedInSec = elpasedInMilliSec/1000; // 运行时间毫秒转为秒
    // 若以1秒为一个周期,播放完所有帧图,即当elpasedInSec==1,纹理位置索引是row*col==16
    // 若以2秒为一个周期,播放完所有帧图,即当elpasedInSec==2,纹理位置索引是row*col==16
    // 所以要用运行时间 / 周期时间 * (row*col)= 当前纹理索引
    int  cycleTimeInSec = 1;
    // 1秒后,纹理位置索引归0,所以要mod上(row*col)防止索引越界
    int    frame        = int(elpasedInSec/cycleTimeInSec * 16)%16;
    
    gpuAnimationProgram->ShaderProgram::userProgram();
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, animation_texure);
    glUniform1i(gpuAnimationProgram->uTextureUnit, 0);
    CELL::Matrix::multiplyMM(modelViewProjectionMatrix, viewProjectionMatrix, cube->modelMatrix);
    gpuAnimationProgram->setMVPUniforms(modelViewProjectionMatrix);
    gpuAnimationProgram->setAnimUniforms(4,4,frame);
    cube->bindData(gpuAnimationProgram);
    cube->draw();
    mWindowSurface->swapBuffers();
}

De hecho, las matemáticas detrás de esto son relativamente simples. Ya está escrito en las notas. Si no lo entiendes, em ... no hay forma. Después de eso, hay un código de plantilla: iniciar sombreador, vincular textura, vincular matriz mvp, vincular datos de vértice, comenzar a renderizar.

El siguiente paso es analizar al protagonista de este artículo: GPUAnimationProgram 

GPUAnimationProgram::GPUAnimationProgram()
{
    const char * vertexShaderResourceStr = const_cast<char *> ("uniform mat4    u_Matrix;\n\
                                                                attribute vec4  a_Position;\n\
                                                                uniform vec3    u_AnimInfor;\n\
                                                                attribute vec2  a_uv;\n\
                                                                varying vec2    out_uv;\n\
                                                                void main()\n\
                                                                {\n\
                                                                      float uS  =  1.0/u_AnimInfor.y;\n\
                                                                      float vS  =  1.0/u_AnimInfor.x;\n\
                                                                      out_uv    =  a_uv * vec2(uS,vS);\n\
                                                                      float  row  =  int(u_AnimInfor.z)/int(u_AnimInfor.y);\n\
                                                                      float  col  =  mod((u_AnimInfor.z), (u_AnimInfor.x));\n\
                                                                      out_uv.x    +=  float(col) * uS;\n\
                                                                      out_uv.y    +=  float(row) * vS;\n\
                                                                      gl_Position = u_Matrix * a_Position;\n\
                                                                }");

    const char * fragmentShaderResourceStr= const_cast<char *>("precision mediump float;\n\
                                                                uniform sampler2D _texture;\n\
                                                                varying vec2      out_uv;\n\
                                                                void main()\n\
                                                                {\n\
                                                                   vec4 texture_color = texture2D(_texture, out_uv);\n\
                                                                   vec4 background_color = vec4(1.0, 1.0, 1.0, 1.0);\n\
                                                                   gl_FragColor = mix(background_color,texture_color, 0.9);\n\
                                                                }");

    programId = ShaderHelper::buildProgram(vertexShaderResourceStr, fragmentShaderResourceStr);

    uMatrixLocation     = glGetUniformLocation(programId, "u_Matrix");
    uAnimInforLocation  = glGetUniformLocation(programId, "u_AnimInfor");

    aPositionLocation   = glGetAttribLocation(programId,  "a_Position");
    aTexUvLocation      = glGetAttribLocation(programId,  "a_uv");

    uTextureUnit        = glGetUniformLocation(programId, "_texture");
}


void GPUAnimationProgram::setMVPUniforms(float* matrix){
    glUniformMatrix4fv(uMatrixLocation, 1, GL_FALSE, matrix);
}

void GPUAnimationProgram::setAnimUniforms(int row,int col,int frame){
    glUniform3f(uAnimInforLocation, row, col, frame);
}

A continuación, inicie el programa de sombreado de vértices, seguido del análisis línea por línea.

uniform vec3 u_AnimInfor; // (1)
// Una nueva variable de entrada personalizada, similar a vec3 (x, y, z),
// donde glUniform3f (ubicación GLint, GLfloat v0, GLfloat v1 ) se puede utilizar en el cliente , GLfloat v2); Especifique el valor del elemento a ser llenado
// Esto representa (fila, columna, marco), donde fila y columna son valores fijos, que son el número de filas y columnas de la imagen de la cuadrícula de arriba,
// marco es el índice de textura actual que cambia dinámicamente La posición es el mapa de cuadrícula de 4 * 4 de arriba, que corresponde a la cuadrícula actual.
uniform mat4 u_Matrix;
atributo vec4 a_Position;
atributo vec2 a_uv;
variando vec2 out_uv;
void main ()
{       float uS = 1.0 / u_AnimInfor.y;       float vS = 1.0 / u_AnimInfor.x;       out_uv = a_uv * vec2 (uS, vS); // (2)       // Las coordenadas de textura de entrada normales son para toda la imagen. Después de cambiar a una imagen compuesta, necesitamos reducir las coordenadas de textura de acuerdo con la proporción de filas y columnas.       // La abscisa u de la textura se multiplica por 1 / col, La ordenada v se debe multiplicar por 1 / fila





      int row = int (u_AnimInfor.z) / int (u_AnimInfor.y);
      float col = mod ((u_AnimInfor.z), (u_AnimInfor.x));
      // Luego calcule la fila y columna específicas de la posición actual del índice s posición.
      out_uv.x + = float (col) * uS; // abscisa, cuántas columnas son el desplazamiento
      out_uv.y + = float (fila) * vS; // ordenada, cuántas filas son el desplazamiento
      gl_Position = u_Matrix * a_Position ;
}

Creo que los comentarios deben ser muy claros, de todas formas hay que prestar atención al cálculo del offset de las coordenadas de textura, al principio yo mismo estuve confundido por unos minutos. Pero tan pronto como me di cuenta de los puntos de atención, fue fácil de resolver. Aquí se analiza el programa de sombreado de vértices y luego el programa de sombreado de fragmentos.

precisión mediump float;
uniform sampler2D _texture;
variando vec2 out_uv;
void main ()
{    vec4 texture_color = texture2D (_texture, out_uv); // Encuentra el valor de coloración de textura normal    vec4 background_color = vec4 (1.0, 1.0, 1.0, 1.0); // Otro valor de color blanco    gl_FragColor = mix (background_color, texture_color, 0.9); // Los dos valores de color se mezclan     // De lo contrario, el cubo completamente transparente es completamente invisible bajo un fondo negro }




Para mencionar aquí, la función incorporada de GLSL: T mix (T x, T y, float a) toma la mezcla lineal de xey, y su fórmula de cálculo es x * (1-a) + y * a.

En algunas versiones muy bajas, es necesario iniciar la función de mezcla en la API de OpenGL.ES, a saber: glEnable (GL_BLEND);           

El otro conocimiento de la mezcla puede ir a  la práctica simple de OpenGL.ES en Android: grabación de 20 marcas de agua (vista previa + marca de agua transparente emoji aluvión gl_blend) para seguir aprendiendo.

Supongo que te gusta

Origin blog.csdn.net/a360940265a/article/details/88977764
Recomendado
Clasificación