Inquirer‘Parents responsible for protecting kids online, not just government’ESPN Deportes¿Podría Ryan Flournoy darles a los Cowboys un tercer receptor de 1,000 yardas?RTP DesportoEuropeus de atletismo. Joana Pontes no top 10 da maratona em marcha com recorde nacionalוואלהחיזבאללה קורא לממשלת לבנון להפסיק את המו"מ עם ישראל ומאיימת בתגובהThe Jerusalem PostWATCH: Funnel cloud seen in Golan Heights amid widespread thunderstorm, flash flood warningsSky TG24Ceuta, la polizia disperde i migranti diretti in SpagnaIl Sole 24 OreTroppo caldo, stop allo sci estivo sul ghiacciaio dello StelvioBBC News BrasilTerremoto de magnitude 7,7 mata ao menos 47 pessoas na IndonésiaSportstarF1 remains the dream, eyeing strong finish to F2 season: Kush MainiRai NewsCinque anni dal ritorno dei Talebani: l’Afghanistan ha cancellato le donne, solo il 7% lavoraLa TerceraConductor en estado de ebriedad atropella a tres bomberos que atendían accidente en ChiguayanteRTL BoulevardSchrijver Peter Andriesse (85) overleden
The Daily Newsstand · Free, Always
Saturday, August 15, 2026

Как я делал компактную библиотеку для создания приложений с графическим интерфейсом на языке C++. Часть 4

Translate

Это четвертая статья из цикла статей о создании компактной кросс‑платформенной библиотеки для разработки приложений с графическим интерфейсом на языке C++ — Frenchie. Для тех, кто привык изучать исходный код самостоятельно, репозиторий с исходным кодом библиотеки можно найти по ссылке. В данном материале я начну рассказывать про ту часть библиотеки, которая отвечает за rendering - rendering backend.

Цель всего цикла материалов — дать представление о том, как устроены подобные проекты, начиная от открытия контекстного окна и управления его состоянием, заканчивая рисованием графических примитивов. Надеюсь, что весь цикл материалов окажется полезным как для тех, кто начинает свой путь в разработке приложений с графическим интерфейсом, так и для опытных разработчиков.

Весь список материалов представлен ниже:

Как будем делать rendering backend?

Современный rendering довольно сложная и многогранная тема, начинающаяся с аналитической геометрии и заканчивающаяся моделями видеокарты, заложенных в конкретных графических API. Не смотря на все различия в том, каким образом конкретные графические API работают, общий принцип работы с 2D/3D моделями остается +/- одинаковым. Поэтому, в этом материале я постараюсь дать общее представление о том, как в принципе работает rendering, а уже в следующих материалах мы поговорим о конкретных реализациях rendering backend-а с использованием конкретных графических API.

Как в принципе работает rendering?

В данном разделе я постараюсь максимально просто и понятно дать общее представление о том, как работает 2D/3D rendering без привязки к конкретному графическому API. По этой причине, многие вещи могут быть описаны технически неточно, но, я надеюсь, что это не помешает понять общую суть.

Из чего состоит 2D/3D модель?

В компьютерной графике любой 2D/3D объект представляют в виде набора точек, которые соединяются между собой линиями так, чтобы образовалась сетка с непересекающимися клетками. В качестве клеток сетки используют треугольники, т.к абсолютно любую поверхность можно разбить на сетку из треугольников, плюс, все точки треугольника всегда лежат в одной плоскости, что позволяет сильно упростить расчеты цветов моделируемой поверхности, а также унифицировать формат, в котором геометрия обрабатывается на видеокартах. Точки, из которых состоит 2D/3D объект, принято называть вершинами, а набор "вершины + треугольники" называется mesh-ем:

Пример 3D mesh-а

Пример 3D mesh-а

Каждая вершина mesh-а в общей сложности должна иметь координату в пространстве, а также цвет. В принципе, этого могло бы быть достаточно, но нам нужно еще уметь рисовать текстуры. Тексутры обычно натягиваются на треугольники mesh-а при помощи текстурных 2D координат, называемых UV координатами. UV координаты нормируются в диапазоне от 0 до +1 и показывают, какую часть текстуры должен на себя натянуть конкретный треугольник mesh-а:

UV координаты текстуры с примером их mapping-а на плоскость прямоугольника в Unity

UV координаты текстуры с примером их mapping-а на плоскость прямоугольника в Unity

Как 2D/3D модель хранится в памяти видеокарты?

Итак, с тем, из чего состоят вершины mesh-а разобрались, теперь посмотрим, как эти вершины хранятся в памяти видеокарты.

