Mundo de cabeça para baixo

Para desenvolver um novo projeto, o Cube Art Project adotou a metodologia Test Driven Development. Nesta abordagem, primeiro é implementado um teste para uma funcionalidade específica do aplicativo e, em seguida, a funcionalidade específica é implementada. Considero que a grande vantagem desta abordagem é a implementação das interfaces finais, que são o menos envolvidas possível nos detalhes de implementação, antes do início do desenvolvimento da funcionalidade. Com esta abordagem, o teste dita a implementação posterior, agregando todos os benefícios da programação contratual, quando as interfaces são contratos para uma implementação específica.
Projeto de Arte Cubo – Um editor 3D no qual o usuário constrói figuras a partir de cubos; não faz muito tempo que esse gênero era muito popular. Por se tratar de uma aplicação gráfica, resolvi adicionar testes com validação de screenshots.
Para validar as capturas de tela, você precisa obtê-las do contexto OpenGL, isso é feito usando a função glReadPixels. A descrição dos argumentos da função é simples – posição inicial, largura, altura, formato (RGB/RGBA/etc.), ponteiro para buffer de saída; qualquer pessoa que tenha trabalhado com SDL ou tenha experiência com buffers de dados em C simplesmente substituirá os argumentos necessários. Entretanto, acho necessário descrever um recurso interessante do buffer de saída glReadPixels; os pixels são armazenados nele de baixo para cima, enquanto em SDL_Surface todas as operações básicas ocorrem de cima para baixo.
Ou seja, tendo carregado uma captura de tela de referência de um arquivo png, não consegui comparar os dois buffers diretamente, pois um deles estava de cabeça para baixo.
Para inverter o buffer de saída do OpenGL, você precisa preenchê-lo subtraindo a altura da captura de tela para a coordenada Y. No entanto, vale a pena considerar que há uma chance de ultrapassar os limites do buffer se você não subtrair um durante o preenchimento, o que acontecerá. levar à corrupção da memória.
Como sempre tento usar o paradigma OOP de “programação por interfaces”, em vez do acesso direto à memória tipo C por ponteiro, quando tentei escrever dados fora do buffer, o objeto me informou sobre isso graças à validação de limites no método .
O código final para o método de obtenção de uma captura de tela no estilo de cima para baixo:

    auto width = params->width;
    auto height = params->height;

    auto colorComponentsCount = 3;
    GLubyte *bytes = (GLubyte *)malloc(colorComponentsCount * width * height);
    glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, bytes);

    auto screenshot = make_shared(width, height);

    for (auto y = 0; y < height; y++) {
        for (auto x = 0; x < width; x++) {
            auto byteX = x * colorComponentsCount;
            auto byteIndex = byteX + (y * (width * colorComponentsCount));
            auto redColorByte = bytes[byteIndex];
            auto greenColorByte = bytes[byteIndex + 1];
            auto blueColorByte = bytes[byteIndex + 2];
            auto color = make_shared(redColorByte, greenColorByte, blueColorByte, 255);
            screenshot->setColorAtXY(color, x, height - y - 1);
        }
    }

    free(bytes);

Fontes

https://community.khronos.org/ t/glreadpixels-fliped-image/26561
https://stackoverflow.com/questions/8346115/why-are-bmps-stored-upside-down

Código fonte

https://gitlab.com/demensdeum/cube- art-project-bootstrap

WebGL + SDL + Emscript

Acabei migrando Mika para WebGL usando SDL 1 e Emscripten.

A seguir descreverei o que precisou ser alterado no código para que a compilação em JavaScript fosse concluída com sucesso.

  1. Use SDL 1 em vez de SDL 2. No momento existe uma porta SDL 2 para emscripten, mas achei mais apropriado usar o SDL 1 embutido no emscripten. O contexto não é inicializado na janela, mas usando SDL_SetVideoMode e o sinalizador SDL_OPENGL. O buffer é desenhado usando o comando SDL_GL_SwapBuffers()
  2. Devido à forma como o JavaScript faz loops – a renderização é colocada em uma função separada e sua chamada periódica é feita usando a função emscripten_set_main_loop
  3. A montagem também deve ser realizada com a chave “-s FULL_ES2=1
  4. Tive que abandonar a biblioteca assimp, carregando o modelo do sistema de arquivos e carregando a textura do disco. Todos os buffers necessários foram carregados na versão desktop e inseridos no arquivo c-header para montagem usando emscripten.

