• Нужно ли подключать все необходимые заголовки если они подтягиваются из других заголовков?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    В контексте использования директивы #include есть два важных принципа.
    Важный из этих двух сейчас - это принцип Include What You Use.

    Суть сводится к тому, что файлы исходного кода формирующие модуль трансляции должны в правильном порядке подключать все заголовки, объявления которых используют. Транзитивные включения не считаются. Методология соответствия IWYU включает в себя и правило перечисления подключаемых заголовков - от общих к частным. Именно таким образом должна обеспечиваться оптимальная скорость формирования модуля трансляции.
    И с этим связано несколько проблем. Основные я опишу ниже.
    Наивное следование IWYU в сложных проектах.

    В обычном проекте считается что один файл исходного кода соответствует одному модулю трансляции. В более сложных проектах, где используется SCU/Unity Build, один модуль трансляции уже представлен несколькими файлами исходного кода. Но IWYU требует соблюдать правила только для модуля трансляции, а не для файлов исходного кода.
    Эта ситуация обозначает одну из проблем соответствия IWYU, которую не замечают многие разработчики, кто пользуется IWYU и SCU одновременно. Дублирование директив, нарушение порядка от общего к частному и доступность директив для всего нижележащего кода других файлов исходного кода сводит на нет весь принцип IWYU.
    Простейшей ошибкой наивного следования IWYU при использовании SCU является простой забытый инклуд, который присутствует в другом файле исходного кода, размещенном модуле трансляции выше по коду. SCU формируются динамически, на базе определенного алгоритма. Сегодня ошибки нет, а завтра исходный код попадет в другой модуль трансляции, где выше по коду уже не будет нужного инклуда и трансляция пойдет прахом. Особой пикантности данной ситуации добавляют разные сценарии SCU для разработчиков и систем CI. В этом случае ошибку с потерянным инкудом искать будут очень долго и не в одно лицо. Такие ситуации реально существовали у меня на работе. Особую пикантность такой ситуации добавляет и то, что на этой работе люди особо активно пропагандировали IWYU, говоря о безмерной пользе принципа вместе с использованием SCU.
    Решением же является понимание несовместимости использования SCU и IWYU в одном проекте.

    Излишне детальное следование IWYU.

    Принцип IWYU говорит включать только то, чем пользуешься. Что это означает в деталях?
    Когда заголовочный файл использует какое-то объявление, он должен сперва подключить соответствующий заголовок. Когда исходный код использует какое-то объявление, он должен сперва подключить соответствующий заголовок.
    Код не пишется в вакууме, код пишется в составе библиотек и систем, в рамках которых код связывается между собой. На более мелком уровне код пишется в составе подсистем и модулей, а подсистемы и модули уже формируют более глобальные системы и библиотеки. В рамках всех этих единиц агломерации кода сам код является тесно связанным между собой. Заголовки одной подсистемы редко не будут перекрестно включать друг друга потому что активно пользуются их объявлениями для реализации функционала подсистемы.
    Внешний же код, использующий функционал подсистемы, редко будет ограничиваться включением только одного заголовка, потому сами заголовки включают друг друга и через IWYU диктуют подобный подход своему пользователю.
    Это заставляет блоки включений раздуваться до невиданных масштабов в сотни строк одних только #include. Если проект использует #pragma once, то огромные блоки включений сказываются только на удобстве чтения, сводя удобство разбора инклудов к нулю. Но если проект использует только define-guard, то от огромных блоков инклудов начинает катастрофически страдать скорость сборки. В этот момент люди обычно вспоминают про SCU и с его помощью окончательно хоронят гибкость проекта в пучине ошибок совместного использования SCU и IWYU.
    А решением в данном случае является выделение для единиц агломерации кода своих собственных публичных заголовков, внутри которых в правильном порядке будут подключены все заголовки используемого кода.
    Этот простой шаг кратно сокращает списки инклудов в пользовательском коде, не противоречит IWYU и позволяет на более высоком уровне организовать публичный интерфейс модуля, подсистемы, системы или библиотеки.


    Полагаясь на транзитивные включения, можно угодить в неприятности даже с кодом стандартной библиотеки. От стандарта к стандарту библиотечный код немного меняется и в любой момент может перестать подключать используемые в твоем коде заголовки. В этом случае при переходе на новый стандарт тебе придется сперва справиться с возникшими ошибками трансляции. Еще более остро эта проблема встает при обновлении сторонних библиотек, авторы которых, как правило, часто и кардинально меняют внутреннюю структуру библиотеки.
    Соблюдение же принципа IWYU обещает реже встречаться с подобными проблемами и легче переживать обновление стороннего кода, делая тем самым проект более гибким и готовым к изменениям.
    Ответ написан
    Комментировать
  • Как переиспользовать opengl-объекты в разных окнах?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Для начала стоит разобраться в том, написан ли код в вопросе на OpenGL - нет, не написан.
    Код в вопросе использует обертку GLFW, которая довольно сильно абстрагирует пользователя от самого по себе OpenGL. Отсюда у тебя и непонимание самых основ работы с OpenGL, а ведь самым основным в этой работе является управление контекстом устройства.
    Коротко описать механизмы работы с контекстом не получится, в трех словах половину документации API не пересказать. Но есть источники, которые обязательно нужно изучить: [1], [2], [3], [4].

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

    Эта информация и является решающей в твоем случае. Конструктор твоего класса Window не даром имеет третий параметр - reference. Ты не мог ввести его специально и не знать о том, что я сейчас напишу.
    Если обратиться к документации на glfwCreateWindow[?], то можно узнать что пятым параметром функции заявлено The window whose context to share resources with, т.е. окно, с контекстом которого следует разделить таблицу ресурсов.

    Решением для тебя будет создание главного окна, с которым первое и второе окна будут разделять таблицу ресурсов. Главное окно не нужно активировать, оно должно просто присутствовать чтобы выступать основным держателем таблицы ресурсов. Указатель на главное окно нужно передать третьим параметром в оба отображаемых окна. Больше ничего менять не надо, все заработает сразу.
    Ответ написан
  • Есть ли хоть какое-то преимущество использования функтора перед обычной функцией в данном случае?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    У функтора перед функцией есть только одно преимущество - это наличие состояния функтора, которое может меняться между обращениями к его функциональному оператору и влиять на его поведение. Функтор настраивается эксклюзивно, функция - по понятным причинам - только глобально.
    У функции перед функтором тоже есть преимущество - это адрес функции, по которому сразу можно начать ее исполнение. У функтора всегда будет два адреса - адрес метода функционального оператора и адрес самого функтора.

    Исходя именно из этих преимуществ и следует выбирать между функтором и функцией.
    Скажем, если бы нужно было nums вписать в CSV таблицу в виде матрицы, то проще использовать функтор. Создать его, настроить поток вывода, символ-разделитель столбцов, количество выводов до перехода на следующую строку и передать в std::for_each.
    Если такая настройка поведения не требуется, от функтора лучше отказаться в пользу функции во всех случаях.

    Функтор используется в реализации идиомы делегата и коллбека. Делегаты позволяют универсальным образом хранить самые разные точки исполнения кода и безопасно проводить по ним исполнение.
    Если использование делегатов оправдано необходимостью, в делегаты оборачивают даже указатели на функцию, чтобы сохранить единообразие подхода к управлению исполнением.
    Ответ написан
    Комментировать
  • Как получать и обрабатывать координаты мыши в окне OpenGL?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    По координатам мышки пойдем от самого верха, операционной системы, и до мирового пространства, в котором рисуется твоя сцена.

    Самое первое - это не нужно именно получать координаты курсора в момент его обработки. Нужно реагировать на сообщения о перемещении курсора по окну и запоминать координаты в его клиентской области, в которой у тебя и происходит отрисовка сцены.
    Но если очень хочется, то на WinAPI можно использовать GetCursorPos[?] и ScreenToClient[?] чтобы на месте получить координаты курсора в клиентской области окна.
    На этом этапе нужно принять во внимание то, в какой координатной системе считаются координаты курсора.
    Например, в WinAPI координатная система окна выглядит так. И именно в ней считаются координаты курсора.

    Все следующие этапы стоит рассматривать как переходы из одного пространства в другое. Числа могут быть одинаковыми, но ценность перехода именно в том, чтобы перевести эти числа из одного пространства в другое.

    Следующим этапом будет переход от клиентской области к области вьюпорта. Этот переход делается через дополненную 2х2 матрицу трансформаций 2D-пространства. Ее важно использовать везде, т.к. это позволит заложить фундамент однообразного перехода из пространства клиентской области экрана в пространство координатной системы камеры.
    Что важно на этом этапе. Пространство вьюпорта и пространство камеры связаны через пространство нормализованных координат области отсечения. В разных GAPI пространство области отсечения представлено по-разному. И пространство вьюпорта тоже по-разному представлено в разных GAPI.
    Например, в OpenGL координатная система вьюпорта выглядит так.
    Еще на этом этапе нужно заложить переход от точки к лучу, т.к. вьюпорт уже имеет объем. Для этого координаты курсора нужно дополнить минимальной и максимальной глубиной вьюпорта, получив уже две точки координат курсора.
    В качестве примера такого преобразования можно взять код функции D3DXVec3Unproject прямо из Wine. Там делается правильный и простой переход из пространства клиентской области окна в пространство области отсечения OpenGL через пространство вьюпорта.

    От пространства области отсечения координаты курсора нужно перевести в пространство сцены.
    Матрица View-Projection переводит геометрию из системы координат пространства сцены в систему координат области отсечения. Значит, чтобы сделать переход из области отсечения в пространство сцены, нужно использовать инверсию матрицы View-Projection.

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

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Если коротко, правила автоматического вывода шаблонных аргументов предусматривают вывод полного типа для переданного аргумента при инстанцировании шаблона функции.

    Что это означает на практике? В языке есть термин массива статического размера. Частным примером такого массива является строковой литерал. Конкретно у строкового литерала из простых символов тип будет таким: const char[N], где N - это размер памяти под строковой лиерал, в байтах.

    Таким образом, если сделать такое объявление шаблона
    template< typename TValue >
    void Foo( TValue& value ); // (1)

    то при инстанцировании как Foo( "Hello" ); аргумент TValue определится как const char[6].
    И мы довольно легко можем воспользоваться этим механизмом. Нужно только дать пояснение, как и во что стоит выводить шаблонные аргументы.

    Шаблон можно записать вот так:
    template< typename TValue, size_t LENGTH >
    void Foo( TValue (&value)[ LENGTH ] ); // (2)

    В этом случае при инстанцировании как Foo( "Hello" );, аргумент TValue будет выведен как const char, а нетиповой аргумент LENGTH будет выведен в значение 6.
    Собственно, все. Размер переданного массива получен.

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

    В чем суть модификаторов в параметрах шаблона

    Если первый шаблон объявить как
    template< typename TValue >
    void Foo( TValue value );

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

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

    Параметр такой "другой" функции лучше всего определить универсальной ссылкой и использовать идеальную передачу. Тогда все будет работать правильно.
    Ответ написан
    2 комментария
  • Как грамотно переписать фабрику?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Однажды я уже показывал как могла бы выглядеть абстрактная фабрика.
    Абстракция там достигается за счет реконфигурируемости самой фабрики.
    Показанная там реализация служит хорошим примером и позволяет понять общий подход, но не годится для использования в настоящем коде. Эту реализацию нужно дорабатывать уже под свои нужды.
    Ответ написан
    Комментировать
  • Как правильно организовать соприкосновения поверхностей при инстансинге?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    В такой постановке вопрос тянет уже на полноценную разработку игр. И суть вот в чем.

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

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

    Ровно так же у тебя может существовать и еще одна модель - модель обработки физики, которая правильно посчитает движение капель с учетом ветра и гравитации. Еще эта модель у тебя может быть задействована для расчетов прыжков и падения персонажа.

    В сложных высокотехнологичных движках множество моделей используют собственную память для представления акторов, вводя многократную избыточность использования памяти, но позволяя работу в полностью параллельном режиме на нескольких потоках. В таких движках существует отдельная стадия сборки представлений из разных подсистем в единое представление актора с попутным разрешением коллизий, если они возникают.
    Тебе подобные сложности точно не нужны на текущем этапе. Реализовать можно просто парой-тройкой моделей (графика, столкновения и физика) поверх единой системы объектов мира, обработку которых пустить последовательно в одном потоке.
    Ответ написан
  • Почему не срабатывает смещение при инстансинге?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Что такое инстансинг по своей сути?
    Вот у нас есть геометрия и параметры ее обработки. Речь только о стадии обработки вершин.
    Самая суть инстансинга в том, что геометрия у нас как бы находится в состоянии суперпозиции. Примитив у нас один, DIP тоже один, но в атрибутах примитива заложено что-то особенное, что заставляет увидеть его сразу во многих местах.
    Это самое особенное - это атрибуты инстанса.

    По умолчанию все атрибуты вершины являются атрибутами примитива. Когда работает DIP, вершинный шейдер запускается для обработки каждой отдельной вершины примитива. Когда инстансинг отключен, можно считать что все вершины примитива принадлежат ровно одному инстансу, для которого атрибутов не задано.
    Но если примитив требуется отрисовать с использованием инстансинга, в его атрибутах должны находиться и атрибуты инстанса. Тогда во время работы уже DIPI вершинный шейдер будет запущен для каждой вершины примитива каждого инстанса.

    Поэтому самым первым этапом включения инстансинга будет именно новый буфер атрибутов в шейдере.
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec2 aOffset;


    aOffset не должен быть uniform-ом. Это должен быть именно атрибут инстанса.
    Uniform-ами стоит делать другие вещи, не уникальные между инстансами. Uniform-ы для инстанса имеются сразу и все. Поэтому рационально в них держать только то, что разные инстансы используют совместно.
    Например - это могут быть цвета фракций или параметры костей скелетов в разных позах.
    Примерный код вершинного шейдера
    #version 460 core
    
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec2 aOffset;
    
    void main()
    {
       gl_Position = vec4(aPos + aOffset, 0.0, 1.0);
    }


    Для указания того, что в буфере находятся атрибуты инстанса, следует использовать glVertexAttribDivisor[?].
    У тебя, скорее всего, это будет выглядеть так: glVertexAttribDivisor( 1, 6 );.
    А внутри буфера у тебя должно быть то, что сейчас записывается в glm::vec2 translations[100].
    Как примерно может заполняться буфер атрибутов инстанса
    glm::vec2 translations[100];
    int index = 0;
    float offset = 0.1f;
    for (int y = -10; y < 10; y += 2)
    {
    	for (int x = -10; x < 10; x += 2)
    	{
    		glm::vec2 translation;
    		translation.x = (float)x / 10.0f + offset;
    		translation.y = (float)y / 10.0f + offset;
    		translations[index++] = translation;
    	}
    }
    
    glBindBuffer(GL_ARRAY_BUFFER, VBOs[3]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(translations), translations, GL_STATIC_DRAW);
    
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec2), (GLvoid*)0);
    glVertexAttribDivisor(1, 6);
    glEnableVertexAttribArray(1);


    С этого момента инстансинг заведется как положено. Просто убираем uniform, меняем подготовку его значений на подготовку еще одного буфера вершин, добавляем код установки этого нового буфера, задействуем его значения в шейдере и все. Эту теорию тебе остается только адаптировать для своего кода.
    Чтобы разобраться почему у тебя конкретно сейчас рисуется неправильно, тебе нужно установить графический отладчик и проанализировать через него презентацию одного кадра. Советую поставить Renderdoc и научиться полноценно работать с ним. Без запуска твоего кода и анализа кадра выяснить причину неправильной работы практически невозможно.
    Ответ написан
  • Как реализовать поиск маршрута в плиточном мире?

    @MarkusD
    все время мелю чепуху :)
    Плиточное поле принято называть дискретным пространством.
    В таком пространстве перемещение измеряется дискретными шагами, между которыми и может быть изменено.

    Для поиска пути в таком дискретном пространстве стоит обратить внимание на алгоритмы поиска в ширину и в глубину.
    Начать стоит с поиска в глубину. Его легко сделать и так же легко понять его неоптимальность. От поиска в глубину довольно легко перейти к поиску в ширину.
    Алгоритмы поиска в ширину и в глубину дадут первичное понимание теории построения маршрута.

    От поиска в ширину можно перейти к эвристическим алгоритмам поиска: A* или поиску по наилучшему совпадению.
    На этом этапе еще полезно познакомиться с алгоритмом JPS. Он может стать даже выгоднее обычного A*.

    A* должен стать для тебя основным инструментом поиска в дискретном пространстве. Но у A* есть проблема большой сложности поиска прямого маршрута.
    При поиске пути по прямой лучше всего использовать алгоритм Брезенхама.
    Этот алгоритм используется для рисования пиксельных примитивов. В частности - прямых.
    И суть должна быть в том, чтобы сперва путь искать через Брезенхама по прямой от точки старта до точки финиша, а если Брезенхам наткнется на препятствие, то уже перейти к поиску через A*.

    Для очень больших дискретных пространств есть варианты иерархического A*: HPA* и HAA*. Они применяются на иерархических дискретных пространствах вида открытого мира. В освоении они являются самыми сложными и самыми интересными.
    Ответ написан
    3 комментария
  • Почему при арифметических операциях bool конвертируется в int?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    В данном случае int правильно выбран в качестве типа для result.

    Чтобы разобраться в логике этого поведения, нужно изучить т.н. технику продвижения целочисленных типов - Integral Promotion.
    В выражении x + y работает только она.

    В частности. Именно продвижение целых подразумевает неявное преобразование булева значения к целому со строго однозначным результатом: [expr.conv.6].
    true всегда в таких случаях будет преобразован в 1 с типом int.

    Конкретно для кода x + y типом результата значения будет именно int, потому что ранг short int ниже ранга int в правилах продвижения. Арифметические операции с значениями всех типов, чей ранг ниже int, всегда подвергаются продвижению и выполняются на значениях с типом int.

    Дело довершает auto result в левой части выражения. Обобщение принимает тип результата выражения справа от знака равенства, коим и является int.
    Иными словами, все это выражение было отдано на откуп продвижению целочисленных типов.
    Ответ написан
    Комментировать
  • Как реализовать 2d физику?

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

    Игровая физика - это очень сложная область, значительно сложнее компьютерной графики. Без добрых десяти лет в инженерии П.О. полноценно зайти в разработку физики будет проблематично.
    Но есть выход. Можно использовать уже готовые физические модели для своих игр. Библиотеки физики.
    Игнорировать их не стоит и для того, чтобы самостоятельно изучать моделирование физики. Используемые там подходы очень полезно понимать для личного обучения.

    Базовая теория по твоему вопросу раскрыта в школьном учебнике физики, еще в разделе механики. Проблемой станет перенос этой теории в код. С этого и начнется знакомство с моделированием законов физики на компьютере.
    Ответ написан
    Комментировать
  • Какие инструменты нужны для разработки игр?

    @MarkusD
    все время мелю чепуху :)
    Сегодня существует ровно два базовых направления разработки конкретного коммерческого проекта.
    Способ первый: купить лицензию или подписку на уже готовый инструмент разработки и заняться непосредственно разработкой своей игры.
    Способ второй: иметь в своем штате команду разработчиков собственного инструмента, на базе которого можно заняться разработкой своей игры.

    Первый способ популяризирует публичные универсальные инструменты разработки.
    Второй способ эксплуатирует проприетарные инструменты.

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

    DirectX, Vulkan и OpenGL, равно как Metal и ряд проприетарных GAPI некоторых закрытых платформ, не являются графическими библиотеками. Это все - Graphics Application Programming Interface - GAPI.
    Это - низкоуровневые интерфейсы драйвера GPU, позволяющие эксплуатировать ресурсы видеокарты в своих целях. Не только для рисования чего-то, а для ИИ, ML, сложных статистических вычислений, предсказаний и прочих расчетов на больших объемах данных.
    Под капотом любого инструмента, будь-то проприетарный или публичный, в его графическом слое используется один или несколько GAPI. Без этого никак.
    OpenGL, как и DirectX 11, нисколько не устарели, поскольку предоставляют упрощенный интерфейс управления ресурсами GPU. Они используются тогда, когда разработчикам не нужны самые тонкие механизмы управления ресурсами GPU, которые предоставляют DirectX 12 или Vulkan. Потому что последние, помимо прочего, требуют от разработчиков более глубокой экспертизы и больше ресурсов на разработку всего того же, что на OpenGL и DirectX 11 реализуется меньшими силами и за меньшее время.

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

    Информации по каждой отдельной области разработки игр хоть отбавляй. Ее настолько много, что одному человеку за жизнь не усвоить. Поэтому от современного специалиста сегодня требуется спрофилироваться, т.е. определиться со своим профилем работы и стать экспертом.
    Я больше 15 лет занимаюсь разработкой игровых движков и медиаферймворков. Более 10 лет занимаюсь коммерческой разработкой кросслпатформенных инструментов. Я начинал свое обучение по книгам и документации для всех интересующих меня областей еще 20 лет назад. Я самостоятельно освоил множество API, включая графические, сетевые, звуковые и API целевых платформ, используя книги и документацию. Экспертные знания C++ и прочих языков я получил тоже через изучение документации, стандартов и книг.
    Я могу сказать что обучаться по книгам и документации можно и самостоятельно. Еще можно заплатить деньги и получить более точечные знания через их интерпретацию на распространенных сегодня онлайн-курсах. Такие знания не всегда бывают лучше полученных самостоятельно, но времени на освоение того же объема знаний на курсах уйдет меньше чем при самостоятельном изучении. Иными словами, занятия на онлайн-курсах не отменяют важности самостоятельного изучения основных источников информации.
    По открытым видеоурокам на ютубе и прочих видеохостингах обучаться нечему. Цель этих видео - чтобы зритель посмотрел рекламу и этим принес доход автору.
    Ответ написан
    2 комментария
  • Как имея позицию и направление двух объектов в пространстве координат y, x, z, повернуть один объект в другой?

    @MarkusD
    все время мелю чепуху :)
    Решением вопроса будет скалярное произведение двух векторов. Операция еще называется Dot Product.

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

    В качестве бонуса.
    Чтобы определить направление вращения, можно воспользоваться векторным произведением, еще называемым Cross Product.

    Векторное произведение трехмерных векторов позволяет определить нормаль к плоскости, образованной этими векторами. Нормаль будет направлена в одном из двух направлений от плоскости, в зависимости от расположения нуль-векторов относительно друг друга. Если в качестве левого аргумента взять все тот же нуль-вектор локального объекта, а в качестве правого - тот же нуль-вектор целевого, результатом произведения будет ось, вдоль которой и нужно выполнять доворот матрицы вращения.
    Ответ написан
    2 комментария
  • Как работает определение дружественной функции внутри класса?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Перегрузка большинства операторов может быть произведена любым из двух способов: внешней функцией или методом класса. Лишь некоторые операторы невозможно перегрузить в виде внешней функции, их перегрузка осуществляется только в виде метода класса.
    Иными словами, все операторы можно перегрузить в виде метода и только некоторые - в виде внешней функции.
    Разница между перегрузкой в виде внешней функции и перегрузкой в виде метода состоит в том, что метод является частью интерфейса класса и имеет доступ к его непубличным объявлениям, а внешняя функция частью интерфейса не является и доступ имеет только к публичным объявлениям класса.

    Дружественность к перегрузке операторов не имеет никакого отношения. Перегрузка оператора в виде внешней функции и добавление дружественности для этой перегрузки выглядит как признак незнания стандарта в плане перегрузки операторов. Потому что в этом случае перегрузку нужно проводить в виде метода.
    Исключением являться может только определение перегрузки оператора по месту объявления дружественности. Я уже объяснял почему это может быть удобно.
    Такая перегрузка становится только ADL-доступной и не может быть вызвана через полную квалификацию имени.

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

    Это не так. У тебя неверное понимание. Наверное ты уже подзабыл мое прошлое объяснение по этому вопросу. Я рекомендую повторно обратиться к тому вопросу и ответу.
    friend std::ostream& operator<<(std::ostream& os, const Object & r) {/**/ return os;}

    Тут operator << все так же остается глобальной функцией, определенной в том же пространстве имен, где определен и тип Object, но не в пространстве имен типа Object. Но, будучи определенным по месту объявления дружественности, оператор стал только ADL-доступным. Обращение к этому оператору может быть найдено только тогда, когда в конструкции std::cout << obj; этот obj имеет тип Object.

    Но почему такая же логика не работает с обычной функцией?

    Минимально, потому что операторы никак нельзя сравнивать ни с глобальными функциями, ни с методами. У операторов своя отдельная методика вызова, отличная от функций.
    friend void fOUT (void) { }, опять же, является определением по месту объявления дружественности и доступна только через ADL. Но у нее нет аргументов чтобы ADL смог найти ее при обращении. Поэтому такая конструкция является бесполезной.
    Ответ написан
    Комментировать
  • [OpenGL]Почему получается такая фигура?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    GL_POLYGON предназначен для вывода только выпуклых многоугольников, а у тебя он выпуклым не является.

    Фигурки для тетриса рисовать стоит или в режиме GL_QUAD, или в режиме GL_TRIANGLES, явным образом выделяя отдельные квадратные участки.
    Так проще ориентироваться в точках.
    Ответ написан
    Комментировать
  • Как сделать простое окно Vulkan с отображением какого либо примитива?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    На gamedev.ru есть статьи по вулкану. В частности, там есть демонстрация самого минимального примера.
    На хабре есть много статей по вулкану. Среди них есть и уроки от самого начала работы, и некоторые минимальные примеры.
    У GLFW есть свои собственные материалы по работе с вулканом. А на их гитхабе есть прямо готовый пример для треугольника.
    Ответ написан
    5 комментариев
  • Где можно найти курс по разработке 3д игры на c++ и vulkan?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Низкий порог вхождения для C++ и Vulkan означает продвинутый уровень владения инструментом C++, экспертные навыки обработки графики, работы с шейдерами и управления памятью GPU (да, там все иначе). Для входа в работу с Vulkan нужно быть, как минимум, Middle Graphics Engineer и уже уметь уверенно работать с DirectX11 или OpenGL4.5. Без этих знаний вулкан будет очень сложно понять, а правильно работать с ним получится только через десятки и сотни полностью неудачных итераций написать одно и то же.

    Vulkan является очень низкоуровневым GAPI и требует от пользователя изначально серьезной подготовки. У этого GAPI много точек привязки к системной памяти, содержимое которой трактуется как на GPU, так и на CPU. Поэтому работать с памятью в C++ правильно нужно уметь с самого начала. Поэтому, еще до начала работы с вулканом от пользователя требуются экспертные знания языка. В противном случае вместо обучения работе с довольно сложным GAPI получится блуждание по полю граблей, где ничего не понятно.

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

    И тем не менее, вулкан легко не дастся. Для его понимания нужна база, нужно знать устройство GPU, принципы коммуникации с ним, принципы его работы и всю теорию обработки графики. Нужно уже уметь быстро писать много стабильного и сложного кода на C++, нужно уметь безошибочно писать на GLSL или SPIR-V. Нужно уметь пользоваться графическими отладчиками, профилировщиками, разбираться в диагностике проблем при работе с графикой.
    Все это приобрести можно в процессе практики с DirectX11 и OpenGL4.5.
    Ответ написан
    6 комментариев
  • Каким образом одна и та же игра может рендериться за счёт разных графических библиотек?

    @MarkusD
    все время мелю чепуху :)
    Любой GAPI является инструментом прикладного уровня, который инженер может встроить в свой код с целью пользоваться его возможностями. Прямое использование любого API является примером тесной интеграции кода программы с кодом целевой платформы.
    Целевой платформой является аппаратно-программный комплекс, на котором планируется запускать разрабатываемый код. Целевой может быть Sony Playstation, ПК с виндой, Мак или мобила с Андроидом.

    Код с тесной интеграцией целевых API нередко пропитан их общей "атмосферой". Это выражается в стиле кода, в общих конструкциях, в заточенности кода проекта на использование совместно с конкретными API.
    Использующий DirectX8 код не так просто перевести на использование DirectX9, еще сложнее перевести на использование DirectX10/11 или DirectX12. Код с использованием OpenGL1.1 тяжело поддается модификации для использования OpenGL2.0, 3.0 или дальше. Любая кодовая база с тесной интеграцией DirectX/OpenGL очень тяжело переводится на Vulkan, Metal или профильные GAPI некоторых закрытых целевых платформ.

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

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

    Когда агностический слой выведен, весь кроссплатформенный код пользуется только им. Под агностическим слоем, тем или иным способом, весь код сводится к использованию целевого API.
    Таким образом, для того чтобы позволить игре рендериться и через DirectX, и через OpenGL, и через Vulkan, нужно в движке игры разработать универсальный внутренний GAPI, вызовы которого будут передаваться в выбранный пользователем целевой GAPI. Форматы данных, ресурсы и шейдеры, при этом, ровно так же выбираются исходя из выбранного пользователем целевого GAPI и могут пересекаться между разными GAPI.
    Те же шейдеры сегодня спокойно транслируются из HLSL в GLSL или SPIR-V специальными трансляторами. Геометрия может быть одинакова, форматы текстур на целевой платформе между всеми GAPI, как правило, одинаковы.
    Дело остается только за грамотной разработкой своего маленького виртуального GPU.
    Ответ написан
    Комментировать
  • Можно ли без высшего образования работать в Геймдеве?

    @MarkusD
    все время мелю чепуху :)
    Без вышки работать можно не только в геймдеве. Вообще везде можно работать. Это иногда даже негласно приветствуется.
    За такую работу можно даже получать некоторые деньги, которых будет хватать на жизнь.

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

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

    Оттарабанить 4-6 лет, вытягивая лямку нормативов на экзаменах без четкого понимания требований к тебе - это пустить деньги и время на ветер.
    В ВУЗ нужно идти за обучением самоконтролю, за обучением самодисциплине, за обучением самоорганизации. Вот те самые навыки, которые сегодня дает ВУЗ. Диплом магистра, бакалавра или специалиста - это дополнительный бонус. Разовьешь эти навыки самостоятельно - станешь одним из точно таких же самородков.
    В ВУЗ стоит идти за трамплином к знаниям. Чаще всего человека надо только подтолкнуть чтобы он стал специалистом. А толчком таким и является программа базового обучения в ВУЗе. Обучение базовое потому что его для последующей работы все равно хватать не будет. Дальше с этого трамплина нужно рвать во весь опор, находя и усваивая самые важные и самые нужные для своей работы знания. Осилишь найти все эти знания сам - ну чтож, ты один из немногих способных.
    По окончании ВУЗа человек не выпускается готовым к работе. На этом этапе он обладает только самыми базовыми навыками и дальше нужно продолжать учиться по профилю работы. Для этого есть стажировки, квалификационные курсы, а так же разнообразные книги и циклы статей для самостоятельного обучения.
    ВУЗы не готовят людей к работе, ВУЗы готовят людей к самостоятельной профессиональной подготовке.

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

    @MarkusD
    все время мелю чепуху :)
    Компьютерная графика - очень большая и сложная область. Обучаться до конкурентного уровня нужно очень усердно и очень долго.

    С чего стоит начать. С математики, начала анализа, геометрии, линейной алгебры, с теории множеств. С физики, оптики и механики.
    В этих областях информации нужно буквально плавать.

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

    После этого двигаться стоит в сторону инструментов разработки. Их должно быть несколько. C# и F# ты знаешь, это хорошо. Java будет прекрасным дополнением. Настоящий инженер не имеет права зажимать себя рамками одного лишь инструмента, это будет его минус в конкуренции. Rust слабо востребован и мало применяется, но знать его на некотором уровне будет просто полезно в качестве инвестиции и для общего развития. C++ сильно распространен и сильно востребован, однако рынок труда сейчас переполнен слабыми середнячками, которые мало на что годятся в реальной работе, а C++ является крайне сложным инструментом и не позволит тебе быстро начать с ним работать на том же уровне, на котором тебе позволяет тот же C#. Поэтому если брать C++, то уходить в него надо прямо очень серьезно для того чтобы получить конкурентное преимущество перед описанными выше людьми.

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

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