Координата вершины в пространстве, цвет, UV координата и другие свойства, которые могут быть у вершины называются атрибутами вершины. Для достижения большой производительности атрибуты всех вершин в памяти видеокарты лежат в одном массиве в порядке следования самих вершин mesh-а. Т.е. если у нас есть три вершины, то в массиве с атрибутами вершин идут сначала координата в пространстве, цвет и UV координата первой вершины, потом тоже самое для второй вершины, далее, третьей вершины и так далее:

Массив с атрибутами вершин mesh-а

Массив с атрибутами вершин mesh-а

Чтобы видеокарта знала, как переходить от одной вершины к другой, ей нужно знать сколько места одна вершина занимает в памяти. Количество места, занимаемого одной вершиной в видеопамяти можно посчитать как сумму величин занимаемой памяти каждого отдельного аттрибута вершины. Объем занимаемой вершиной mesh-а памяти называется отступом. Вот этот самый отступ видеокарта и использует, чтобы шагать в массиве от одной вершины к другой. С вершинами, надеюсь, понятно, теперь нужно понять, как сказать видеокарте, какие множества вершин образуют треугольники, т.к именно с треугольников, образующих поверхность mesh-а, и начинается rendering.

Общий принцип рисования 2D/3D объектов?

Чтобы видеокарта могла нарисовать наш mesh, мы должны ей сказать, какие множества вершин образуют треугольники. Самый простой способ это сделать - загружать последовательно все вершины нашего mesh-а в память видеокарты так, чтобы каждые три последовательно расположенные вершины образовывали треугольник. Тогда видеокарта могла бы идти с шагом в три вершины по массиву с аттрибутами вершин и выполнять рисовку.

Описанный способ рисования существутет во всех графических API, даже в самых старых, он достаточно прост, но, к сожалению, малоэффективен. Дело в том, что если нам нужно будет рисовать много разных mesh-ей, то под каждый из них в видеопамяти нужно будет выделить свой массив с аттрибутами вершин, после чего видеокарте при рисовке придется переключаться между массивами с аттрибутами вершин разных mesh-ей, что приведет к ее "простою" внутри цикла работы приложения в виду того, что переключение между массивами вершин занимает время.

В таком случае, возникает логичный вопрос: а можно ли как-то "слепить" все mesh-ы в один большой mesh, загрузить все разом на видеокарту и потом рисовать отдельные части этого mesh-а, если нужно ? Да, так делать можно и более того, такой подход фактически является стандартным в приложениях с 2D/3D графикой.

Рисование частей одного большого mesh-а, загруженного на видеокарту, называется индексированной рисовкой. Суть индексированной рисовки заключается в том, чтобы каждой вершине mesh-а присвоить индекс и далее, зная с какого по какой индекс лежит нужный нам mesh, внутри нашего большого mesh-а, попросить видеокарту нам его нарисовать через графический API:

Пример хранения вершин mesh-а с их индексами в одном массиве для последующей индексированной рисовки

Пример хранения вершин mesh-а с их индексами в одном массиве для последующей индексированной рисовки

Опытный читатель уже мог задаться вопросом о том, а зачем вообще индексированная рисовка, если можно попросить видеокарту нарисовать все разом, ведь все mesh-ы же лежат в одном большом массиве, так ? Дело в том, что к разным mesh-ам могут применяться разные геометрические преобразования типа поворота, отражения, масштабирования и.т.д. Поэтому, если вложенные mesh-ы внутри одного большого mesh-а имеют разные геометрические преобразования, то нарисовать все разом мы, к сожалению, не сможем. Но, зато мы сэкономим кучу процессорного времни на том, что нам не придется заставлять видеокарту переключаться между массивами с аттрибутами вершинин разных mesh-ей, т.к. они все будут лежать в одном массиве.

На самом деле, не только разные геометрические преобразования у разных mesh-ей являются препятствием к тому, чтобы рисовать их все разом. Есть еще нюансы, связанные с тем, как должен выполняться расчет цветов в промежуточных точках треугольников, образующих поверхность конкретного mesh-а: освещение, затенение и.т.д. Т.е, если алгоритмы расчета цветов в промежуточных точках треугольников, образующих поверхность конкретного mesh-а, разные, то нарисовать все mesh-ы, лежащие в одном большом массиве, за раз тоже не получится. Теперь, выясним, где же хранятся индексы вершин mesh-ей.

Индексы вершин хранятся в отдельном массиве, который также, как и массив с аттрибутами вершин mesh-а грузится на видеокарту. Т.е для использования механизма индексированной рисовки нам нужно загрузить на видеокарту сначала массив с аттрибутами вершин mesh-а, а потом еще загрузить массив с индексами этих вершин. Как только загрузка выполнена, можно функциями графического API просить видеокарту рисовать контекретный mesh, зная индекс его самой первой вершины и индекс самой последней вершины.