Código:
https://github.com/demensdeum/OpenGLES3-Experiments/tree/master/9-sdl-gles-obj-textured-assimp-miku-webgl/mikuWebGL

Artigos:
http://blog.scottlogic.com/2014/03/12/native-code-emscripten-webgl-simmer-gently.html
https://kripken.github.io/emscripten-site/docs/porting/multimedia_and_graphics/OpenGL-support.html

Modelo:
https://sketchfab.com/models/7310aaeb8370428e966bdcff414273e7

Só existe Miku

O resultado de trabalhar na biblioteca FSGL com OpenGL ES e código:

A seguir descreverei como tudo foi programado, vários problemas interessantes foram resolvidos.

Primeiro, inicializaremos o contexto OpenGL ES, conforme escrevi no post anterior. Além disso, consideraremos apenas a renderização e uma breve descrição do código.

A Matrix está observando você

Esta figura de Miku no vídeo consiste em triângulos. Para desenhar um triângulo no OpenGL, você precisa especificar três pontos com coordenadas x, y, z. em coordenadas 2D do contexto OpenGL.
Como precisamos desenhar uma figura contendo coordenadas 3D, precisamos usar uma matriz de projeção. Também precisamos girar, ampliar ou o que quisermos fazer com o modelo. Para tanto, é utilizada a matriz modelo. Não existe o conceito de câmera no OpenGL; na verdade, os objetos giram em torno de uma câmera estática; Para isso, é utilizada uma matriz de visualização.

Para simplificar a implementação do OpenGL ES – não contém dados de matriz. Você pode usar bibliotecas que adicionam funcionalidades ausentes, por exemplo, GLM.

Sombreadores

Para permitir que o desenvolvedor desenhe qualquer coisa, e de qualquer forma, o OpenGL ES deve implementar shaders de vértices e fragmentos. O vertex shader deve receber coordenadas de renderização como entrada, realizar transformações usando matrizes e passar as coordenadas para gl_Position. Fragmento ou pixel shader – já desenha cor/textura, aplica sobreposição, etc.

Eu escrevi shaders em GLSL. Na minha implementação atual, os shaders são integrados diretamente no código principal do aplicativo como strings C.

Buffers

O buffer de vértices contém as coordenadas dos vértices (vértices); este buffer também contém coordenadas para texturização e outros dados necessários para shaders. Depois de gerar o buffer de vértice, você precisa vincular o ponteiro aos dados do sombreador de vértice. Isso é feito com o comando glVertexAttribPointer, onde você precisa especificar o número de elementos, um ponteiro para o início dos dados e o tamanho do passo que será usado para percorrer o buffer. Na minha implementação, é feita a ligação de coordenadas de vértice e coordenadas de textura para o pixel shader. Porém, vale ressaltar que a transferência dos dados (coordenadas de textura) para o fragment shader é realizada através do vertex shader. Para conseguir isso, as coordenadas são declaradas usando variando.

Para que o OpenGL saiba em que ordem desenhar pontos para triângulos – você precisará de um buffer de índice (índice). O buffer de índice contém o número do vértice na matriz. Usando três desses índices, um triângulo é obtido.

Texturas

Primeiro você precisa carregar/gerar uma textura para OpenGL. Para isso utilizei SDL_LoadBMP, a textura é carregada a partir de um arquivo bmp. No entanto, é importante notar que apenas BMPs de 24 bits são adequados e as cores neles são armazenadas não na ordem RGB usual, mas em BGR. Ou seja, após o carregamento, é necessário substituir o canal vermelho por um azul.
As coordenadas de textura são especificadas no formato UV< /a>, ou seja, você só precisa transferir duas coordenadas. A saída da textura é feita no fragment shader. Para fazer isso, você precisa vincular a textura em um fragment shader.

Nada extra

