Pintor gráfico 2D de plataforma cruzada eficiente mínima em C Pure C usando a API gráfica moderna através da excelente biblioteca Sokol GFX.
Sokol GP, ou em curto SGP, significa Sokol Graphics Painter.
O SOKOL GFX é uma excelente biblioteca para renderizar usando pipelines não fixados de placas gráficas modernas, mas é muito complexo para usar para desenho 2D simples, e sua API é muito genérica e especializada para renderização em 3D. Para desenhar material 2D, o programador geralmente precisa configurar shaders personalizados ao usar o Sokol GFX ou usar sua biblioteca extra Sokol GL, mas o Sokol GL também possui uma API com o design 3D em mente, o que incorre em alguns custos e limitações.
Esta biblioteca foi criada para desenhar primitivas 2D através do SOKOL GFX com facilidade e, ao não considerar o uso 3D, é otimizado apenas para renderização em 2D, além disso, possui um otimizador de lote automático , mais detalhes serão descritos abaixo.
Ao desenhar a biblioteca, cria uma fila de comando de desenho de todos os primitivos ainda a serem desenhados, toda vez que um novo comando de draw é adicionado, o otimizador de lote olha para os 8 últimos comandos de desenho recentes (isso é ajustável) e tente reorganizar e mesclar comandos de desenho se encontrar um comando de desenho anterior que atende aos seguintes critérios:
Ao fazer isso, o otimizador em lote pode, por exemplo, mesclar chamadas de desenho texturizadas, mesmo que tenham sido desenhadas com outras texturas intermediárias diferentes entre elas. O efeito é mais eficiência ao desenhar, porque menos chamadas de desenho serão despachadas para a GPU,
Essa biblioteca pode evitar muito trabalho de fazer um sistema de lotes de desenho 2D eficiente, fundindo automaticamente as chamadas nos bastidores em tempo de execução; portanto, o programador não precisa gerenciar chamadas de sorteio em lotes manualmente, nem ele precisa classificar as chamadas de textura em lotes, a biblioteca fará isso sem problemas nos bastidores.
O algoritmo em lote é rápido, mas possui a complexidade da CPU O(n) para cada novo comando de draw adicionado, onde n é a configuração SGP_BATCH_OPTIMIZER_DEPTH . Em experimentos usando 8 como padrão, é um bom padrão, mas você pode experimentar valores diferentes, dependendo do seu caso. O uso de valores muito altos não é recomendado, porque o algoritmo pode levar comandos de desenho anteriores muito longos e que podem consumir mais recursos da CPU.
O otimizador em lote pode ser desativado definindo SGP_BATCH_OPTIMIZER_DEPTH para 0, você pode usá -lo para medir seu impacto.
No diretório de amostras deste repositório, há um exemplo de referência que testa o desenho com o otimizador de banho ativado/desativado. Na minha máquina, o benchmark foi capaz de aumentar o desempenho em um fator de 2,2x quando está ativado. Em alguns jogos particulares, os ganhos do otimizador de lote provaram aumentar o desempenho do FPS acima de 1,5x, apenas substituindo o back -end de gráficos por esta biblioteca, sem alterações internas no próprio jogo.
A biblioteca tem algumas opções de design com desempenho em mente que serão discutidas brevemente aqui.
Como o SOKOL GFX, o Sokol GP nunca fará nenhuma alocação no loop de draw; portanto, ao inicializar, você deve configurar com antecedência o tamanho máximo do buffer da fila de comando draw e o buffer dos vértices.
All the 2D space transformation (functions like sgp_rotate ) are done by the CPU and not by the GPU, this is intentionally to avoid adding extra overhead in the GPU, because typically the number of vertices of 2D applications are not that large, and it is more efficient to perform all the transformation with the CPU right away rather than pushing extra buffers to the GPU that ends up using more bandwidth of the CPU <-> barramento de GPU. Em contraste, os aplicativos 3D geralmente despacham transformações de vértice na GPU usando um shader de vértice, eles fazem isso porque a quantidade de vértices de objetos 3D pode ser muito grande e geralmente é a melhor escolha, mas isso não é verdade para a renderização 2D.
Muitas APIs para transformar o espaço 2D antes de desenhar um primitivo estão disponíveis, como traduzir, girar e escalar. Eles podem ser usados da mesma forma que os disponíveis nas APIs de gráficos 3D, mas são criados apenas para 2D, por exemplo, ao usar 2D, não precisamos usar uma matriz 4x4 ou 3x3 para executar a transformação de vértices, em vez disso, o código é especializado para 2D e pode usar uma matriz de 2x3, salvar as computações de CPU extra.
Todos os oleodutos sempre usam uma textura associada a ele, mesmo ao desenhar primitivas não texturizadas, porque isso minimiza as alterações do pipeline de gráficos ao misturar chamadas texturizadas e chamadas não texturizadas, melhorando a eficiência.
A biblioteca é codificada no estilo dos cabeçalhos Sokol GFX, reutilizando muitas macros a partir daí, você pode alterar parte de sua semântica, como alocador personalizado, função de log personalizada e alguns outros detalhes, leia a documentação sokol_gfx.h para mais sobre isso.
Copie sokol_gp.h junto com outros cabeçalhos Sokol para a mesma pasta. Configure o SOKOL GFX como você normalmente faria, adicione a chamada ao sgp_setup(desc) logo após sg_setup(desc) e ligue para sgp_shutdown() logo antes sg_shutdown() . Observe que você geralmente deve verificar se o SGP é válido após sua criação com sgp_is_valid() e saia graciosamente com um erro, se não.
Na sua função de desenho de quadros, adicione sgp_begin(width, height) antes de chamar qualquer função de desenho SGP e desenhe suas primitivas. No final do quadro (ou buffer de quadro), você sempre deve ligar para sgp_flush() entre um passe de renderização SOKOL GFX BEGIN/END, o sgp_flush() despachará todos os comandos do SOKOL GFX. Em seguida, ligue para sgp_end() imediatamente para descartar a fila de comando de desenho.
Um exemplo real dessa configuração será mostrado abaixo.
A seguir, é apresentado um exemplo rápido de como esta biblioteca com o aplicativo Sokol GFX e Sokol:
// This is an example on how to set up and use Sokol GP to draw a filled rectangle.
// Includes Sokol GFX, Sokol GP and Sokol APP, doing all implementations.
#define SOKOL_IMPL
#include "sokol_gfx.h"
#include "sokol_gp.h"
#include "sokol_app.h"
#include "sokol_glue.h"
#include "sokol_log.h"
#include <stdio.h> // for fprintf()
#include <stdlib.h> // for exit()
#include <math.h> // for sinf() and cosf()
// Called on every frame of the application.
static void frame ( void ) {
// Get current window size.
int width = sapp_width (), height = sapp_height ();
float ratio = width /( float ) height ;
// Begin recording draw commands for a frame buffer of size (width, height).
sgp_begin ( width , height );
// Set frame buffer drawing region to (0,0,width,height).
sgp_viewport ( 0 , 0 , width , height );
// Set drawing coordinate space to (left=-ratio, right=ratio, top=1, bottom=-1).
sgp_project ( - ratio , ratio , 1.0f , -1.0f );
// Clear the frame buffer.
sgp_set_color ( 0.1f , 0.1f , 0.1f , 1.0f );
sgp_clear ();
// Draw an animated rectangle that rotates and changes its colors.
float time = sapp_frame_count () * sapp_frame_duration ();
float r = sinf ( time ) * 0.5 + 0.5 , g = cosf ( time ) * 0.5 + 0.5 ;
sgp_set_color ( r , g , 0.3f , 1.0f );
sgp_rotate_at ( time , 0.0f , 0.0f );
sgp_draw_filled_rect ( -0.5f , -0.5f , 1.0f , 1.0f );
// Begin a render pass.
sg_pass pass = {. swapchain = sglue_swapchain ()};
sg_begin_pass ( & pass );
// Dispatch all draw commands to Sokol GFX.
sgp_flush ();
// Finish a draw command queue, clearing it.
sgp_end ();
// End render pass.
sg_end_pass ();
// Commit Sokol render.
sg_commit ();
}
// Called when the application is initializing.
static void init ( void ) {
// Initialize Sokol GFX.
sg_desc sgdesc = {
. environment = sglue_environment (),
. logger . func = slog_func
};
sg_setup ( & sgdesc );
if (! sg_isvalid ()) {
fprintf ( stderr , "Failed to create Sokol GFX context!n" );
exit ( -1 );
}
// Initialize Sokol GP, adjust the size of command buffers for your own use.
sgp_desc sgpdesc = { 0 };
sgp_setup ( & sgpdesc );
if (! sgp_is_valid ()) {
fprintf ( stderr , "Failed to create Sokol GP context: %sn" , sgp_get_error_message ( sgp_get_last_error ()));
exit ( -1 );
}
}
// Called when the application is shutting down.
static void cleanup ( void ) {
// Cleanup Sokol GP and Sokol GFX resources.
sgp_shutdown ();
sg_shutdown ();
}
// Implement application main through Sokol APP.
sapp_desc sokol_main ( int argc , char * argv []) {
( void ) argc ;
( void ) argv ;
return ( sapp_desc ){
. init_cb = init ,
. frame_cb = frame ,
. cleanup_cb = cleanup ,
. window_title = "Rectangle (Sokol GP)" ,
. logger . func = slog_func ,
};
} Para executar este exemplo, primeiro copie o cabeçalho sokol_gp.h ao lado de outros cabeçalhos da Sokol para a mesma pasta e depois compilasse com qualquer compilador C usando os sinalizadores de vinculação adequados (leia sokol_gfx.h ).
Nas samples de pastas, você pode encontrar os seguintes exemplos completos, cobrindo todas as APIs da biblioteca:
sgp_begin() com buffers de quadros. Esses exemplos são usados como suíte de teste para a biblioteca, você pode construí -los digitando make .
É possível que, após muitas chamadas de desenho, o comando ou o buffer de vértice possa transbordar, nesse caso, a biblioteca definirá um estado de erro de erro e continuará operando normalmente, mas ao lavar a fila de comando de desenho com sgp_flush() nenhum comando de draw será despachado. Isso pode acontecer porque a biblioteca usa buffers pré -alocados; nesses casos, o problema pode ser corrigido aumentando o buffer de fila de comando prefixado e o buffer de vértices ao chamar sgp_setup() .
Fazer o número inválido de push/pops de sgp_push_transform() e sgp_pop_transform() , ou aninhar muitos sgp_begin() e sgp_end() também podem levar a erros, isso é um erro de uso.
Você pode ativar a macro SOKOL_DEBUG nesses casos para depurar ou lidar com o erro programaticamente lendo sgp_get_last_error() depois de ligar sgp_end() . Também é aconselhado deixar SOKOL_DEBUG ativado ao desenvolver com o Sokol, para que você possa pegar erros mais cedo.
A biblioteca suporta os modos de mistura mais usuais usados em 2D, que são os seguintes:
SGP_BLENDMODE_NONE - sem mistura ( dstRGBA = srcRGBA ).SGP_BLENDMODE_BLEND -Alpha Blending ( dstRGB = (srcRGB * srcA) + (dstRGB * (1-srcA)) e dstA = srcA + (dstA * (1-srcA)) )SGP_BLENDMODE_BLEND_PREMULTIPLIED -Alpha Blending pré-multiplicada ( dstRGBA = srcRGBA + (dstRGBA * (1-srcA)) )SGP_BLENDMODE_ADD - mistura aditiva ( dstRGB = (srcRGB * srcA) + dstRGB e dstA = dstA )SGP_BLENDMODE_ADD_PREMULTIPLIED - mistura aditiva pré -multiplicada ( dstRGB = srcRGB + dstRGB e dstA = dstA )SGP_BLENDMODE_MOD - MODULULE DE COLOR ( dstRGB = srcRGB * dstRGB e dstA = dstA )SGP_BLENDMODE_MUL -Multiplicação de cores ( dstRGB = (srcRGB * dstRGB) + (dstRGB * (1-srcA)) e dstA = (srcA * dstA) + (dstA * (1-srcA)) ) Você pode alterar a área da tela para desenhar chamando sgp_viewport(x, y, width, height) . Você pode alterar o sistema de coordenadas do espaço 2D chamando sgp_project(left, right, top, bottom) , com ele.
Você pode traduzir, girar ou escalar o espaço 2D antes de uma chamada de desenho, usando as funções de transformação que a biblioteca fornece, como sgp_translate(x, y) , sgp_rotate(theta) , etc. Verifique a folha de dicas ou o cabeçalho para mais.
Para salvar e restaurar o estado de transformação, você deve ligar para sgp_push_transform() e posteriormente sgp_pop_transform() .
A biblioteca fornece funções de desenho para todas as primitivas básicas, isto é, para pontos, linhas, triângulos e retângulos, como sgp_draw_line() e sgp_draw_filled_rect() . Verifique a folha de dicas ou o cabeçalho para mais. Todos eles têm variações em lote.
Para desenhar retângulos texturizados, você pode usar sgp_set_image(0, img) e, em seguida sgp_draw_filled_rect() , isso atrairá uma textura inteira em um retângulo. Posteriormente, você deve redefinir a imagem com sgp_reset_image(0) para restaurar a imagem ligada à imagem branca padrão, caso contrário, você terá falhas ao desenhar uma cor sólida.
Caso você queira desenhar uma fonte específica da textura, use sgp_draw_textured_rect() .
Por padrão, as texturas são desenhadas usando um amostrador de filtro mais próximo, você pode alterar o amostrador com sgp_set_sampler(0, smp) antes de desenhar uma textura, é recomendável restaurar o amostrador padrão usando sgp_reset_sampler(0) .
Todos os pipelines comuns têm modulação de cores e você pode modular uma cor antes de um desenho definir a cor do estado atual com sgp_set_color(r,g,b,a) , mais tarde você deve redefinir a cor para padrão (branco) com sgp_reset_color() .
Ao usar um shader personalizado, você deve criar um pipeline para ele com sgp_make_pipeline(desc) , usando shader, modo de mistura e um Draw Primitive associado a ele. Em seguida, você deve ligar para sgp_set_pipeline() antes do shader Draw Call. Você é responsável por usar o mesmo modo de mistura e desenhar primitivo como o pipeline criado.
Os uniformes personalizados podem ser passados para o shader com sgp_set_uniform(vs_data, vs_size, fs_data, fs_size) , onde você sempre deve passar um ponteiro para uma estrutura com exatamente o mesmo esquema e tamanho que o definido nos shaders de vertex e fragmento.
Embora você possa criar shaders personalizados para cada back -end de gráficos manualmente, é aconselhável usar o Sokol Shader Compiler SHDC, porque ele pode gerar shaders para vários back -ends a partir de um único arquivo .glsl , e isso geralmente funciona bem.
Por padrão, o buffer uniforme da biblioteca por chamada de desenho possui apenas 8 uniformes de flutuação ( SGP_UNIFORM_CONTENT_SLOTS Configuration), e isso pode ser muito baixo para usar com shaders personalizados. Esse é o padrão, porque normalmente os recém -chegados podem não querer usar shaders 2D personalizados e aumentar um valor maior significa mais sobrecarga. Se você estiver usando shaders personalizados, aumente esse valor para ser grande o suficiente para manter o número de uniformes do seu maior shader.
As macros a seguir podem ser definidas antes de incluir a alteração do comportamento da biblioteca:
SGP_BATCH_OPTIMIZER_DEPTH - Número de comandos de desenho que o otimizador de lote olha para trás. O padrão é 8.SGP_UNIFORM_CONTENT_SLOTS - Número máximo de carros alegóricos que podem ser armazenados em cada buffer uniforme de chamada. O padrão é 8.SGP_TEXTURE_SLOTS - Número máximo de texturas que podem ser vinculadas por chamada de draw. O padrão é 4. MIT, consulte o arquivo de licença ou o final do arquivo sokol_gp.h .
Aqui está uma lista rápida de todas as funções da biblioteca para referência rápida:
/* Initialization and de-initialization. */
void sgp_setup ( const sgp_desc * desc ); /* Initializes the SGP context, and should be called after `sg_setup`. */
void sgp_shutdown ( void ); /* Destroys the SGP context. */
bool sgp_is_valid ( void ); /* Checks if SGP context is valid, should be checked after `sgp_setup`. */
/* Error handling. */
sgp_error sgp_get_last_error ( void ); /* Returns last SGP error. */
const char * sgp_get_error_message ( sgp_error error ); /* Returns a message with SGP error description. */
/* Custom pipeline creation. */
sg_pipeline sgp_make_pipeline ( const sgp_pipeline_desc * desc ); /* Creates a custom shader pipeline to be used with SGP. */
/* Draw command queue management. */
void sgp_begin ( int width , int height ); /* Begins a new SGP draw command queue. */
void sgp_flush ( void ); /* Dispatch current Sokol GFX draw commands. */
void sgp_end ( void ); /* End current draw command queue, discarding it. */
/* 2D coordinate space projection */
void sgp_project ( float left , float right , float top , float bottom ); /* Set the coordinate space boundary in the current viewport. */
void sgp_reset_project ( void ); /* Resets the coordinate space to default (coordinate of the viewport). */
/* 2D coordinate space transformation. */
void sgp_push_transform ( void ); /* Saves current transform matrix, to be restored later with a pop. */
void sgp_pop_transform ( void ); /* Restore transform matrix to the same value of the last push. */
void sgp_reset_transform ( void ); /* Resets the transform matrix to identity (no transform). */
void sgp_translate ( float x , float y ); /* Translates the 2D coordinate space. */
void sgp_rotate ( float theta ); /* Rotates the 2D coordinate space around the origin. */
void sgp_rotate_at ( float theta , float x , float y ); /* Rotates the 2D coordinate space around a point. */
void sgp_scale ( float sx , float sy ); /* Scales the 2D coordinate space around the origin. */
void sgp_scale_at ( float sx , float sy , float x , float y ); /* Scales the 2D coordinate space around a point. */
/* State change for custom pipelines. */
void sgp_set_pipeline ( sg_pipeline pipeline ); /* Sets current draw pipeline. */
void sgp_reset_pipeline ( void ); /* Resets to the current draw pipeline to default (builtin pipelines). */
void sgp_set_uniform ( const void * vs_data , uint32_t vs_size , const void * fs_data , uint32_t fs_size ); /* Sets uniform buffer for a custom pipeline. */
void sgp_reset_uniform ( void ); /* Resets uniform buffer to default (current state color). */
/* State change functions for the common pipelines. */
void sgp_set_blend_mode ( sgp_blend_mode blend_mode ); /* Sets current blend mode. */
void sgp_reset_blend_mode ( void ); /* Resets current blend mode to default (no blending). */
void sgp_set_color ( float r , float g , float b , float a ); /* Sets current color modulation. */
void sgp_reset_color ( void ); /* Resets current color modulation to default (white). */
void sgp_set_image ( int channel , sg_image image ); /* Sets current bound image in a texture channel. */
void sgp_unset_image ( int channel ); /* Remove current bound image in a texture channel (no texture). */
void sgp_reset_image ( int channel ); /* Resets current bound image in a texture channel to default (white texture). */
void sgp_set_sampler ( int channel , sg_sampler sampler ); /* Sets current bound sampler in a texture channel. */
void sgp_unset_sampler ( int channel ); /* Remove current bound sampler in a texture channel (no sampler). */
void sgp_reset_sampler ( int channel ); /* Resets current bound sampler in a texture channel to default (nearest sampler). */
/* State change functions for all pipelines. */
void sgp_viewport ( int x , int y , int w , int h ); /* Sets the screen area to draw into. */
void sgp_reset_viewport ( void ); /* Reset viewport to default values (0, 0, width, height). */
void sgp_scissor ( int x , int y , int w , int h ); /* Set clip rectangle in the viewport. */
void sgp_reset_scissor ( void ); /* Resets clip rectangle to default (viewport bounds). */
void sgp_reset_state ( void ); /* Reset all state to default values. */
/* Drawing functions. */
void sgp_clear ( void ); /* Clears the current viewport using the current state color. */
void sgp_draw ( sg_primitive_type primitive_type , const sgp_vertex * vertices , uint32_t count ); /* Low level drawing function, capable of drawing any primitive. */
void sgp_draw_points ( const sgp_point * points , uint32_t count ); /* Draws points in a batch. */
void sgp_draw_point ( float x , float y ); /* Draws a single point. */
void sgp_draw_lines ( const sgp_line * lines , uint32_t count ); /* Draws lines in a batch. */
void sgp_draw_line ( float ax , float ay , float bx , float by ); /* Draws a single line. */
void sgp_draw_lines_strip ( const sgp_point * points , uint32_t count ); /* Draws a strip of lines. */
void sgp_draw_filled_triangles ( const sgp_triangle * triangles , uint32_t count ); /* Draws triangles in a batch. */
void sgp_draw_filled_triangle ( float ax , float ay , float bx , float by , float cx , float cy ); /* Draws a single triangle. */
void sgp_draw_filled_triangles_strip ( const sgp_point * points , uint32_t count ); /* Draws strip of triangles. */
void sgp_draw_filled_rects ( const sgp_rect * rects , uint32_t count ); /* Draws a batch of rectangles. */
void sgp_draw_filled_rect ( float x , float y , float w , float h ); /* Draws a single rectangle. */
void sgp_draw_textured_rects ( int channel , const sgp_textured_rect * rects , uint32_t count ); /* Draws a batch textured rectangle, each from a source region. */
void sgp_draw_textured_rect ( int channel , sgp_rect dest_rect , sgp_rect src_rect ); /* Draws a single textured rectangle from a source region. */
/* Querying functions. */
sgp_state * sgp_query_state ( void ); /* Returns the current draw state. */
sgp_desc sgp_query_desc ( void ); /* Returns description of the current SGP context. */ Esta biblioteca foi testada desde 2020 em projetos privados e provou ser estável.
Esta biblioteca foi originalmente patrocinada pela MMORPG Game Medivia Online, gostaria de agradecer a eles por apoiar meu trabalho.
Obrigado @kkukshtel por patrocinar chamadas de sorteio com diferentes características de cores.
Certifique -se de verificar o excelente projeto SOKOL da @floooh, ele apresenta muitas bibliotecas C de cabeçalho único útil, feitas com qualidade que pode ser usada para o desenvolvimento de jogos.
Você também pode verificar o meu outro único cabeçalho C da biblioteca Minicoro, ele traz coroutinas empilhadas para C, muito útil para simplificar as máquinas de estado finitas no jogo de jogo.
sgp_set_uniform , adicionou novos modos de mistura pré-multiplicados. Aqui estão algumas capturas de tela de todas as amostras no diretório samples . Clique em qualquer uma das imagens para vê -la realmente renderizada em tempo real no seu navegador.
Amostra primitivos:
Modos de mistura amostras:
Amostra de buffer de quadro:
Amostra de retângulo:
Amostra de efeito:
Amostra SDF:
Sou um desenvolvedor de código aberto em tempo integral, qualquer quantidade da doação através do meu Github será apreciada e pode me incentivar para continuar apoiando esse e outros projetos de código aberto. Posso aceitar patrocínios únicos para pequenos recursos ou aprimoramentos menores alinhados com as metas do projeto, neste caso, entre em contato comigo.