Теперь мы знаем, как вершины mesh-а хранятся в памяти видеокарты, как видеокарта понимает, какие множества вершин образуют треугольники и как загрузить все mesh-ы на видеокарту разом и рисовать нужные из них, используя индексы их первой и последней вершины. Теперь вопрос состоит в том, насколько часто надо обновлять mesh на видеокарте и насколько это эффективно ? Начнем с того, что подразумевается под обновлением mesh-а на видеокарте?

Обновление mesh-а на видеокарте - это удаление старого mesh-а и загрузка нового. В библиотеке Frenchie и подобных ей библиотеках типа ImGUI, обновление mesh-а на видеокарте реализуется каждый кадр, т.к mesh для рисуемых графических объектов постоянно перестраивается.

Опытный читатель наверняка уже задался вопросом о том, насколько эффективно загружать большие mesh-ы на видеокарту ? На самом деле, механизм загрузки работает довольно быстро, особенно, если грузить все разом (это еще один плюс в пользу механизма индексированной рисовки). Поэтому, на современных компьютерах mesh-ы размером даже до пары десятков тысяч треугольников можно загружать на видеокарту каждый кадр работы приложения без особой потери в производительности. Однако, загрузка/выгрузка больших 2D/3D моделей на каждом кадре действительно приведет к значительному снижению частоты кадров (frames per second - FPS). Поэтому, в тех же компьютерных играх применяется большое к-во оптимизаций, направленных на снижение количества рисуемых на экране объектов, а также на то, чтобы объекты с неменяющейся геометрией (mesh-ем) загружались на видеокарту один раз и рисовались, когда это необходимо. Но, не будем отвлекаться на компьютерные игры от основной темы и поговорим о геометрических преобразованиях.

Что такое матрицы геометрических преобразований и зачем они нужны?

Как мы говорили в прошлом разделе, вершины mesh-а имеют координату в пространстве. Чтобы поменять угол поворота, размер или расположение mesh-а мы можем нужным образом изменить координаты всех его вершин. Как это сделать ?

Координата вершины mesh-а - это вектор, а любое изменение координаты mesh-а - это, по сути, изменение положения вектора в 2D/3D пространстве. Расположение вектора в пространстве меняется путем умножения этого вектора на матрицу, называемой матрицей геометрических преобразований. Таким образом, матрицы геометрических преобрвазований как раз таки и позволяют применять различные геометрические преобрвазования к вершинам mesh-а (поворот, перемещение и.т.д).

В принципе, на этом можно было бы закончить, но, есть еще один важный нюанс, связанный с особенностями того, как матрицы геометрических преобразований применяются к 2D/3D объектам в компьютерной графике. Допустим, мы хотим нарисовать какой-то 3D объект. Объект у нас находится в 3D пространстве, но, монитор у нас 2D. Задача рисования в таком случае с точки зрения геометрии сводится к тому, чтобы спроецировать вершины mesh-а из 3D пространства в 2D пространство. В принципе, суть происходящего понятна, но помимо того, что монитор у нас 2D, есть еще один очень важный нюанс также связанный с монитором.

Мониторы существуют разных размеров и плотность пикселей на поверхности их экранов (к-во пикселей на единицу размера) тоже разная. Поэтому, чтобы задача проецирования точек из 3D в 2D решалась одинаково на всех мониторах производители видеокарт ввели понятие - нормированные координаты (Normalized Device Coordinates - NDC). Понятие нормированные означает, что координаты по всем трем осям - XYZ в NDC находятся в диапазоне от -1 до +1. Все, что выходит за границы указанного диапазона рисоваться не будет. Таким образом, задача проецирования точек из 3D в 2D пространство сводится еще и к тому, чтобы выполнить проекцию вершин mesh-а из 3D в 2D так, чтобы координаты наших вершин по всем трем осям уложились в диапазон от -1 до +1:

Система нормированных координат

Система нормированных координат

Вот теперь, для общего представления о том, зачем нужны матрицы геометрических преобразований, думаю, материала достаточно. Более подробную информацию о системах координат, используемых в компьютерной графике можно почитать здесь. О том, какие виды NDC систем координат существуют, а также о том, какие виды матриц проекций применяются, поговорим в отдельном материале, ибо это очень обширная тема. А сейчас предлагаю перейти к тому, как представляются текстуры.

Как представляются текстуры в памяти видеокарты? Что такое фильтрация и mipmap-ы?

