Минимальный эффективный кросс -платформный 2D -графический художник в Pure C с использованием современной графики API через превосходную библиотеку Sokol GFX.
Sokol GP, или в короткой SGP, обозначает Sokol Graphics Painter.
Sokol GFX - отличная библиотека для рендеринга с использованием нефиксированных трубопроводов современных видеокарт, но он слишком сложный для использования для простого 2D -рисунка, и его API слишком общий и специализирован для 3D -рендеринга. Чтобы нарисовать 2D -материал, программисту обычно необходимо настраивать пользовательские шейдеры при использовании Sokol GFX или использования своей дополнительной библиотеки Sokol GL, но Sokol GL также имеет API с учетом 3D -дизайна, который прилагает некоторые затраты и ограничения.
Эта библиотека была создана для того, чтобы с легкостью нарисовать 2D -примитивы через Sokol GFX, и, не рассматривая 3D -использование, она оптимизирована только для 2D -рендеринга, кроме того, она имеет автоматический оптимизатор партии , более подробная информация будет описана ниже.
При рисовании библиотека создает очередь команд рисования всех примитивов, которые еще предстоит нарисовать, каждый раз, когда добавляется новая команда Draw, оптимизатор пакета оглядывается до последних 8 недавних команд Draw (это регулируется), и пытайтесь изменить и объединить команды рисования, если она находит предыдущую команду Draw, которая соответствует следующим критериям:
Сделав это, пакетный оптимизатор способен, например, объединять текстурированные вызовы, даже если между ними были нарисованы другие посреднические текстуры. Эффект более эффективен при рисовании, потому что меньше вызовов будет направлено на графический процессор,
Эта библиотека может избежать большой работы по созданию эффективной системы партии 2D рисунка, автоматически объединяя вызовы рисунков за кулисами во время выполнения, поэтому программисту не нужно управлять вызовами вручную вручную, и ему не нужно сортировать вызовы с пакетированными текстурами, библиотека будет плавно за кулисами.
Алгоритм пакетирования быстрый, но он имеет сложность CPU O(n) для каждой новой команды Draw, где n - конфигурация SGP_BATCH_OPTIMIZER_DEPTH . В экспериментах с использованием 8 в качестве дефолта - хороший дефолт, но вы можете попробовать разные значения в зависимости от вашего случая. Использование значений, которые слишком высоки, не рекомендуется, потому что алгоритм может занимать слишком длительное сканирование предыдущих команд розыгрыша, и это может потреблять больше ресурсов процессора.
Оптимизатор партии может быть отключен, установив SGP_BATCH_OPTIMIZER_DEPTH на 0, вы можете использовать его для измерения его воздействия.
В каталоге образцов этого хранилища есть эталонный пример, который проверяет рисунок с включенным/отключенным оптимизатором ванны. На моей машине эталона смог повысить производительность в 2,2 -кратном факторе, когда он включен. В некоторых частных игровых проектах прибыль оптимизатора партии оказался увеличивающим производительность FPS выше 1,5x, просто заменив бэкэнд Graphics на эту библиотеку, без внутренних изменений в самой игре.
Библиотека имеет некоторые варианты дизайна с учетом производительности, который будет обсуждаться здесь кратко.
Как и Sokol GFX, Sokol GP никогда не будет делать никакого распределения в цикле Draw, поэтому при инициализации вы должны заранее настроить максимальный размер буфера очереди команды Draw и буфера вершин.
Все 2D -трансформация пространства (такие функции, как sgp_rotate ), выполняются процессором, а не с помощью графического процессора, это намеренно избегать добавления дополнительных накладных расходов в графический процесс, потому что обычно количество вершин 2D -применений не так много, и это более эффективно выполнять все преобразование с помощью CPU сразу же, что в конце концов, в то время как gp ЦП <--> Автобус графического процессора. Напротив, 3D -приложения обычно отправляют преобразования вершины в графический процессор с помощью шейдера вершины, они делают это, потому что количество вершин 3D -объектов может быть очень большим, и это обычно лучший выбор, но это не так для 2D -рендеринга.
Доступны много API для преобразования 2D -пространства перед тем, как рисовать примитив, например, перевод, вращение и масштаб. Их можно использовать так же, как и те, которые доступны в API 3D Graphics, но они созданы только для 2D, например, при использовании 2D нам не нужно использовать матрицу 4x4 или 3x3 для выполнения трансформации вершины, вместо этого код специализируется на 2D и может использовать матрицу 2x3, что со сэкономив дополнительные вычисления с плавающей заплавой CPU.
Все трубопроводы всегда используют связанную с ней текстуру, даже при рисовании не текстурированных примитивов, потому что это сводит к минимуму изменения графического трубопровода при смешивании текстурированных вызовов и не текстурированных вызовов, повышая эффективность.
Библиотека кодирована в стиле заголовков Sokol GFX, повторно используя многие макросы оттуда, вы можете изменить некоторые из его семантики, таких как пользовательские выплаты, пользовательскую функцию журнала и некоторые другие детали, прочитать документацию sokol_gfx.h для получения дополнительной информации об этом.
Скопируйте sokol_gp.h вместе с другими заголовками Sokol в ту же папку. Настройка Sokol GFX, как обычно, затем добавьте вызов в sgp_setup(desc) сразу после sg_setup(desc) и позвоните в sgp_shutdown() перед sg_shutdown() . Обратите внимание, что обычно следует проверить, действите ли SGP после его создания с sgp_is_valid() и изящно выйти с ошибкой, если нет.
В вашей функции рисования в рамке добавьте sgp_begin(width, height) прежде чем вызовать любую функцию рисования SGP, затем нарисуйте свои примитивы. В конце кадры (или кадриста) вы всегда должны вызывать sgp_flush() между проходом Sokol GFX Begin/End Render, sgp_flush() будет отправлять все команды рисования Sokol GFX. Затем вызовите sgp_end() немедленно, чтобы отказаться от очереди команды Draw.
Фактический пример этой настройки будет показан ниже.
Ниже приведен быстрый пример того, как эта библиотека с приложением Sokol GFX и 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 ,
};
} Чтобы запустить этот пример, сначала скопируйте заголовок sokol_gp.h вместе с другими заголовками Sokol в одну и ту же папку, затем компилируйте с любым компилятором C, используя правильные флаги связывания (прочитайте sokol_gfx.h ).
В samples папок вы можете найти следующие полные примеры, охватывающие все API библиотеки:
sgp_begin() с кадтными буферами. Эти примеры используются в качестве тестового набора для библиотеки, вы можете построить их, набрав make .
Вполне возможно, что после того, как многие вызовы рисования могут переполнены команды или буфера вершины, в этом случае библиотека установит состояние ошибки и будет продолжать работать нормально, но при промывании очередь команды чертежа с помощью sgp_flush() Команда DRAW не будет отправлено. Это может произойти, потому что библиотека использует предварительно выделенные буферы, в таких случаях проблема может быть зафиксирована путем увеличения префиксированного буфера очереди команд и буфера вершин при вызове sgp_setup() .
Создание недопустимого количества push/pops of sgp_push_transform() и sgp_pop_transform() или гнездовать слишком много sgp_begin() и sgp_end() также может привести к ошибкам, то есть ошибка использования.
В таких случаях вы можете включить макрос SOKOL_DEBUG для отладки или программно обрабатывать ошибку, прочитав sgp_get_last_error() после вызова sgp_end() . Также рекомендуется оставить SOKOL_DEBUG включенным при разработке с Sokol, так что вы можете достать ошибки рано.
Библиотека поддерживает самые обычные режимы смеси, используемые в 2D, которые являются следующими:
SGP_BLENDMODE_NONE - без смешивания ( dstRGBA = srcRGBA ).SGP_BLENDMODE_BLEND -Alpha Blending ( dstRGB = (srcRGB * srcA) + (dstRGB * (1-srcA)) и dstA = srcA + (dstA * (1-srcA)) ))SGP_BLENDMODE_BLEND_PREMULTIPLIED -Предварительно-мультипликативная альфа-смешивание ( dstRGBA = srcRGBA + (dstRGBA * (1-srcA)) ))SGP_BLENDMODE_ADD - Аддитивное смешивание ( dstRGB = (srcRGB * srcA) + dstRGB и dstA = dstA )SGP_BLENDMODE_ADD_PREMULTIPLIED - Предварительно -мультипликативное добавление ( dstRGB = srcRGB + dstRGB и dstA = dstA )SGP_BLENDMODE_MOD - Color Modulate ( dstRGB = srcRGB * dstRGB и dstA = dstA )SGP_BLENDMODE_MUL -color multiply ( dstRGB = (srcRGB * dstRGB) + (dstRGB * (1-srcA)) и dstA = (srcA * dstA) + (dstA * (1-srcA)) )) Вы можете изменить область экрана, чтобы нарисовать, позвонив sgp_viewport(x, y, width, height) . Вы можете изменить систему координат 2D пространства, вызывая sgp_project(left, right, top, bottom) , с ним.
Вы можете перевести, вращать или масштабировать 2D -пространство перед вызовом рисования, используя функции преобразования, которые предоставляет библиотека, такие как sgp_translate(x, y) , sgp_rotate(theta) и т. Д. Проверьте шпаргалку или заголовок для большего.
Чтобы сохранить и восстановить состояние преобразования, вы должны вызвать sgp_push_transform() , а затем sgp_pop_transform() .
Библиотека предоставляет функции рисования для всех основных примитивов, то есть для точек, линий, треугольников и прямоугольников, таких как sgp_draw_line() и sgp_draw_filled_rect() . Проверьте шпаргалку или заголовок, чтобы узнать больше. Все они имеют пакетные вариации.
Чтобы нарисовать текстурированные прямоугольники, вы можете использовать sgp_set_image(0, img) , а затем sgp_draw_filled_rect() , это приведет всю текстуру в прямоугольник. Позже вы должны сбросить изображение с помощью sgp_reset_image(0) чтобы восстановить связанное изображение в белом изображении по умолчанию, в противном случае у вас будут глюки при рисовании сплошного цвета.
Если вы хотите вытащить конкретный источник из текстуры, вы должны использовать sgp_draw_textured_rect() вместо этого.
По умолчанию текстуры нарисованы с использованием простого ближайшего пробоотборника фильтра, вы можете изменить сэмплер с помощью sgp_set_sampler(0, smp) прежде чем нарисовать текстуру, рекомендуется восстановить пробоотборник по умолчанию с помощью sgp_reset_sampler(0) .
Все общие трубопроводы имеют цветовую модуляцию, и вы можете модулировать цвет перед рисованием, установив цвет текущего состояния с помощью sgp_set_color(r,g,b,a) , позже вы должны сбросить цвет по умолчанию (белый) с sgp_reset_color() .
При использовании пользовательского шейдера вы должны создать для него трубопровод с sgp_make_pipeline(desc) , используя шейдер, режим смешивания и примитив, связанный с ним. Затем вам следует позвонить в sgp_set_pipeline() перед вызовом шейдера. Вы несете ответственность за использование того же режима смеси и примитивный рисунок, что и созданный трубопровод.
Пользовательские униформы могут быть переданы в шейдер с sgp_set_uniform(vs_data, vs_size, fs_data, fs_size) , где вы всегда должны передавать указатель в структуру с той же схемой и размером, что и в вершине и фрагментных шейдерах.
Хотя вы можете создавать пользовательские шейдеры для каждого бэкэнда графики вручную, рекомендуется использовать SHDC SOKOL Shader SHDC, потому что он может генерировать шейдеры для нескольких бэкэндов из одного файла .glsl , и это обычно работает хорошо.
По умолчанию в библиотечном униформе буфера на вызов на рисунке содержит всего 8 платных униформ (конфигурация SGP_UNIFORM_CONTENT_SLOTS ), и это может быть слишком низким для использования с пользовательскими шейдерами. Это по умолчанию, потому что обычно новички могут не хотеть использовать пользовательские 2D -шейдеры, а увеличение большего значения означает больше накладных расходов. Если вы используете пользовательские шейдеры, пожалуйста, увеличьте это значение, чтобы быть достаточно большим, чтобы удерживать количество униформы вашего самого большого шейдера.
Следующие макросы могут быть определены, прежде чем включать в себя поведение библиотеки:
SGP_BATCH_OPTIMIZER_DEPTH - Количество команд рисунков, на которые отступает оптимизатор партии. По умолчанию 8.SGP_UNIFORM_CONTENT_SLOTS - Максимальное количество поплавок, которые можно хранить в каждом буфере для вызовов. По умолчанию 8.SGP_TEXTURE_SLOTS - Максимальное количество текстур, которые могут быть связаны за вызов. По умолчанию 4. MIT, см. Файл лицензии или конец файла sokol_gp.h .
Вот быстрый список всех библиотечных функций для быстрой ссылки:
/* 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. */ Эта библиотека была проверена с 2020 года в частных проектах и оказалась стабильной.
Эта библиотека была первоначально спонсирована игрой Mmorpg Medivia Online, я хотел бы поблагодарить их за поддержку моей работы.
Спасибо @kkukshtel за спонсирование партийных вызовов с различными цветами.
Обязательно проверьте превосходный проект Sokol от @floooh, в нем есть много полезных библиотек с одним заголовком C, изготовленными с качеством, которые можно использовать для разработки игр.
Вы также можете захотеть проверить мой другой отдельный заголовок C Library MinicoRo, он приносит стеки для C, очень полезные для упрощения конечных государственных машин в Game Devlopment.
sgp_set_uniform , добавлены новые предварительно-мультимированные режимы смеси. Вот несколько скриншотов всех образцов в каталоге samples . Нажмите на любое из изображений, чтобы просмотреть его фактически отображается в режиме реального времени в вашем браузере.
Примитивы образец:
Образцы режимов смешивания:
Образец кадра буфера:
Образец прямоугольника:
Эффект образец:
Образец SDF:
Я работающий на полный рабочий день с открытым исходным кодом, любое количество пожертвований через мой GitHub будет оценен и может привести меня в поддержку поддержать эти и другие проекты с открытым исходным кодом. Я могу принять одноразовое спонсорство для небольших функций или незначительных улучшений, связанных с целями проекта, в данном случае свяжитесь со мной.