Pintor de gráficos 2D de plataforma multiplataforma mínima eficiente en C Pure C utilizando una API de gráficos modernos a través de la excelente biblioteca Sokol GFX.
Sokol GP, o en SGP corto, significa Sokol Graphics Painter.
Sokol GFX es una excelente biblioteca para el uso de tuberías no fijadas de tarjetas gráficas modernas, pero es demasiado complejo de usar para un dibujo 2D simple, y su API es demasiado genérica y especializada para la representación 3D. Para dibujar cosas 2D, el programador generalmente necesita configurar sombreadores personalizados cuando usan Sokol GFX, o usar su biblioteca Sokol GL Extra, pero Sokol GL también tiene una API con diseño 3D en mente, que incurre en algunos costos y limitaciones.
Esta biblioteca fue creada para dibujar primitivas 2D a través de Sokol GFX con facilidad, y al no considerar el uso 3D, se optimiza solo para representación 2D, además presenta un optimizador de lotes automático , se describirán más detalles a continuación.
Al dibujar la biblioteca crea una cola de comando de dibujo de todas las primitivas aún por dibujar, cada vez que se agrega un nuevo comando de dibujo, el optimizador de lotes mira hacia atrás hasta los últimos 8 comandos de dibujo recientes (esto es ajustable) e intente reorganizar y fusionar comandos de dibujo si encuentra un comando de dibujo anterior que cumple con los siguientes criterios:
Al hacer esto, el optimizador por lotes puede, por ejemplo, fusionar llamadas de dibujo texturizado, incluso si se dibujaban con otras texturas intermediarias diferentes entre ellas. El efecto es más eficiencia al dibujar, porque se enviarán menos llamadas de sorteo a la GPU,
Esta biblioteca puede evitar mucho trabajo para hacer un sistema de lotes de dibujo 2D eficiente, fusionando automáticamente las llamadas de dibujo detrás de escena en tiempo de ejecución, por lo que el programador no necesita administrar llamadas de dibujo por lotes manualmente, ni necesita ordenar llamadas de dibujo de textura lotes, la biblioteca lo hará sin cosis detrás de las escenas.
El algoritmo de lotes es rápido, pero tiene una complejidad de CPU O(n) para cada nuevo comando de dibujo agregado, donde n es la configuración SGP_BATCH_OPTIMIZER_DEPTH . En los experimentos que usan 8 como predeterminado, es un buen valor predeterminado, pero es posible que desee probar diferentes valores según su caso. No se recomienda usar valores demasiado altos, porque el algoritmo puede llevar demasiado tiempo escaneando comandos de dibujo anteriores, y eso puede consumir más recursos de CPU.
El optimizador de lotes se puede deshabilitar configurando SGP_BATCH_OPTIMIZER_DEPTH a 0, puede usarlo para medir su impacto.
En el directorio de muestras de este repositorio hay un ejemplo de referencia que prueba el dibujo con el optimizador de baño habilitado/deshabilitado. En mi máquina, ese punto de referencia pudo aumentar el rendimiento en un factor de 2.2x cuando está habilitado. En algunos proyectos de juegos privados, las ganancias del optimizador de lotes demostraron aumentar el rendimiento de FPS por encima de 1.5x simplemente reemplazando el backend gráficos con esta biblioteca, sin cambios internos en el juego en sí.
La biblioteca tiene algunas opciones de diseño teniendo en cuenta el rendimiento que se discutirán brevemente aquí.
Al igual que Sokol GFX, Sokol GP nunca hará ninguna asignación en el bucle de dibujo, por lo que al inicializar debe configurar de antemano el tamaño máximo del búfer de cola de comando de dibujo y el búfer de vértices.
La CPU realiza toda la transformación del espacio 2D (funciones como sgp_rotate ) y no por la GPU, esto es intencionalmente para evitar agregar sobrecarga adicional en la GPU, porque generalmente el número de vértices de las aplicaciones 2D no es tan grande, y es más eficiente realizar toda la transformación con la CPU a la derecha en lugar de impulsar los buffers adicionales a los buffers adicionales que termina con un nivel más importante que termina con un p. CPU <-> BUS GPU. En contraste, las aplicaciones 3D generalmente envían transformaciones de vértice a la GPU usando un sombreador de vértice, lo hacen porque la cantidad de vértices de objetos 3D puede ser muy grande y generalmente es la mejor opción, pero esto no es cierto para la representación 2D.
Muchas API para transformar el espacio 2D antes de dibujar un primitivo están disponibles, como traducir, rotar y escala. Se pueden usar de manera tan similar a las disponibles en las API de gráficos 3D, pero están diseñados solo para 2D, por ejemplo, por ejemplo, cuando se usa 2D, no necesitamos usar una matriz 4x4 o 3x3 para realizar la transformación de vértices, en su lugar, el código está especializado para 2D y podemos usar una matriz 2x3, guardar las computaciones de flotación CPU adicionales.
Todas las tuberías siempre usan una textura asociada con ella, incluso cuando se dibujan primitivas no texturizadas, porque esto minimiza los cambios en los gráficos de la tubería al mezclar llamadas texturizadas y llamadas no texturizadas, mejorando la eficiencia.
La biblioteca está codificada al estilo de los encabezados Sokol GFX, reutilizando muchas macros desde allí, puede cambiar parte de su semántica, como el asignador personalizado, la función de registro personalizado y algunos otros detalles, leer la documentación sokol_gfx.h para obtener más información sobre eso.
Copie sokol_gp.h junto con otros encabezados de Sokol a la misma carpeta. Configurar Sokol GFX como lo haría, luego agregue la llamada a sgp_setup(desc) justo después de sg_setup(desc) y llame a sgp_shutdown() justo antes de sg_shutdown() . Tenga en cuenta que generalmente debe verificar si SGP es válido después de su creación con sgp_is_valid() y salir con gracia con un error si no.
En su función de dibujo de cuadro, agregue sgp_begin(width, height) antes de llamar a cualquier función de dibujo SGP, luego dibuje sus primitivas. Al final del marco (o FrameBuffer), siempre debe llamar sgp_flush() entre un pase de renderizado Sokol GFX Begin/End, el sgp_flush() enviará todos los comandos de dibujo a Sokol GFX. Luego llame sgp_end() inmediatamente para descartar la cola de comando Draw.
A continuación se mostrará un ejemplo real de esta configuración.
El siguiente es un ejemplo rápido sobre cómo esta biblioteca con la aplicación Sokol GFX y 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 ejecutar este ejemplo, primero copie el encabezado sokol_gp.h junto con otros encabezados Sokol a la misma carpeta, luego compile con cualquier compilador C utilizando los indicadores de enlace adecuados (lea sokol_gfx.h ).
En samples de carpetas, puede encontrar los siguientes ejemplos completos que cubren todas las API de la biblioteca:
sgp_begin() con buffers de cuadro. Estos ejemplos se utilizan como suite de prueba para la biblioteca, puede construirlos make .
Es posible que después de muchas llamadas de dibujo, el comando o el búfer de vértice puedan desbordarse, en ese caso, la biblioteca establecerá un estado de error de error y continuará funcionando normalmente, pero al descargar la cola de comando de dibujo con sgp_flush() no se enviará ningún comando de dibujo. Esto puede suceder porque la biblioteca usa búferes prefiridos, en tales casos, el problema se puede solucionar aumentando el búfer de cola de comando prefijado y el búfer de vértices al llamar sgp_setup() .
Hacer un número inválido de push/pops de sgp_push_transform() y sgp_pop_transform() , o anidar demasiados sgp_begin() y sgp_end() también puede conducir a errores, eso es un error de uso.
Puede habilitar la macro SOKOL_DEBUG en tales casos para depurar, o manejar el error programáticamente leyendo sgp_get_last_error() después de llamar sgp_end() . También se recomienda dejar habilitado SOKOL_DEBUG cuando se desarrolle con Sokol, para que pueda captar errores temprano.
La biblioteca admite los modos de mezcla más habituales utilizados en 2D, que son los siguientes:
SGP_BLENDMODE_NONE - sin mezcla ( dstRGBA = srcRGBA ).SGP_BLENDMODE_BLEND -Alpha Blending ( dstRGB = (srcRGB * srcA) + (dstRGB * (1-srcA)) y dstA = srcA + (dstA * (1-srcA)) )SGP_BLENDMODE_BLEND_PREMULTIPLIED -mezcla alfa pre-multiplicada ( dstRGBA = srcRGBA + (dstRGBA * (1-srcA)) ))SGP_BLENDMODE_ADD - mezcla aditiva ( dstRGB = (srcRGB * srcA) + dstRGB y dstA = dstA )SGP_BLENDMODE_ADD_PREMULTIPLIED - mezcla aditiva pre -multiplicada ( dstRGB = srcRGB + dstRGB y dstA = dstA )SGP_BLENDMODE_MOD - color modulado ( dstRGB = srcRGB * dstRGB y dstA = dstA )SGP_BLENDMODE_MUL -Color Multiply ( dstRGB = (srcRGB * dstRGB) + (dstRGB * (1-srcA)) y dstA = (srcA * dstA) + (dstA * (1-srcA)) ) Puede cambiar el área de la pantalla para dibujar llamando a sgp_viewport(x, y, width, height) . Puede cambiar el sistema de coordenadas del espacio 2D llamando sgp_project(left, right, top, bottom) , con él.
Puede traducir, rotar o escalar el espacio 2D antes de una llamada de sorteo, utilizando las funciones de transformación que proporciona la biblioteca, como sgp_translate(x, y) , sgp_rotate(theta) , etc. Verifique la hoja de trucos o el encabezado para obtener más información.
Para guardar y restaurar el estado de transformación, debe llamar sgp_push_transform() y luego sgp_pop_transform() .
La biblioteca proporciona funciones de dibujo para todas las primitivas básicas, es decir, para puntos, líneas, triángulos y rectángulos, como sgp_draw_line() y sgp_draw_filled_rect() . Revise la hoja de trucos o el encabezado para obtener más información. Todos ellos tienen variaciones por lotes.
Para dibujar rectángulos texturizados, puede usar sgp_set_image(0, img) y luego sgp_draw_filled_rect() , esto dibujará una textura completa en un rectángulo. Más tarde debe restablecer la imagen con sgp_reset_image(0) para restaurar la imagen unida a la imagen blanca predeterminada, de lo contrario tendrá fallas al dibujar un color sólido.
En caso de que desee extraer una fuente específica de la textura, debe usar sgp_draw_textured_rect() .
Por defecto, las texturas se dibujan utilizando un muestreador de filtro más cercano más cercano, puede cambiar el muestreador con sgp_set_sampler(0, smp) Antes de dibujar una textura, se recomienda restaurar la muestra predeterminada usando sgp_reset_sampler(0) .
Todas las tuberías comunes tienen modulación de color, y puede modular un color antes de un sorteo estableciendo el color de estado actual con sgp_set_color(r,g,b,a) , luego debe restablecer el color a predeterminado (blanco) con sgp_reset_color() .
Al usar un sombreador personalizado, debe crear una tubería para él con sgp_make_pipeline(desc) , usando sombreador, modo de mezcla y un dibujo primitivo asociado con él. Entonces debe llamar sgp_set_pipeline() antes de la llamada de dibujo del sombreador. Usted es responsable de usar el mismo modo de mezcla y dibujar primitivo que la tubería creada.
Los uniformes personalizados se pueden pasar al sombreador con sgp_set_uniform(vs_data, vs_size, fs_data, fs_size) , donde siempre debe pasar un puntero a una estructura con exactamente el mismo esquema y tamaño que el definido en los sombreadores de vértices y fragmentos.
Aunque puede crear sombreadores personalizados para cada backend de gráficos manualmente, se recomienda que debe usar el compilador de sombreador Sokol SHDC, ya que puede generar sombreadores para múltiples backends desde un solo archivo .glsl , y esto generalmente funciona bien.
Por defecto, el búfer de uniforme de biblioteca por llamada tiene solo 8 uniformes de flotación (configuración SGP_UNIFORM_CONTENT_SLOTS ), y eso puede ser demasiado bajo de usar con sombreadores personalizados. Este es el valor predeterminado porque, por lo general, los recién llegados pueden no querer usar sombreadores 2D personalizados, y aumentar un valor mayor significa más gastos generales. Si está utilizando sombreadores personalizados, aumente este valor para que sea lo suficientemente grande como para contener la cantidad de uniformes de su sombreador más grande.
Las siguientes macros se pueden definir antes de incluir para cambiar el comportamiento de la biblioteca:
SGP_BATCH_OPTIMIZER_DEPTH - Número de comandos de dibujo que el optimizador de lotes mira hacia atrás. El valor predeterminado es 8.SGP_UNIFORM_CONTENT_SLOTS - número máximo de flotadores que se pueden almacenar en cada búfer uniforme de llamadas de dibujo. El valor predeterminado es 8.SGP_TEXTURE_SLOTS - Número máximo de texturas que se pueden unir por llamada de dibujo. El valor predeterminado es 4. MIT, consulte el archivo de licencia o el final del archivo sokol_gp.h .
Aquí hay una lista rápida de todas las funciones de la biblioteca para una referencia 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 ha sido probada desde 2020 en proyectos privados, y ha demostrado ser estable.
Esta biblioteca ha sido patrocinada originalmente por el juego MMORPG Medivia Online, me gustaría agradecerles por apoyar mi trabajo.
Gracias @kkukshtel por patrocinar las llamadas de dibujo por lotes con una función de colores diferentes.
Asegúrese de visitar el excelente proyecto Sokol de @floooh, presenta muchas bibliotecas C de encabezado único útiles hechas con calidad que se puede utilizar para el desarrollo de juegos.
También es posible que desee consultar mi otro minicoro de la biblioteca C de Otro encabezado único, trae coroutinas de pila para C, muy útil para simplificar las máquinas de estado finito en el desarrollo del juego.
sgp_set_uniform , agregó nuevos modos de mezcla pre-multiplicados. Aquí hay algunas capturas de pantalla de todas las muestras en el directorio samples . Haga clic en cualquiera de las imágenes para verlo que realmente se presente en tiempo real en su navegador.
Muestra primitiva:
Modos de mezcla Muestras:
Muestra de búfer de marco:
Muestra de rectángulo:
Muestra de efecto:
Muestra de SDF:
Soy un desarrollador de código abierto a tiempo completo, cualquier cantidad de donación a través de mi GitHub será apreciado y podría traerme aliento para seguir apoyando este y otros proyectos de código abierto. Puedo aceptar patrocinios únicos para características pequeñas o mejoras menores alineadas con los objetivos del proyecto, en este caso contactarme.