В предыдущих разделах мы разобрались с тем, как моделируются вершины mesh-ей, как они хранятся в памяти видеокарты, а также с тем, что такое матрицы геометрических преобразований и как с их помощью вершины mesh-ей переносятся из 3D координат в 2D координаты. В этом разделе мы разберемся, как моделируются текстуры.

Текстура, по сути, представляет собой массив, в котором каким-то образом сохранены цвета каждого пикселя. Размер массива равен m x n x k, где m - ширина текстуры, n - высота, а k - количество цветовых каналов. Количество цветовых каналов определяется форматом текстуры. В компьютерной графике обычно используют форматы RBGA, RGB и Alpha, в которых имеется четыре, три и один цветовой канал соответственно. Для наглядности, массив с цветами текстуры на четыре канала размером 4x4 пикселя в памяти компьютера будет выглядеть так:

Пример представления в видеопамяти RGBA текстуры

Пример представления в видеопамяти RGBA текстуры

Помимно цветов, у текстуры также задают режим ее "схлопывания" по краям. Что имеется в виду ? Допустим, мы натягиваем текстуру на mesh из двух треугольников, образующих прямоугольник, а размер текстуры меньше, чем размер прямоугольника. В таком случае, какимм цветами нам заполнять те пиксели, цветов которых нет в массиве с цветами текстуры ? На практике, как правило, применяются всего четыре способа того, как это можно сделать.

Первый способ - взять цвета крайних пикселей текстуры и их цветами заполнить пространство вокруг текстуры, такой способ называется "схлопывание" - clamp.

Второй способ - это пиксели пространства, покрываемого текстурой, заполнить цветами пикселей текстуры, а остальное пространство вокруг текстуры заполнить прозрачным цветом. Такой способ называется "clamp zero" или "clamp to edge".

Третий способ - это взять, и повторить текстуру в пространстве вокруг нее - "repeat".

Четвертый способ, также связан с повторением цветов пикслей текстуры в пространстве вокрук текстуры, только повторяющаяся текстура отзеркаливается - "mirrorrepeat" или просто "mirror". Все четыре способа представлены на картинке ниже:

Пример работы алгоритмов схлопывания текстур

Пример работы алгоритмов схлопывания текстур

Про схлопывание текстур поговорили, теперь разберемся, зачем нужна фильтрация. Дело в том, что координаты текстуры не зависят от разрешения монитора и могут принимать любое значение с плавающей точкой, поэтому видеокарта должна как-то определять, какой пиксель текстуры на какой пиксель монитора натягивать. Это важно, т.к. мы можем пытаться натянуть текстуру маленького размера на большой объект на экране. Как раз для таких случаев и нужна фильтрация. В этом материале предлагаю рассмотреть два типа фильтрации - выбор цвета близжайшего пикселя, и линейную интерполяцию цветов.

Выбор цвета ближайшего пикселя - это когда renderer выбирает из текстуры цвет того пикселя, который располагается ближе всего к нужной координате на мониторе:

Пример выбора цвета пикселя из набора пикселей текстуры при применении алгоритма фильтрации путем выбора ближайшего пикселя

Пример выбора цвета пикселя из набора пикселей текстуры при применении алгоритма фильтрации путем выбора ближайшего пикселя

Линейная интерполяция - это когда renderer берет два ближайших пикселя из текстуры и считает цвет пикселя на мониторе как линейную интерполяцию между этими двумя цветами:

Пример выбора цвета пикселя из набора пикселей текстуры путем линейной интерполяции цветов двух ближайших пикселей

Пример выбора цвета пикселя из набора пикселей текстуры путем линейной интерполяции цветов двух ближайших пикселей

Визуальный эффект от описанных выше видов фильтрации текстур выглядит следующим образом:

Пример того, как работают алгоритмы фильтрации: слева - выбор ближайшего пикселя, справа - линейная фильтрация

Пример того, как работают алгоритмы фильтрации: слева - выбор ближайшего пикселя, справа - линейная фильтрация

С фильтрацией разобрались, теперь разберемся, что такое mipmap-ы ? Чтобы понять, зачем нужны mipmap-ы, нужно разобраться с тем, как renderer выбирает цвет пикселей из текстуры в случае, когда текстура высокого разрешения, а объект, на который она натягивается - маленького. При натягивании большой текстуры на маленький объект renderer вынужден каким-то образом выбрать цвет пикселя объекта из уменьшенной текстуры. В таком случае, почти всегда возникают графические артефакты из-за того, что алгоритмы фильтрации текстуры плохо работают в случае, когда пиксели текстуры очень мелкие и расположены очень близко друг к другу.