Como, de acordo com nossas instruções, o OpenGL desenha de 3D a 2D – em seguida, implemente a profundidade e selecione triângulos invisíveis – você precisa usar seleção e um buffer de profundidade (Z-Buffer). Na minha implementação, consegui evitar a geração manual do buffer de profundidade usando dois comandos: glEnable(GL_DEPTH_TEST); e seleções glEnable(GL_CULL_FACE);
Certifique-se também de verificar se o plano próximo da matriz de projeção é maior que zero, porque verificar a profundidade com um plano próximo nulo não funcionará.

Renderização

Para preencher o buffer de vértice, buffer de índice com algo consciente, por exemplo o modelo Miku, você precisa carregar este modelo. Para isso usei a biblioteca assimp. Miku foi colocado em um arquivo no formato Wavefront OBJ, carregado usando assimp, e a conversão de dados de assimp para vértice e buffers de índice foi implementada.

A renderização ocorre em vários estágios:

  1. Gire Miku usando a rotação da matriz do modelo
  2. Limpando a tela e o buffer de profundidade
  3. Desenhar triângulos usando o comando glDrawElements.

Próxima etapa – Implementação de renderização em WebGL usando Emscripten.

Código fonte:
https://github.com/demensdeum/OpenGLES3-Experiments/tree/master/8-sdl-gles-obj-textured-assimp-miku
Modelo:
https://sketchfab.com/models/7310aaeb8370428e966bdcff414273e7

 

Projete

Tendo desenhado um bule vermelho em 3D, considero meu dever descrever brevemente como isso é feito.

O OpenGL moderno não desenha em 3D, apenas desenha triângulos, pontos, etc. em coordenadas de tela 2D.
Para produzir pelo menos algo usando OpenGL, você precisa fornecer um buffer de vértice, escrever um sombreador de vértice, adicionar todas as matrizes necessárias (projeção, modelo, visualização) ao sombreador de vértice,associar todos os dados de entrada com o shader, chame o método renderização em OpenGL. Parece simples?


Ok, o que é um buffer de vértice? Lista de coordenadas a serem desenhadas (x, y, z)
O vertex shader informa à GPU quais coordenadas desenhar.
O pixel shader informa o que desenhar (cor, textura, mesclagem, etc.)
As matrizes traduzem coordenadas 3D em coordenadas OpenGL 2D que podem ser renderizadas

Nos artigos a seguir fornecerei exemplos de código e resultados.

SDL2 – OpenGL ES

I love Panda3D game engine. But right now this engine is very hard to compile and debug on Microsoft Windows operation system. So as I said some time ago, I begin to develop my own graphics library. Right now it’s based on OpenGL ES and SDL2.
In this article I am going to tell how to initialize OpenGL ES context and how SDL2 helps in this task. We are going to show nothing.

King Nothing

First of all you need to install OpenGL ES3 – GLES 3 libraries. This operation is platform dependant, for Ubuntu Linux you can just type sudo apt-get install libgles2-mesa-dev. To work with OpenGL you need to initialize OpenGL context. There is many ways to do that, by using one of libraries – SDL2, GLFW, GLFM etc. Actually there is no one right way to initialize OpenGL context, but I chose SDL2 because it’s cross-platform solution, code will look same for Windows/*nix/HTML5/iOS/Android/etc.

To install sdl2 on Ubuntu use this command sudo apt-get install libsdl2-dev

So here is OpenGL context initialization code with SDL2:

    SDL_Window *window = SDL_CreateWindow(
            "SDL2 - OGLES",
            SDL_WINDOWPOS_UNDEFINED,
            SDL_WINDOWPOS_UNDEFINED,
            640,
            480,
            SDL_WINDOW_OPENGL
            );
	    

    SDL_GLContext glContext = SDL_GL_CreateContext(window);

After that, you can use any OpenGL calls in that context.

Here is example code for this article:
https://github.com/demensdeum/OpenGLES3-Experiments/tree/master/3sdl-gles
https://github.com/demensdeum/OpenGLES3-Experiments/blob/master/3sdl-gles/sdlgles.cpp

You can build and test it with command cmake . && make && ./SDLGles