Painter graphique 2D Minimal Efficient Cross Plateforme en C pur en utilisant l'API graphique moderne via l'excellente bibliothèque SOKOL GFX.
Sokol GP, ou en SGP court, signifie Sokol Graphics Painter.
Sokol GFX est une excellente bibliothèque pour le rendu à l'aide de pipelines non fixés de cartes graphiques modernes, mais elle est trop complexe à utiliser pour un dessin 2D simple, et son API est trop générique et spécialisée pour le rendu 3D. Pour dessiner des trucs 2D, le programmeur doit généralement configurer les shaders personnalisés lors de l'utilisation de Sokol GFX, ou utiliser sa bibliothèque Sokol GL Extra, mais Sokol GL a également une API avec une conception 3D à l'esprit, qui entraîne certains coûts et limitations.
Cette bibliothèque a été créée pour dessiner des primitives 2D via Sokol GFX avec facilité, et en ne considérant pas l'utilisation 3D, il est optimisé pour le rendu 2D uniquement, en outre, il dispose d'un optimiseur de lots automatique , plus de détails seront décrits ci-dessous.
Lorsque le dessin de la bibliothèque crée une file d'attente de commandes de tirage de toutes les primitives à dessiner, chaque fois qu'une nouvelle commande de tirage est ajoutée, l'optimiseur par lots regarde vers les 8 dernières commandes de dessin récentes (ceci est réglable) et essayez de réorganiser et de fusionner les commandes de dessin si elle trouve une commande de tirage précédente qui répond aux critères suivants:
En faisant cela, l'optimiseur de lots est par exemple pour fusionner des appels de dessin texturés, même s'ils ont été dessinés avec d'autres textures différentes intermédiaires entre eux. L'effet est plus efficace lors du dessin, car moins d'appels à dessiner seront expédiés au GPU,
Cette bibliothèque peut éviter beaucoup de travail de fabrication d'un système de lot de dessin 2D efficace, en fusionnant automatiquement les appels de dessin dans les coulisses de l'exécution, donc le programmeur n'a pas besoin de gérer les appels de tirage par lots manuellement, ni qu'il doit trier les appels de texture par lots, la bibliothèque le fera parfaitement dans les coulisses.
L'algorithme par lots est rapide, mais il a une complexité de CPU O(n) pour chaque nouvelle commande de tirage ajoutée, où n est la configuration SGP_BATCH_OPTIMIZER_DEPTH . Dans les expériences utilisant 8 comme défaut, c'est une bonne valeur par défaut, mais vous voudrez peut-être essayer différentes valeurs en fonction de votre cas. L'utilisation de valeurs trop élevées n'est pas recommandée, car l'algorithme peut prendre trop de commandes de tirage précédentes à balayage, et qui peuvent consommer plus de ressources CPU.
L'optimiseur de lots peut être désactivé en définissant SGP_BATCH_OPTIMIZER_DEPTH sur 0, vous pouvez l'utiliser pour mesurer son impact.
Dans le répertoire des échantillons de ce référentiel, il existe un exemple de référence qui teste le dessin avec l'optimiseur de bain activé / désactivé. Sur ma machine, cette référence a pu augmenter les performances dans un facteur de 2,2x lorsqu'elle est activée. Dans certains projets de jeux privés, les gains de l'optimiseur par lots ont avéré augmenter les performances FPS au-dessus de 1,5x en remplaçant simplement le backend graphique par cette bibliothèque, sans modifications internes du jeu lui-même.
La bibliothèque a des choix de conception avec des performances à l'esprit qui seront discutées brièvement ici.
Comme Sokol GFX, Sokol GP ne fera jamais aucune allocation dans la boucle de dessin, donc lors de l'initialisation, vous devez configurer au préalable la taille maximale du tampon de file d'attente de commande de tirage et du tampon de sommets.
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 <-> Bus GPU. En revanche, les applications 3D dépassent généralement les transformations de sommet en GPU à l'aide d'un shader de sommet, elles le font parce que la quantité de sommets d'objets 3D peut être très grande et c'est généralement le meilleur choix, mais ce n'est pas vrai pour le rendu 2D.
De nombreuses API pour transformer l'espace 2D avant de dessiner une primitive sont disponibles, telles que la traduction, la rotation et l'échelle. Ils peuvent être utilisés aussi de la même manière que ceux disponibles dans les API graphiques 3D, mais ils sont fabriqués pour 2D uniquement, par exemple lorsque vous utilisez 2D, nous n'avons pas besoin d'utiliser une matrice 4x4 ou 3x3 pour effectuer une transformation du sommet, plutôt que le code est spécialisé pour 2D et peut utiliser une matrice 2x3, enregistrant des calculs flottants supplémentaires CPU.
Tous les pipelines utilisent toujours une texture qui lui est associée, même lors du dessin de primitives non texturées, car cela minimise le pipeline graphique change lors du mélange d'appels texturés et d'appels non texturés, améliorant l'efficacité.
La bibliothèque est codée dans le style des en-têtes Sokol GFX, réutilisant de nombreuses macros à partir de là, vous pouvez modifier certaines de ses sémantiques telles que l'allocateur personnalisé, la fonction de journal personnalisée et d'autres détails, lire sokol_gfx.h documentation pour en savoir plus à ce sujet.
Copiez sokol_gp.h avec d'autres en-têtes Sokol dans le même dossier. Configurez Sokol GFX comme vous le feriez habituellement, puis ajoutez un appel à sgp_setup(desc) juste après sg_setup(desc) , et appelez à sgp_shutdown() juste avant sg_shutdown() . Notez que vous devez généralement vérifier si SGP est valide après sa création avec sgp_is_valid() et sortir gracieusement avec une erreur sinon.
Dans votre fonction de dessin de trame, ajoutez sgp_begin(width, height) avant d'appeler une fonction de dessin SGP, puis de dessiner vos primitives. À la fin du cadre (ou framebuffer), vous devez toujours appeler sgp_flush() entre un Sokol GFX Begin / End Render Render, le sgp_flush() expédiera toutes les commandes de dessin vers Sokol GFX. Appelez ensuite sgp_end() immédiatement pour rejeter la file d'attente de commande de tirage.
Un exemple réel de cette configuration sera illustré ci-dessous.
Ce qui suit est un exemple rapide sur la façon de cette bibliothèque avec Sokol GFX et Sokol App:
// 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 ,
};
} Pour exécuter cet exemple, copiez d'abord l'en-tête sokol_gp.h aux côtés d'autres en-têtes SOKOL dans le même dossier, puis compilez avec n'importe quel compilateur C en utilisant les indicateurs de liaison appropriés (lire sokol_gfx.h ).
Dans samples de dossiers, vous pouvez trouver les exemples complets suivants couvrant toutes les API de la bibliothèque:
sgp_begin() avec des tampons de trame. Ces exemples sont utilisés comme suite de tests pour la bibliothèque, vous pouvez les construire en tapant make .
Il est possible qu'après que de nombreux appels appellent la commande ou le tampon de vertex puisse déborder, dans ce cas, la bibliothèque définira un état d'erreur d'erreur et continuera de fonctionner normalement, mais lors de la réduction de la file d'attente de commande de dessin avec sgp_flush() aucune commande de dessin ne sera expédiée. Cela peut se produire car la bibliothèque utilise des tampons pré-alloués, dans de tels cas, le problème peut être résolu en augmentant le tampon de file d'attente de commande préfixé et le tampon de sommets lors de l'appel sgp_setup() .
Faire un nombre non valide de push / pops de sgp_push_transform() et sgp_pop_transform() , ou nichez trop sgp_begin() et sgp_end() peut également conduire à des erreurs, c'est une erreur d'utilisation.
Vous pouvez activer la macro SOKOL_DEBUG dans de tels cas pour déboguer ou gérer l'erreur par programme en lisant sgp_get_last_error() après avoir appelé sgp_end() . Il est également conseillé de laisser SOKOL_DEBUG activé lors du développement avec Sokol, afin que vous puissiez prendre des erreurs tôt.
La bibliothèque prend en charge les modes de mélange les plus habituels utilisés en 2D, qui sont les suivants:
SGP_BLENDMODE_NONE - pas de mélange ( dstRGBA = srcRGBA ).SGP_BLENDMODE_BLEND - ALPHA BLENTING ( dstRGB = (srcRGB * srcA) + (dstRGB * (1-srcA)) et dstA = srcA + (dstA * (1-srcA)) )SGP_BLENDMODE_BLEND_PREMULTIPLIED - mélange alpha pré-multiplié ( dstRGBA = srcRGBA + (dstRGBA * (1-srcA)) )SGP_BLENDMODE_ADD - mélange additif ( dstRGB = (srcRGB * srcA) + dstRGB et dstA = dstA )SGP_BLENDMODE_ADD_PREMULTIPLIED - mélange additif pré-multiplié ( dstRGB = srcRGB + dstRGB et dstA = dstA )SGP_BLENDMODE_MOD - Color Modulate ( dstRGB = srcRGB * dstRGB et dstA = dstA )SGP_BLENDMODE_MUL - COULEUR Multiply ( dstRGB = (srcRGB * dstRGB) + (dstRGB * (1-srcA)) et dstA = (srcA * dstA) + (dstA * (1-srcA)) ) Vous pouvez modifier la zone de l'écran pour dessiner en appelant sgp_viewport(x, y, width, height) . Vous pouvez modifier le système de coordonnées de l'espace 2D en appelant sgp_project(left, right, top, bottom) , avec lui.
Vous pouvez traduire, faire tourner ou évoluer l'espace 2D avant un appel de dessin, en utilisant les fonctions de transformation fournies par la bibliothèque, telles que sgp_translate(x, y) , sgp_rotate(theta) , etc. Vérifiez la feuille de triche ou l'en-tête pour en savoir plus.
Pour enregistrer et restaurer l'état de transformation, vous devez appeler sgp_push_transform() et ultérieurement sgp_pop_transform() .
La bibliothèque fournit des fonctions de dessin pour toutes les primitives de base, c'est-à-dire pour les points, les lignes, les triangles et les rectangles, tels que sgp_draw_line() et sgp_draw_filled_rect() . Vérifiez la feuille de triche ou l'en-tête pour en savoir plus. Tous ont des variations par lots.
Pour dessiner des rectangles texturés, vous pouvez utiliser sgp_set_image(0, img) puis sgp_draw_filled_rect() , cela dessinera une texture entière dans un rectangle. Vous devez ensuite réinitialiser l'image avec sgp_reset_image(0) pour restaurer l'image liée à l'image blanche par défaut, sinon vous aurez des problèmes lorsque vous dessinez une couleur solide.
Dans le cas où vous souhaitez dessiner une source spécifique à partir de la texture, vous devez utiliser sgp_draw_textured_rect() .
Par défaut, les textures sont dessinées à l'aide d'un échantillonneur de filtre le plus proche le plus proche, vous pouvez modifier l'échantillonneur avec sgp_set_sampler(0, smp) Avant de dessiner une texture, il est recommandé de restaurer l'échantillonneur par défaut à l'aide de sgp_reset_sampler(0) .
Tous les pipelines communs ont une modulation de couleur, et vous pouvez moduler une couleur avant un dessin en définissant la couleur de l'état actuelle avec sgp_set_color(r,g,b,a) , plus tard, vous devez réinitialiser la couleur par défaut (blanc) avec sgp_reset_color() .
Lorsque vous utilisez un shader personnalisé, vous devez créer un pipeline pour celui-ci avec sgp_make_pipeline(desc) , à l'aide de Shader, Mode Blend et une primitive de dessin qui lui est associée. Ensuite, vous devez appeler sgp_set_pipeline() avant l'appel de dessin shader. Vous êtes responsable de l'utilisation du même mode de mélange et de dessin primitif que le pipeline créé.
Les uniformes personnalisés peuvent être transmis au shader avec sgp_set_uniform(vs_data, vs_size, fs_data, fs_size) , où vous devez toujours transmettre un pointeur vers une structure avec exactement le même schéma et la même taille que celui défini dans les shaders vertex et fragment.
Bien que vous puissiez créer des shaders personnalisés pour chaque backend graphique manuellement, il est conseillé d'utiliser le compilateur Sokol Shader SHDC, car il peut générer des shaders pour plusieurs backends à partir d'un seul fichier .glsl , et cela fonctionne généralement bien.
Par défaut, le tampon uniforme de la bibliothèque par appel de dessin n'a que 8 uniformes flottants ( SGP_UNIFORM_CONTENT_SLOTS Configuration), et cela peut être trop faible pour être utilisé avec des shaders personnalisés. Il s'agit de la valeur par défaut car généralement les nouveaux arrivants peuvent ne pas vouloir utiliser des shaders 2D personnalisés, et augmenter une valeur plus grande signifie plus de frais généraux. Si vous utilisez des shaders personnalisés, veuillez augmenter cette valeur pour être suffisamment grande pour contenir le nombre d'uniformes de votre plus grand shader.
Les macros suivantes peuvent être définies avant de modifier le comportement de la bibliothèque:
SGP_BATCH_OPTIMIZER_DEPTH - Nombre de commandes de dessin que l'optimiseur de lots regarde. La valeur par défaut est 8.SGP_UNIFORM_CONTENT_SLOTS - Nombre maximum de flotteurs qui peuvent être stockés dans chaque tampon uniforme d'appel. La valeur par défaut est 8.SGP_TEXTURE_SLOTS - nombre maximum de textures qui peuvent être liées par appel de dessin. La valeur par défaut est 4. MIT, voir le fichier de licence ou la fin du fichier sokol_gp.h .
Voici une liste rapide de toutes les fonctions de bibliothèque pour une référence rapide:
/* 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. */ Cette bibliothèque est testée depuis 2020 dans des projets privés et s'est avérée stable.
Cette bibliothèque a été parrainée à l'origine par le jeu MMORPG Medivia Online, je voudrais les remercier d'avoir soutenu mon travail.
Merci @kkukshtel d'avoir parrainé des appels de dessin par lots avec différentes fonctionnalités de couleurs.
Assurez-vous de vérifier l'excellent projet SOKOL par @floooh, il dispose de nombreuses bibliothèques en en-tête unique C composées de qualité qui peut être utilisée pour le développement de jeux.
Vous voudrez peut-être également vérifier mon autre en-tête unique C Library Minicoro, il apporte des coroutines Stackful pour C, très utile pour simplifier les machines d'État finies dans le Devlopment de jeu.
sgp_set_uniform , ajoutait de nouveaux modes de mélange pré-multiplié. Voici quelques captures d'écran de tous les échantillons dans le répertoire samples . Cliquez sur l'une des images pour afficher qu'elle soit réellement rendue en temps réel dans votre navigateur.
Échantillon de primitives:
Échantillons de modes de mélange:
Échantillon de tampon de cadre:
Échantillon de rectangle:
Échantillon d'effet:
Échantillon SDF:
Je suis un développeur open source à plein temps, tout montant de don via mon github sera apprécié et pourrait m'encourager pour continuer à soutenir ce projets et d'autres projets open source. Je peux accepter des parrainages ponctuels pour de petites fonctionnalités ou des améliorations mineures alignées sur les objectifs du projet, dans ce cas, contactez-moi.