mipmap-ы - это, по сути, набор из копий одной и той же текстуры, в котором каждая последующая копия текстуры в два раза меньше предыдущей. При использовании mipmap-ов и масштабировании текстуры, из набора mipmap-ов берется нужная уменьшенная копия, что в комбинации с фильтрацией позволяет убрать графические артефакты при рисовке. Во многих графических API, при применении mipmap-ов и масштабировании текстуры есть возможность применить отдельные фильтры при уменьшении и увеличении размера текстуры.

Таким образом, мы разобрались с тем, как текстуры представляются в памяти, зачем нужна фильтрация и что такое mipmap-ы. Теперь перейдем непосредственно к тому, из каких этапов устроен процесс рисования.

Что такое rendering pipeline?

Rendering pipeline - это то, в каком порядке видеокарта рисует объекты на мониторе после того, как все mesh-ы и текстуры загружены на видеокарту и для них всех определены геометрические преобразования. Так в каком же порядке и что именно тогда делает видеокарта ?

Cначала, видеокарта применяет все геометрические преобразования к рисуемым объектам в результате чего, все их координаты оказываются в NDC координатах. Далее, видеокарта рассчитывает цвета на поверхностях треугольников рисуемых mesh-ей, после чего происходит смешивание цветов накладывающихся друг на друга полупрозрачных объектов. Как только все упомянутое сделано, производится обрезка всего, что выходит за границы NDC координат.

Описанный выше порядок рисования - это лишь набор основных действий, которые должна выполнить видеокарта, чтобы нарисовать нам что-то на мониторе. В зависимости от используемого графического API каждый из описанных выше этапов в rendering pipeline может разбиваться еще на несколько этапов, но, суть происходящего от этого не меняется.

Существует два типа rendering pipeline - фиксированный и программируемый. Фиксированный rendering pipeline - это такой pipeline, в котором мы никак не можем вмешиваться ни в один из этапов рисовки, мы можем лишь менять какие-то заранее определенные настройки. Такие виды pipeline-ов применялись в старых графических API типа DirectX9, либо старых версиях OpenGL.

Программируемый pipeline - это такой pipeline, в котором мы можем программировать самостоятельно отдельные этапы рисовки на свое усмотрение. Программируемые pipeline-ы являются промышленным стандартом и используются во всех современных графических API типа OpenGL, Vulkan, Metal.

С чего же начинать делать rendering backend?

Вооружившись базовыми знаниями о том, как работает rendering, мы можем наконец потихоньку начать разбираться в деталях. С чего же нам начать ? Начать нужно прежде всего с того, чтобы научиться инициализировать конкретный графический API. Что входит в процесс инициализации ? При инициализации графического API происходит создание состояния графического API и привязка этого состояния к контекстному окну. В процессе инициализации мы также должны создать буфферы для хранения аттрибутов вершин и индексов вершин. А как, собственно говоря, видеокарта узнает, какие аттрибуты имеют наши вершины ?

Большинство более-менее новых графических API, как правило, позволяют создавать так называемые дескрипторы атрибутов вершин (или что-то похожее), по информации из которых видеокарта будет понимать, какие атрибуты есть у вершин рисуемых mesh-ей. Т.е. перед тем, как создавать буферы с атрибутами вершин и индексами мы должны создать дескрипторы вершин или что-то похожее на них на конкретном графическом API. Проще говоря, мы должны сказать графическому API, из чего состоят наши вершины и создать буфферы под их хранение.

Далее, если rendering pipeline программируемый, то мы должны запрограммировать основные его этапы: применение геометрических преобразований и расчет цветов на поверхностях треугольников mesh-ей. Если же rendering pipeline не программируемый, мы должны правильно его настроить, чтобы геометрические преобразования и расчет цветов работали так, как мы того хотим. На этом, этап инициализации закончен. Далее, надо научиться непосредственно рисовать.

Прежде чем начать рисовать mesh, его нужно загрузить на видеокарту и подготовить графический API к рисованию. Как только mesh загружен, а графический API готов, мы можем механизмом индексированной рисовки рисовать нужные нам mesh-ы, зная индексы их первой и последней вершины. После того, как цикл индексированной рисовки закончен, мы можем попросить видеокарту отобразить результат рисовки на мониторе и сбросить состояние графического API в исходное.

Вот такой вот минимальный набор действий должен уметь выполнять rendering backend при помощи графического API, чтобы мы могли что-то нарисовать на мониторе. Про реализации rendering backend-а в библиотеке Frenchie с примером на конкретном графическом API рассмотрим в следующем материале. А пока, предлагаю закончить, надеюсь, было интересно.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.