Ответы пользователя по тегу C++
  • Почему сплайны OpenGL прорисовываются не на всех компьютерах?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Дело в том, что функция glMap1[?] имеет довольно узкий коридор поддержки. Она была введена в OpenGL 1.0 и удалена из поддержки в OpenGL 3.2 Core Profile.

    Т.к. ты пользуешься wglCreateContext, система сама решает какой контекст ей для тебя создавать. Это может быть и контекст с версией 4.6, в котором уже нет поддержки функции glMap1f.
    Тебе стоит более точно указывать версию создаваемого контекста. Это можно сделать с помощью расширения WGL_ARB_create_context. Функция wglCreateContextAttribsARB позволяет задавать атрибуты для создаваемого контекста, среди которых ты можешь обозначить и требуемую версию.

    В качестве примера использования этого расширения можно взять такой код.
    Код примера
    // Sample code showing how to create a modern OpenGL window and rendering context on Win32.
    
    #include <windows.h>
    #include <gl/gl.h>
    #include <stdbool.h>
    
    typedef HGLRC WINAPI wglCreateContextAttribsARB_type(HDC hdc, HGLRC hShareContext,
            const int *attribList);
    wglCreateContextAttribsARB_type *wglCreateContextAttribsARB;
    
    // See https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_create_context.txt for all values
    #define WGL_CONTEXT_MAJOR_VERSION_ARB             0x2091
    #define WGL_CONTEXT_MINOR_VERSION_ARB             0x2092
    #define WGL_CONTEXT_PROFILE_MASK_ARB              0x9126
    
    #define WGL_CONTEXT_CORE_PROFILE_BIT_ARB          0x00000001
    
    typedef BOOL WINAPI wglChoosePixelFormatARB_type(HDC hdc, const int *piAttribIList,
            const FLOAT *pfAttribFList, UINT nMaxFormats, int *piFormats, UINT *nNumFormats);
    wglChoosePixelFormatARB_type *wglChoosePixelFormatARB;
    
    // See https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_pixel_format.txt for all values
    #define WGL_DRAW_TO_WINDOW_ARB                    0x2001
    #define WGL_ACCELERATION_ARB                      0x2003
    #define WGL_SUPPORT_OPENGL_ARB                    0x2010
    #define WGL_DOUBLE_BUFFER_ARB                     0x2011
    #define WGL_PIXEL_TYPE_ARB                        0x2013
    #define WGL_COLOR_BITS_ARB                        0x2014
    #define WGL_DEPTH_BITS_ARB                        0x2022
    #define WGL_STENCIL_BITS_ARB                      0x2023
    
    #define WGL_FULL_ACCELERATION_ARB                 0x2027
    #define WGL_TYPE_RGBA_ARB                         0x202B
    
    static void
    fatal_error(char *msg)
    {
        MessageBoxA(NULL, msg, "Error", MB_OK | MB_ICONEXCLAMATION);
        exit(EXIT_FAILURE);
    }
    
    static void
    init_opengl_extensions(void)
    {
        // Before we can load extensions, we need a dummy OpenGL context, created using a dummy window.
        // We use a dummy window because you can only set the pixel format for a window once. For the
        // real window, we want to use wglChoosePixelFormatARB (so we can potentially specify options
        // that aren't available in PIXELFORMATDESCRIPTOR), but we can't load and use that before we
        // have a context.
        WNDCLASSA window_class = {
            .style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
            .lpfnWndProc = DefWindowProcA,
            .hInstance = GetModuleHandle(0),
            .lpszClassName = "Dummy_WGL_djuasiodwa",
        };
    
        if (!RegisterClassA(&window_class)) {
            fatal_error("Failed to register dummy OpenGL window.");
        }
    
        HWND dummy_window = CreateWindowExA(
            0,
            window_class.lpszClassName,
            "Dummy OpenGL Window",
            0,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            0,
            0,
            window_class.hInstance,
            0);
    
        if (!dummy_window) {
            fatal_error("Failed to create dummy OpenGL window.");
        }
    
        HDC dummy_dc = GetDC(dummy_window);
    
        PIXELFORMATDESCRIPTOR pfd = {
            .nSize = sizeof(pfd),
            .nVersion = 1,
            .iPixelType = PFD_TYPE_RGBA,
            .dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
            .cColorBits = 32,
            .cAlphaBits = 8,
            .iLayerType = PFD_MAIN_PLANE,
            .cDepthBits = 24,
            .cStencilBits = 8,
        };
    
        int pixel_format = ChoosePixelFormat(dummy_dc, &pfd);
        if (!pixel_format) {
            fatal_error("Failed to find a suitable pixel format.");
        }
        if (!SetPixelFormat(dummy_dc, pixel_format, &pfd)) {
            fatal_error("Failed to set the pixel format.");
        }
    
        HGLRC dummy_context = wglCreateContext(dummy_dc);
        if (!dummy_context) {
            fatal_error("Failed to create a dummy OpenGL rendering context.");
        }
    
        if (!wglMakeCurrent(dummy_dc, dummy_context)) {
            fatal_error("Failed to activate dummy OpenGL rendering context.");
        }
    
        wglCreateContextAttribsARB = (wglCreateContextAttribsARB_type*)wglGetProcAddress(
            "wglCreateContextAttribsARB");
        wglChoosePixelFormatARB = (wglChoosePixelFormatARB_type*)wglGetProcAddress(
            "wglChoosePixelFormatARB");
    
        wglMakeCurrent(dummy_dc, 0);
        wglDeleteContext(dummy_context);
        ReleaseDC(dummy_window, dummy_dc);
        DestroyWindow(dummy_window);
    }
    
    static HGLRC
    init_opengl(HDC real_dc)
    {
        init_opengl_extensions();
    
        // Now we can choose a pixel format the modern way, using wglChoosePixelFormatARB.
        int pixel_format_attribs[] = {
            WGL_DRAW_TO_WINDOW_ARB,     GL_TRUE,
            WGL_SUPPORT_OPENGL_ARB,     GL_TRUE,
            WGL_DOUBLE_BUFFER_ARB,      GL_TRUE,
            WGL_ACCELERATION_ARB,       WGL_FULL_ACCELERATION_ARB,
            WGL_PIXEL_TYPE_ARB,         WGL_TYPE_RGBA_ARB,
            WGL_COLOR_BITS_ARB,         32,
            WGL_DEPTH_BITS_ARB,         24,
            WGL_STENCIL_BITS_ARB,       8,
            0
        };
    
        int pixel_format;
        UINT num_formats;
        wglChoosePixelFormatARB(real_dc, pixel_format_attribs, 0, 1, &pixel_format, &num_formats);
        if (!num_formats) {
            fatal_error("Failed to set the OpenGL 3.3 pixel format.");
        }
    
        PIXELFORMATDESCRIPTOR pfd;
        DescribePixelFormat(real_dc, pixel_format, sizeof(pfd), &pfd);
        if (!SetPixelFormat(real_dc, pixel_format, &pfd)) {
            fatal_error("Failed to set the OpenGL 3.3 pixel format.");
        }
    
        // Specify that we want to create an OpenGL 3.3 core profile context
        int gl33_attribs[] = {
            WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
            WGL_CONTEXT_MINOR_VERSION_ARB, 3,
            WGL_CONTEXT_PROFILE_MASK_ARB,  WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
            0,
        };
    
        HGLRC gl33_context = wglCreateContextAttribsARB(real_dc, 0, gl33_attribs);
        if (!gl33_context) {
            fatal_error("Failed to create OpenGL 3.3 context.");
        }
    
        if (!wglMakeCurrent(real_dc, gl33_context)) {
            fatal_error("Failed to activate OpenGL 3.3 rendering context.");
        }
    
        return gl33_context;
    }
    
    static LRESULT CALLBACK
    window_callback(HWND window, UINT msg, WPARAM wparam, LPARAM lparam)
    {
        LRESULT result = 0;
    
        switch (msg) {
            case WM_CLOSE:
            case WM_DESTROY:
                PostQuitMessage(0);
                break;
            default:
                result = DefWindowProcA(window, msg, wparam, lparam);
                break;
        }
    
        return result;
    }
    
    static HWND
    create_window(HINSTANCE inst)
    {
        WNDCLASSA window_class = {
            .style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
            .lpfnWndProc = window_callback,
            .hInstance = inst,
            .hCursor = LoadCursor(0, IDC_ARROW),
            .hbrBackground = 0,
            .lpszClassName = "WGL_fdjhsklf",
        };
    
        if (!RegisterClassA(&window_class)) {
            fatal_error("Failed to register window.");
        }
    
        // Specify a desired width and height, then adjust the rect so the window's client area will be
        // that size.
        RECT rect = {
            .right = 1024,
            .bottom = 576,
        };
        DWORD window_style = WS_OVERLAPPEDWINDOW;
        AdjustWindowRect(&rect, window_style, false);
    
        HWND window = CreateWindowExA(
            0,
            window_class.lpszClassName,
            "OpenGL",
            window_style,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            rect.right - rect.left,
            rect.bottom - rect.top,
            0,
            0,
            inst,
            0);
    
        if (!window) {
            fatal_error("Failed to create window.");
        }
    
        return window;
    }
    
    int WINAPI
    WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmd_line, int show)
    {
        HWND window = create_window(inst);
        HDC gldc = GetDC(window);
        HGLRC glrc = init_opengl(gldc);
    
        ShowWindow(window, show);
        UpdateWindow(window);
    
        bool running = true;
        while (running) {
            MSG msg;
            while (PeekMessageA(&msg, 0, 0, 0, PM_REMOVE)) {
                if (msg.message == WM_QUIT) {
                    running = false;
                } else {
                    TranslateMessage(&msg);
                    DispatchMessageA(&msg);
                }
            }
    
            glClearColor(1.0f, 0.5f, 0.5f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            // Do OpenGL rendering here
    
            SwapBuffers(gldc);
        }
    
        return 0;
    }
    Ответ написан
    Комментировать
  • Выбор игрового движка для C++?

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

    Cocos2d-x является одним из самых популярных открытых движков. У него большое сообщество и масса поклонников. Есть документация и все нужное для старта.

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

    Godot Engine не менее популярен и не менее поднят по возможностям. В чем-то Godot даже будет лучше чем Cocos. Сообщество у него тоже большое. Документация тоже присутствует.

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

    Дальше пойдут не такие популярные решения, однако и проходить мимо них тоже не стоит.

    Urho3D является нареченной Open-Source альтернативой Unity. Движок используется многими энтузиастами. По разным уголкам сети раскиданы многочисленные группы обсуждения этого движка. Документация и примеры у него на месте.

    GDevelop - это довольно популярное решение для небольших игр. Документация на месте.

    Panda3D - тоже довольно популярное решение со своим сообществом. Документация имеется.

    Hazel Engine - один разработчик - один движок. Полностью вся разработка изложена в видео на youtube. Пользоваться можно... на свой страх и риск.

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

    GZDoom - современная инкарнация движка DOOM.

    Дальше решения пойдут или сложные, или экзотические. Всё на свой страх и риск.

    CryEngine - от Crytek.
    X-Ray - движок S.T.A.L.K.E.R.
    UE 3 - для коммерческих проектов использовать нельзя.
    Lumberyard - от Amazon. Да-да, тот самый.
    Banshee Engine - он просто существует.
    Diligent Engine - у него есть свое сообщество.
    Atomic Engine - на нем тоже выпускают игры.
    Lumix Engine - тоже что-то может.
    Horde 3D - просто существует и этого уже достаточно.
    Ответ написан
    Комментировать
  • Почему дружественная функция, определённая внутри класса с первым параметром встроенного типа, недоступна вне определения класса?

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

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

    Функция f5 не просто первично объявлена, она и определена по месту объявления дружественности. Ее имя является однозначно неквалифицированным в следствии своего определения.

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

    Все потому что т.н. имена скрытых друзей могут быть найдены только средствами ADL.
    Если коротко, Argument-Dependent Lookup опирается на типы аргументов при вызове функции, пространства их имен и пространства имен, в которых эти типы объявлены.
    ADL не выполняет поиск в пространствах имен в отношении фундаментальных типов. Поэтому код f5(5); буквально обречен на ошибку трансляции.

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

    В результате.
    Чтобы ADL нашел функцию f5, среди ее параметров обязан быть параметр и с типом A.
    Чтобы UNL или QNL смогли найти функцию f5, ее надо дополнительно объявить за пределами типа A в его пространстве имен.
    Ответ написан
    1 комментарий
  • Как объяснить данный фрагмент кода?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    В данном коде демонстрируется типичная ошибка подмены типа (type punning [?]), ведущая к неминуемому UB [?] по стандарту.

    num в данном случае представляет собой шестнадцатеричное число, при выводе оно переводится в десятичное число типа double.


    Вот что стандарт говорит о подмене типа.
    [basic.lval#11]
    If a program attempts to access the stored value of an object through a glvalue whose type is not similar to one of the following types the behavior is undefined:
    (11.1) -- the dynamic type of the object,
    (11.2) -- a type that is the signed or unsigned type corresponding to the dynamic type of the object, or
    (11.3) -- a char, unsigned char, or std​::​byte type.

    Относительно unsigned long long тип double не обладает ни одним из требуемых стандартом свойств. Отсюда прямой вывод что данный код вносит UB.
    Поэтому под большим вопросом остается то, что выводится данным кодом.

    В итоге, правильным примером подмены типа на другой может быть только пример, где используется подмена на разрешенный тип представления объекта: char, unsigned char или std::byte для C++17.
    C++ Core Guidelines в секции C.183 однозначно не советует для подмены использовать union. Этого действительно не стоит делать даже при условии того, что все современные трансляторы понимают такие конструкции так, как подозревает писатель.

    Почему неверно работает, к примеру, такой фрагмент кода:

    Второй блок кода означает приведение значения из типа unsigned long long в тип double. В этом случае производится именно стандартное преобразование типа, а не подмена с целью интерпретации представления памяти num иным образом.
    0xC068C0C000000000 для типа unsigned long long означает 13864543383726325760, при преобразовании этого значения в тип double значение будет преобразовано в 1.3864543383726326e+19. В таком виде оно и будет выведено.

    Относительно первого блока кода, лучшим примером подмены типа для кода из вопроса будет такой:
    unsigned long long num = 0xC068C0C000000000;
    double representation = {};
    memcpy( &representation, &num, sizeof( num ) );
    cout << representation;

    И не стоит бояться обращения к memcpy тут. Данный код полностью соответствует стандарту и ожидаемо оптимизируется транслятором до желаемого результата.

    Мне не понятно, как работает такая конструкция, т.е зачем передавать именно ссылку на переменную num?

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

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    В вакансиях много чего пишут и нередко мало понимают что пишут. Номер стандарта сегодня является некоторым атрибутом хайпа, которым очередная нерадивая компания буквально обязана блеснуть ради привлечения увлеченного энтузиаста к круглогодичному ковырянию своего легаси на C98 вперемешку с шаблончиками из C++03.
    Хотя, безусловно, есть и высококлассные коллективы с передовыми знаниями и актуальными стандартами качества, где работать - сплошная мечта.

    Между тем, понимание конкретного стандарта языка означает не только знание этого стандарта, но и знание всех стандартов до него, и понимание различий между ними всеми. А так же - понимание цены перехода в коде от более старого стандарта к более новому.
    Понимание C++17, прежде всего, означает понимание цены перехода с C++14 в коде с auto и инициализацией списками, понимание цены отказа от std::tie в пользу структурного связывания (не говоря уже о понимании семантики структурного связывания) и понимание рациональности перехода на структурное связывание в конкретном месте кода, понимание последствий использования автоматического выведения аргументов шаблона типа в месте вызова конструктора и возможные последствия применения этого подхода в конкретном участке кода, понимание семантики работы с this в замыкании лямбды и отличия этого поведения в более старых стандартах. И так далее.
    Ответ написан
    Комментировать
  • Как сделать так чтобы функция принимала неограниченное число аргументов разного типа?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    В C++ гораздо сложнее написать функцию, которая принимала бы переменное количество аргументов одного типа, чем разных типов.
    Задача решается с помощью т.н. вариативных параметров шаблона [?].

    template< typename... TArguments >
    void Foo( const TArguments&... arguments )
    {
    	static_assert( sizeof...( TArguments ) > 0, "This function shall not be called without arguments." );
    }


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

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Как показало обсуждение в комментариях, исключение бросается из функции glTexImage2D.
    Подозрение в этом случае может пасть только на то, что размер переданной через data памяти меньше расчетного размера, который определяет функция исходя из параметров width, height, GL_RGBA, GL_UNSIGNED_BYTE.

    Это может означать что переданные в glTexImage2D параметры не соответствуют считанному изображению. А т.к. width и height получены напрямую при чтении изображения, вопросы возникать могут только относительно заявленного формата изображения - GL_RGBA, GL_UNSIGNED_BYTE.

    Собственно, именно так и оказалось на самом деле. Прочитанное изображение имеет другой формат и меньше каналов, а следовательно и меньший размер относительно расчетного.

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

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

    Вопросов к первому варианту нет. Тут сразу и наглядно видно агрегатную инициализацию, т.к. тип Book является агрегатным.

    Ко второму варианту есть вопросы. Какова цель присутствия auto вместо явного указания типа переменной? Зачем писателю потребовалось дважды указать тип, заменив первое указание на auto? Какова цель инициализации копией для данной переменной? Зачем писатель написал так сложно?
    Ответы на каждый вопрос должны предоставлять веское доказательство необходимости существования именно такого кода.

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

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

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

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

    Одним из критериев выбора скриптового движка является его производительность. Чтобы не занимать специалистов подобной рутиной, когда-то давно уже были проведены замеры версий различных скриптовых движков. Результаты замеров доступны всем желающим.
    Однако, стоит напомнить, что не всегда самое быстрое решение является самым оптимальным.
    Ответ написан
    1 комментарий
  • Как организовать Tween систему в рамках ECS (entt)?

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

    Почему дизайн с одним компонентом обречен. Каждая система в ECS должна работать с выборкой по компонентам. В EnTT это делается с помощью представления потока компонентов - entt::registry::view.
    Такое представление оптимальным образом организует последовательность сущностей, в которых точно присутствуют обозначенные в представлении компоненты. Если начать проверять наличие у сущности иных компонент, помимо того что это противоречит концепции ECS, это затронет выборку из памяти за пределами отображения и сильно снизит производительность системы. Поэтому системе не стоит работать с компонентами за пределами своей выборки.

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

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

    Функции интерполяции у тебя всегда чистые, т.е. зависят только от переданных им аргументов. Для любого типа данных, от скаляра и до матрицы, можно определить функцию интерполяции между двумя значениями. Это все - внешние относительно ECS вещи, а внутри ECS на каждый ***InterpolationComponent у тебя будет своя система, которая делает выборку по целевому компоненту и по компоненту интерполируемости. Таким образом, уже на стадии выборки компонентов у тебя останутся только подходящие сущности, а все остальные - отфильтруются.
    Заметить стоит еще то, что такой подход позволяет одно конкретное значение интерполировать только одной функцией за раз. Если для одного значения требуется несколько одновременных интерполяций, такой подход уже не подходит. С другой стороны, для того чтобы правильно свести несколько таких анимаций для одного значения, требуется значительно более сложная система, чем просто функция интерполяции.
    Ответ написан
    2 комментария
  • Как избавиться от warning C26812: Enum type VkResult is unscoped?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Особенностью нестрогих перечислений является то, что все значения перечисления являются глобальными целочисленными константами подлежащего типа. Подлежащим по умолчанию является тип int.
    Таким образом, вместо VkResult result можно написать int result в виду того, что перечисление VkResult является нестрогим.

    С другой стороны, C26812 [?] относится к разделу Enum.3 из C++ Core Guidelines [?] и напрямую указывает на место определения нестрогого перечисления. К строке VkResult result = оно отношения не имеет.

    Дополнительно. #pragma warning(push, 0) [?] не делает того, что ты от него ожидаешь.
    Предупреждения подавлять стоит таким образом.
    #pragma warning( push )
    #pragma warning( disable: 26812 )
    #include <GLFW/glfw3.h>
    #pragma warning( pop )
    Ответ написан
    Комментировать
  • Почему работает const, а не constexpr для char*?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Дело в том, что указанный слева квалификатор const относится к правой половине спецификации типа до первого модификатора.
    const char& - ссылка на константный символ. Квалификатор - const, модификатор - &.
    const char* - указатель на память константного символа. Квалификатор - const, модификатор - *.

    При этом, указанный справа квалификатор const относится ко всей части спецификации типа левее, включая все модификаторы.
    char* const - константный указатель на память символа.
    const char* const - константный указатель на память константного символа.
    char* const * - указатель на память константного указателя на память символа.
    char& const существовать не может, т.к. квалификаторы не применяется к ссылкам. Тут будет ошибка трансляции.

    И при чем же здесь constexpr? Просто constexpr всегда относится только ко всей спецификации типа со всеми модификаторами.
    const char* - указатель на память константного символа.
    constexpr char* - константный указатель времени компиляции на память символа. Тут нет ошибки, память символа тут считается модифицируемой.

    И если объект с типом constexpr char* получит характеристику ODR-used [?], то после трансляции кода это будет уже объект с типом char* const. Вот так.
    В то же время, строковые литералы имеют тип const char[N], т.е. статически определенный массив константных символов. Такой тип можно привести только к типу const char*.

    В результате, чтобы правильно определить константный указатель времени компиляции на память константного символа, нужно тип определить как constexpr const char*.
    И const в этом месте никакого прямого отношения к constexpr не имеет.
    Ответ написан
    Комментировать
  • Разработка игр для андроид на языке С++?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    C++ не является полноценно "родным" языком для Android. Но для этой платформы можно вести разработку на C++.
    Такие движки, как cocos2d-x [?], Urho 3D [?], Unreal Engine 4 [?], Godot Engine [?] и, буквально, море менее известных проектов, позволяют вести разработку игр с использованием языка C++.

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

    Если использование C++ для тебя является важным, о Unity можно забыть. Это взаимоисключающие инструменты.
    И это не важно. Тот же Urho 3D позиционируется как Open Source Unity. Godot Engine обладает не меньшей свободой и гибкостью, но предлагает больше.
    Ответ написан
  • Variadic template, как перебрать в шаблоне все типы по очереди?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Пусть у нас есть некий аксессор:
    template< typename TComponent >
    TComponent* const QueryComponent();


    Пусть у нас есть, для простоты, некоторый обработчик:
    void PerformWorkflow( A*, B*, C* );
    Тут A, B и C - это типы компонентов.

    Чтобы подружить обработчик с аксессором необходимых ему компонентов, понадобится самый простой Parameter pack expansion.
    template< typename... TComponents >
    void ApplyComponents( void (*workflow)( TComponents*... ) )
    {
    	workflow( QueryComponent<TComponents>()... );
    }

    Здесь можно посмотреть как работает пример этого кода.

    В комментарии я вижу несоответствие типа параметра функции для each и типа результата get. Стоит напомнить что разыменование nullptr, который может вернуть get, приводит к неопределенному поведению [?].
    Поэтому, если распаковку переписать на *QueryComponent<TComponents>()..., т.е. на лету разыменовывать полученные указатели, то перед запуском функции для твоей entity следует убедиться что у нее есть все необходимые компоненты. Такая проверка является необходимой для запуска систем в ECS, т.к. система должна обрабатывать только удовлетворяющие своим фильтрам сущности.

    Для этого уже потребуется использовать Fold expression из C++17.
    template <typename... T>
    void each(typename identity<std::function<void(T& ...)>>::type func)
    {
    	for(auto &entity: m_entities)
    	{
    		if( !( (entity.HasComponent<T>() && ...) ) )
    		{
    			continue;
    		}
    		
    		func( *entity.get<T>()... );
    	}
    }
    Ответ написан
    1 комментарий
  • Инициализация элемента к нулю?

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

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

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

    Интереснее будет рассмотреть поведение инициализации значением.
    Форма T() может привести к вызову конструктора с пустым std::initializer_list, к вызову конструктора по умолчанию, а может свестись к инициализации по умолчанию в том случае, если конструктор по умолчанию удален или по своим причинам пропущен в пользовательском типе.
    Каждый, кроме последних двух, пункт поведения инициализации значением оперирует словом class, т.е. имеет отношение только к сложным типам. Последний пункт описывает поведение инициализации для всех неописанных выше случаев как инициализацию нулем.
    И именно инициализация нулем написана в примере вопроса.

    Форма T() хорошо подходит для инициализации локальных временных объектов, а для инициализации именованных объектов лучше подходит форма T object {};.
    Форма T object = T();, на самом деле, выполняет инициализацию копией, где в качестве копии используется локальный временный объект T().

    В результате, конкретно конструкция T() или T{} может привести к вызову конструктора по умолчанию или вызову конструктора от пустого std::initializer_list для сложного типа и инициализацию нулем для простого типа.
    Это - очень полезное поведение в метапрограммировании, где у тебя может быть шаблон типа, в котором требуется по значению разместить объект с типом параметра шаблона. Этот объект требуется инициализировать правильным образом, но ты никак не можешь классифицировать тип, передаваемый параметром шаблона. Благодаря инициализации значением можно не задаваться вопросами природы типа из шаблонного параметра.

    Но увлекаться этим подходом тоже не стоит. Я предпочту вариант std::string string; варианту std::string string = std::string(); потому что для std::string инициализация по умолчанию сделает точно то же самое, будет эффективнее (т.к. во втором случае будет копирование) и будет понятнее.
    Код HRESULT res = HRESULT(); стоит заменить на инициализацию одним из базовых значений. Например - кодом S_OK. Это создаст понимание базового состояния программы в месте определения переменной. Тип HRESULT имеет очень сильную семантику и сразу о многом говорит подготовленному читателю.
    Только HWND window = HWND(); или HWND window{}; на самом деле смотрится к месту, т.к. в WinAPI нет предопределенного начального значения и объект проще провести через стандартную инициализацию значением. Уж этот краевой случай разработчики WinAPI должны предусмотреть.
    Ответ написан
    1 комментарий
  • Лучшие источники для изучения CPP?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    В самую первую очередь - это будет документация языка. Ее очень удобно использовать как справочник. Это - твой самый первый источник информации по любому вопросу.
    isocpp поддерживается создателем языка и содержит море полезной информации.
    C++ Core Guidelines является манифестом пользователя C++. Его знать обязательно. Документ регулярно дополняется.

    More C++ Idioms. Шаблоны проектирования имеют свою собственную многомерную классификацию. Идиомы - это функциональные шаблоны проектирования, применимые, как правило, или для конкретного языка, или для некоторого семейства языков. Эта открытая книга помогает ориентироваться в некотором начальном наборе идиом конкретно для языка C++.
    C++ Patterns - еще один полезный ресурс для изучения применимых к C++ шаблонов проектирования.
    С Fluent C++ ты уже знаком.
    Безусловно, блог создателей PVS-Studio.
    Habr, конечно же.
    Блогов очень много, их можно просто найти по релевантной фразе "C++ blog".

    Помимо этого есть большое количество каналов от разных конференций, доклады на которых всегда помогают понять язык лучше.
    С++Russia,
    C++Now,
    Pacific C++,
    CppCon,
    code::dive,
    Meeting C++.

    Так же будет полезно изучить книги авторов:
    Андрея Александреску,
    Герба Саттера,
    Девида Вандервуда,
    Скотта Мейерса,
    Роберта Мартина.
    Есть и другие очень полезные авторы. Тут у меня, пожалуй, только самый основной список.

    Последим, и самым важным, источником будет текущая рабочая версия стандарта языка, а так же пара лабораторий для практики: Compiler Explorer и C++ Insights.
    Ответ написан
    Комментировать
  • Как вычислить Z для линии?

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

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

    Допустим, у нас есть тип 3D-позиции.
    struct Vector3i final
    {
    	int32_t x;
    	int32_t y;
    	int32_t z;
    };


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

    Пример типа AxisProxy
    using AxisPointer = int32_t Vector3i::*;
    
    class AxisProxy final
    {
    public:
        AxisProxy( AxisPointer major_axis, AxisPointer middle_axis, AxisPointer minor_axis)
            : m_major_axis{ major_axis }
            , m_middle_axis{ middle_axis }
            , m_minor_axis{ minor_axis }
        {}
    
    public:
        inline int32_t& AccessMajorAxis( Vector3i& value ) const               { return value.*m_major_axis; };
        inline int32_t& AccessMiddleAxis( Vector3i& value ) const              { return value.*m_middle_axis; };
        inline int32_t& AccessMinorAxis( Vector3i& value ) const               { return value.*m_minor_axis; };
    
        inline const int32_t& AccessMajorAxis( const Vector3i& value ) const   { return value.*m_major_axis; };
        inline const int32_t& AccessMiddleAxis( const Vector3i& value ) const  { return value.*m_middle_axis; };
        inline const int32_t& AccessMinorAxis( const Vector3i& value ) const   { return value.*m_minor_axis; };
    
    private:
        AxisPointer    m_major_axis    = &Vector3i::x;
        AxisPointer    m_middle_axis   = &Vector3i::y;
        AxisPointer    m_minor_axis    = &Vector3i::z;
    };


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

    Примерное объявление типа LineIterator
    class LineIterator final
    {
    public:
        LineIterator( const Vector3i from, const Vector3i to );
    
    public:
        const Vector3i& operator ++ ();
        const Vector3i operator ++ ( int );
    
        inline const Vector3i& operator * () const     { return m_current_point; };
        inline const Vector3i* operator -> () const    { return &m_current_point; };
    
    private:
        static inline const int32_t GetCorrectionStepAxis( const int32_t value )   { return std::abs( value ) << 1; };
        static inline const int32_t GetShiftStepAxis( const int32_t value )        { return ( value > 0 ) - ( value < 0 ); };
    
        void PerformLineStep();
    
    private:
        Vector3i   m_current_point;            // Current position at line.
        Vector3i   m_correction_step;            // Values to change the point corrections.
        Vector3i   m_shift_step;                // The shift step for current point in each iteration.
        int32_t    m_middle_axis_correction;    // The marker for middle axis correction.
        int32_t    m_minor_axis_correction;    // The marker for minor axis correction.
        AxisProxy  m_axis_proxy;                // Point fields proxy.
    };


    Это - обычный поступательный итератор, каждый шаг которого отображает движение вдоль заданной при конструировании линии. Его операторы будут очень простыми.
    Пример кода операторов итератора
    const Vector3i& LineIterator::operator ++ ()
    {
        PerformLineStep();
        return m_current_point;
    }
    
    const Vector3i LineIterator::operator ++ ( int )
    {
        Vector3i current_point{ m_current_point };
        PerformLineStep();
        return current_point;
    }


    С инициализацией будет уже поинтереснее.
    Пример кода конструктора итератора
    LineIterator::LineIterator( const Vector3i from, const Vector3i to )
        : m_current_point{ from }
    {
        const Vector3i line_delta{ to - from };
    
        m_correction_step  = { GetCorrectionStepAxis( line_delta.x ), GetCorrectionStepAxis( line_delta.y ), GetCorrectionStepAxis( line_delta.z ) };
        m_shift_step       = { GetShiftStepAxis( line_delta.x ), GetShiftStepAxis( line_delta.y ), GetShiftStepAxis( line_delta.z ) };
    
        AxisPointer axis[3] = { &Vector3i::x, &Vector3i::y, &Vector3i::z };
        std::sort(
            std::begin( axis ), std::end( axis ),
            [this]( const AxisPointer left, const AxisPointer right ) -> bool
            {
                return m_correction_step.*left > m_correction_step.*right;
            }
        );
        m_axis_proxy = { axis[0], axis[1], axis[2] };
    
        m_middle_axis_correction   = m_axis_proxy.AccessMiddleAxis( m_correction_step ) - ( m_axis_proxy.AccessMajorAxis( m_correction_step ) >> 1 );
        m_minor_axis_correction    = m_axis_proxy.AccessMinorAxis( m_correction_step ) - ( m_axis_proxy.AccessMajorAxis( m_correction_step ) >> 1 );
    }

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

    Функция выполнения шага вдоль линии будет очень простой за счет работы с прокси-объектом.
    Пример функции шага итератора
    void LineIterator::PerformLineStep()
    {
        if( m_middle_axis_correction > 0 )
        {
            m_middle_axis_correction -= m_axis_proxy.AccessMajorAxis( m_correction_step );
            m_axis_proxy.AccessMiddleAxis( m_current_point ) += m_axis_proxy.AccessMiddleAxis( m_shift_step );
        }
    
        if( m_minor_axis_correction > 0 )
        {
            m_minor_axis_correction -= m_axis_proxy.AccessMajorAxis( m_correction_step );
            m_axis_proxy.AccessMinorAxis( m_current_point ) += m_axis_proxy.AccessMinorAxis( m_shift_step );
        }
    
        m_middle_axis_correction += m_axis_proxy.AccessMiddleAxis( m_correction_step );
        m_minor_axis_correction += m_axis_proxy.AccessMinorAxis( m_correction_step );
        m_axis_proxy.AccessMajorAxis( m_current_point ) += m_axis_proxy.AccessMajorAxis( m_shift_step );
    }


    Если убрать поле AxisProxy::m_middle_axis из кода и удалить весь код, где на него есть ссылки, то весь оставшийся код будет представлять из себя обычный итератор линии для 2D-пространства. В этом случае оси даже сортировать не надо будет, для инициализации прокси можно будет обойтись одним тернарным оператором.
    Ответ написан
    Комментировать
  • Как разрабатывать оконные приложения на С++ в Visual Studio 2019?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Самым простым решением для разработки GUI-приложений в среде MSVS2019 будет использование Qt. Собственно, для этого тебе потребуется Qt. Но разработку вести ты cможешь в VS.

    Вторым решением будет чуть более сложное: использование C++/CX вместе с UWP / Windows Forms.

    Третьим решением, еще более сложным, будет использование уже чистого C++ при поддержке MFC или WTL.

    И самым сложным решением будет прямое использование WinAPI.

    Помимо всего этого еще можно воспользоваться библиотекой wxWidgets и на ее базе разработать GUI для своего приложения.
    Ответ написан
    1 комментарий
  • Диапазон типов данных C++?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Хочу порекомендовать тебе материал от разработчиков PVS-Studio относительно моделей данных в C++. Там хорошо и коротко объясняется суть твоего вопроса.
    Так же тебе будет полезно изучить документацию C++, где можно изучить подробности.

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

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

    Поэтому, в общем смысле на твой вопрос ответить можно так. Если клиентская система ожидает от процесса одну модель данных, а он работает в другой, то падения процесса не миновать. В ряде случаев процесс даже не загрузится в память, т.к. загрузчик бинарного кода может по косвенным признакам определить несовместимость процесса и системы у клиента.
    Ответ написан
    Комментировать
  • Чем отличается mvpmatrix от тех которые заданы по отдельности и надо ли их подключать (по отдельности) и в каких случаях?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Для лучшего понимания материала тебе стоит изучить, как минимум, эту статью.
    Если коротко
    ModelMatrix - это матрица преобразования локального пространства геометрии конкретного объекта.
    Внутри этого локального пространства геометрия объекта расположена относительно центра пространства. Чтобы геометрию модели отобразить в каком-либо целевом пространстве, необходимо произвести отображение локального пространства модели в целевое пространство. Именно таким отображением является ModelMatrix.

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

    Projectionmatrix - матрица отображения видового пространства в пространство проекции вида. Фактически это отображение выполняет проецирование некоторого участка видового пространства на плоскость. Именно отображенная таким образом геометрия становится видимой при презентации кадра.

    Каждый объект сцены находится и анимируется внутри своего локального пространства. Чтобы объект на сцене встал в положенное ему место, его геометрия отображается из своего локального пространства в глобальное пространство сцены с помощью своей ModelMatrix.
    Чтобы сцена стала видна для камеры, всю ее геометрию требуется отобразить в пространство вида камеры с помощью матрицы ViewMatrix.
    Чтобы часть пространства вида камеры попала в область презентации кадра, всю геометрию видового пространства нужно отобразить в пространство проекции с помощью матрицы Projectionmatrix.
    При этом важно заметить, что матрицы Projectionmatrix и ViewMatrix уникальны для камеры, а вот матрицы ModelMatrix уникальны уже для каждого объекта отдельно.


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

    Учитывая что Projectionmatrix и ViewMatrix в процессе презентации кадра являются неизменными, затраты в 48 операций на каждую позицию выглядят как расточительство. Если до презентации выполнить отображение самого пространства вида в пространство проекции, т.е. перемножить ViewMatrix и Projectionmatrix в правильном порядке, то с помощью результирующей матрицы viewProjectionMatrix число операций на одну позицию можно снизить до 32.
    Если же произвести отображение самого локального пространства модели в пространство презентации через перемножение матриц viewProjectionMatrix и ModelMatrix в правильном порядке, то благодаря полученной таким образом MVPMatrix число операций на одну позицию снизится до изначальных 16.

    Таким образом, матрицы viewProjectionMatrix и MVPMatrix просто позволяют снизить трудоемкость презентации кадра. Однако, во время презентации может потребоваться определение положения геометрии в каком-либо промежуточном пространстве, поэтому вдобавок к MVPMatrix в шейдер принято отдавать и ModelMatrix, и матрицы камеры.
    У каждой из матриц свой смысл. И если MVPMatrix бесспорно нужна всегда, то любую другую матрицу в шейдер добавлять стоит только исходя из осмысленной необходимости. Регистры GPU для шейдера не резиновые и могут очень быстро забиться избыточными данными. К тому же, чем меньше на каждом кадре в шейдер передается данных, тем быстрее выполняется презентация кадра.
    Ответ написан
    Комментировать