1. Друзья, в это тяжёлое и непонятное для всех нас время мы просим вас воздержаться от любых упоминаний политики на форуме, - этим ситуации не поможешь, а только возникнут ненужные ссоры и обиды. Это касается также шуток и юмора на тему конфликта. Пусть войны будут только виртуальными, а политики решают разногласия дипломатическим путём. С уважением, администрация Old-Games.RU.

    Скрыть объявление
  2. Пожалуйста, внимательно прочитайте правила раздела.
  3. Если Вы видите это сообщение, значит, вы ещё не зарегистрировались на нашем форуме.

    Зарегистрируйтесь, если вы хотите принять участие в обсуждениях. Перед регистрацией примите к сведению:
    1. Не регистрируйтесь с никами типа asdfdadhgd, 354621 и тому подобными, не несущими смысловой нагрузки (ник должен быть читаемым!): такие пользователи будут сразу заблокированы!
    2. Не регистрируйте больше одной учётной записи. Если у вас возникли проблемы при регистрации, то вы можете воспользоваться формой обратной связи внизу страницы.
    3. Регистрируйтесь с реально существующими E-mail адресами, иначе вы не сможете завершить регистрацию.
    4. Обязательно ознакомьтесь с правилами поведения на нашем форуме, чтобы избежать дальнейших конфликтов и непонимания.
    С уважением, администрация форума Old-Games.RU
    Скрыть объявление

Эксперимент 200к спрайтов в кадре

Тема в разделе "Мастерская", создана пользователем Strategus, 16 фев 2026.

Метки:
  1. Strategus

    Strategus

    Регистрация:
    1 мар 2024
    Сообщения:
    173
    В общем то, тут в другой теме у нас возник спор. Чтобы не засорять тему - делаю отделенную. Современные монстры из мира видиокарт не рассматриваются. Тестим на системе 15 летней давности.

    Сама мысль,что в 3д, можно рисовать миллионы треугольников в кадре, а 2д играх, жалкие несколько тысяч (иногда десятков тясяч) спрайтов, давно мне не давала покоя. Уже много лет назад,я убедился что SDL2 и SFML, штатным рендером тянут, порядка 8-10 тысяч, спрайтов в кадре. Но там нет оптимизации совсем.
    Первый этап оптимизации, это не менять шейдер и текстуру, для спрайтов (вся графика лежит в едином атласе). Ну это разделение есть в любых движках.
    Следующий этап, это батчинг - суть в том, чтобы пачкой отправить рисоваться наши спрайты. Есть тоже везде, и по сути на этом как правило оптимизации для 2д и заканчиваются. У меня это порядка 30к спрайтов, чуть не дотянул, но там во всех движках, что пробовал, очень много лишних данных по шине перегоняется в видеопамять. А я всё таки в рамках изометрии смотрю, там вращение например вообще не надо.

    Пришлось повозится с нюансами opengl, я теории то знал много, а практики серьёзной не было )) Тест пока не закончен, но уже вселяет уверенность, что 200к легко можно нарисовать. В реальной игре по всей видимости будет слабым местом CPU. На данный момент, получилось что от 390к до 420к спрайтов, при 60 фпс тянет моя древняя AMD HD 5750 (обрабатывая свыше 80млн пикселей в кадре). По заявлениям гугла, она способна обработать около 180 млн, ну это при минимальном шейдере.

    Код:
    #include <iostream>
    #include <GL/glew.h>
    #include <GLFW/glfw3.h>
    
    #include "lodepng.h"
    
    static const char* vertex_shader_text = R"(
    #version 330 core
    layout(location = 0) in vec3 aPos;
    layout(location=1) in vec2 instance_position;
    
    const vec2 Pos[4] = vec2[4](
        vec2(-1.0, 1.0),
        vec2(-1.0, -1.0),
        vec2(1.0, -1.0),
        vec2(1.0, 1.0)
    );
    
    out vec2 texCoord;
    
    void main()
    {
        vec2 pos = Pos[gl_VertexID] * vec2(0.01, 0.0125);
        texCoord = Pos[gl_VertexID] * vec2(0.01, 0.01) + instance_position;
        gl_Position = vec4(pos + instance_position, 0.0, 1.0);
    }
    )";
    
    static const char* fragment_shader_text = R"(
    #version 330 core
    
    uniform sampler2D myTexture; // Объявляем текстурную переменную
    
    out vec4 FragColor;
    in vec2 texCoord;
    
    void main()
    {
        FragColor = vec4(texture(myTexture, texCoord));
    }
    
    )";
    
    // Функция для обработки ошибок
    void error_callback(int error, const char* description) {
        std::cerr << "Error: " << description << std::endl;
    }
    
    // Создаем окно с OpenGL контекстом
    GLFWwindow* create_window(int width, int height) {
        GLFWwindow* window;
    
        // Инициализируем GLFW
        if (!glfwInit()) {
            std::cerr << "Failed to initialize GLFW" << std::endl;
            return nullptr;
        }
    
        glfwSetErrorCallback(error_callback);
    
        // Настраиваем версию OpenGL
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    
        // Создаем окно
        window = glfwCreateWindow(width, height, "OpenGL 3.3", nullptr, nullptr);
        if (!window) {
            std::cerr << "Failed to create GLFW window" << std::endl;
            glfwTerminate();
            return nullptr;
        }
    
        glfwMakeContextCurrent(window);
        return window;
    }
    #define SIZE 16
    #define XX 100
    #define YY 80
    #define TEST 52
    // Основная функция
    int main() {
        GLFWwindow* window = create_window(SIZE*XX, SIZE*YY);
        if (!window) return -1;
    
        // Инициализация GLEW
        glewExperimental = GL_TRUE;
        GLenum err = glewInit();
        if (err != GLEW_OK) {
            std::cerr << "Failed to initialize GLEW: " << glewGetErrorString(err) << std::endl;
            return -1;
        }
    
         // Создаем и настраиваем VBO и VAO
        GLuint vertex_array_object;    // VAO
    //    GLuint vertex_buffer_coord;    // VBO
        GLuint vertex_buffer_position; // VBO
    
    /*
        GLfloat vertex_coord_data[] = {       // Triangle coords
            -0.3, -0.3, 0.0,
             0.3, -0.3, 0.0,
             0.0,  0.3, 0.0
        };
    */
        size_t t = 0;
        GLfloat *vertex_position_data = new GLfloat[TEST * XX * YY * 2];
        for(uint32_t test = 0 ; test < TEST; test++)
            for(uint32_t y = 0; y < YY ; y++)
                for(uint32_t x = 0; x < XX ; x++)
                {
                   vertex_position_data[t++] = 1.f - 0.02f * x;
                   vertex_position_data[t++] = 1.f - 0.025f * y;
                }
        std::cout << "!!!!!!" << t << " = " <<  TEST * XX * YY * 2 << std::endl;
    
        glGenVertexArrays(1, &vertex_array_object);
        glBindVertexArray(vertex_array_object);
    /*
    // Координаты модели (aPos)
    glGenBuffers(1, &vertex_buffer_coord);
    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_coord);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_coord_data), vertex_coord_data, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    */
    
        // Позиции инстансов (instance_position)
        glGenBuffers(1, &vertex_buffer_position);
        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_position);
        glBufferData(GL_ARRAY_BUFFER, TEST * XX * YY * 2  * sizeof(GLfloat), vertex_position_data, GL_STATIC_DRAW);
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (void*)0);
        glEnableVertexAttribArray(1);
    
        glVertexAttribDivisor(1, 1);
    
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindVertexArray(0);
    
        // ==================== Шейдеры =====================================
        const GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertex_shader, 1, &vertex_shader_text, NULL);
        glCompileShader(vertex_shader);
    
        GLint success;
        GLchar infoLog[512];
    
        // Для каждого шейдера (vertex и fragment):
        glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
        if (!success) {
            glGetShaderInfoLog(vertex_shader, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::COMPILATION_FAILED\n" << infoLog << std::endl;
        }
    
        const GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragment_shader, 1, &fragment_shader_text, NULL);
        glCompileShader(fragment_shader);
        // Для каждого шейдера (vertex и fragment):
        glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
        if (!success) {
            glGetShaderInfoLog(fragment_shader, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::COMPILATION_FAILED\n" << infoLog << std::endl;
        }
    
            const GLuint program = glCreateProgram();
            glAttachShader(program, vertex_shader);
            glAttachShader(program, fragment_shader);
            glLinkProgram(program);
    
        // Удалите неиспользуемые шейдеры после связывания
        glDeleteShader(vertex_shader);
        glDeleteShader(fragment_shader);
    
        glGetProgramiv(program, GL_LINK_STATUS, &success);
        if (!success) {
            glGetProgramInfoLog(program, 512, NULL, infoLog);
            std::cout << "SHADER LINKING ERROR: " << infoLog << std::endl;
        }
    
        // ===================================================================================
    
        // Создаем текстурный объект
        GLuint textureID;
        glGenTextures(1, &textureID);
        glActiveTexture(GL_TEXTURE0); // Выбор текстурного канала 0
        glBindTexture(GL_TEXTURE_2D, textureID);
    
        // Устанавливаем параметры фильтрации
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    
        // Загружаем данные текстуры (например, однопиксельная текстура)
        std::vector<unsigned char> image;
        unsigned width, height;
        unsigned error = lodepng::decode(image, width, height, "atlas.png");
            if(error)
                std::cout << " No file atlas.png ... " << std::endl;
    
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, &image[0]);
        GLint textureLocation = glGetUniformLocation(program, "myTexture");
        glUniform1i(textureLocation, 0); // Указываем, что текстура будет использована с текстурным юнитом 0
        // ============================================================================================================
        // Устанавливаем цвет фона
        glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
        // Порт просмотра
        glViewport(0, 0, XX*SIZE, YY*SIZE);
        // Подключаем наш единственный шейдер
        glUseProgram(program);
    
        GLuint queries[2];
        glGenQueries(2, queries);  // Один для вершин, второй для фрагментов
    
        //while (!glfwWindowShouldClose(window))
        //glDisable(GL_MULTISAMPLE);
        glEnable(GL_BLEND);
        //glDisable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
        int frameCounter = 0;
        const int SAMPLES = 20;
    
            // Разогреваем видеокарту, если показатели не стабильны - увеличить
            for (int i = 0; i < 250; i++)
            {
                glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
                    glBindVertexArray(vertex_array_object);
                        glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, TEST * XX * YY);
                    glBindVertexArray(0);
                glfwSwapBuffers(window);
            }
    
            glBeginQuery(GL_PRIMITIVES_GENERATED, queries[0]);
            double lastTime = glfwGetTime();
            for (int i = 0; i < SAMPLES; i++)
            {
                frameCounter++;
                glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
                    glBindVertexArray(vertex_array_object);
                        glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, TEST * XX * YY);
                    glBindVertexArray(0);
    
                glfwSwapBuffers(window);
                glfwPollEvents();
            }
            double currentTime = glfwGetTime();
            double Fps = double(frameCounter) / (currentTime - lastTime);
            glEndQuery(GL_PRIMITIVES_GENERATED);
    
            std::cout << "FPS: " << std::fixed << Fps << std::endl;
    
            // УЗнаём сколько пикселей в кадре было обработано
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glBeginQuery(GL_SAMPLES_PASSED, queries[1]);
                glBindVertexArray(vertex_array_object);
                    glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, TEST * XX * YY);
                glBindVertexArray(0);
            glEndQuery(GL_SAMPLES_PASSED);
    
            glfwSwapBuffers(window);
    
        GLuint processedVertices = 0;
        GLuint processedFragments = 0;
    
        glGetQueryObjectuiv(queries[0], GL_QUERY_RESULT, &processedVertices);
        glGetQueryObjectuiv(queries[1], GL_QUERY_RESULT, &processedFragments);
    
        // Вывод результатов
        printf("Обработанные вершины: %d\n", (processedVertices / SAMPLES) / 2 );
        printf("Обработанные фрагменты: %d\n", processedFragments);
        // Освобождаем ресурсы
        glDeleteBuffers(1, &vertex_buffer_position);
        //glDeleteBuffers(1, &vertex_buffer_coord);
    
        glDeleteVertexArrays(1, &vertex_array_object);
        glDeleteProgram(program);
    
        glfwDestroyWindow(window);
        glfwTerminate();
        delete [] vertex_position_data;
        return 0;
    }
    
     
    GreenEyesMan нравится это.
  2. Strategus

    Strategus

    Регистрация:
    1 мар 2024
    Сообщения:
    173
    Тест пока синтетический, координаты для атласа генерируются в шейдере, что быстрее, чем читать их из буфера или текстуры. Но отличие не сможет убить производительность сильно. Ну и важный и интересный момент. Количество обработанных пикселей, меньше, чем должно быть. Судя по всему то что не попадает на экран - не рисуется.Так что не понятно, насколько надо заморачиваться с оптимизациями, чтобы лишнее не пихать в видеокарту. Где балнас пролегает?
     
  3. GreenEyesMan

    GreenEyesMan

    Регистрация:
    25 авг 2017
    Сообщения:
    3.148
    Хм, забавно. Но кому может потребоваться столько спрайтов в кадре? Майклу Бею? :)

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

    Тут же прикол в том, что в спрайтах может быть больше информации, чем в кусках полигонов. Ну и прочая техническая дичь.
     
  4. realavt Суету навести охота

    realavt

    Регистрация:
    11 окт 2006
    Сообщения:
    9.286
    Вспомнилось консольное, на железе 1999 года выпуска:
     
  5. Strategus

    Strategus

    Регистрация:
    1 мар 2024
    Сообщения:
    173
    Ну, можно сделать разрушаемый ландшафт, или здания составлять из мелких кусочков. Добавить погоду, там эффекты какие то... Просто они перестают быть сдерживающим фактором. Спрайт сложнее треугольника, примерно в два раза, ну это на стороне видеокарты. Самая тормозная штука, пока что - это разные сортировки, что и когда рисовать, что не рисовать. Есть вариант и это всё запихать на видеокарту, но надо ли?

    Я три вечера мучался, пытаясь подружить Текстурный буфер и текстуру. Везде всё по разному расписано (вернее одинаково, но с разными нюансами), ИИ так же чуть по разному отвечает, или не договаривает, примеры кода, работают по отдельности, а всё вместе перестаёт. Там сам чёрт ногу сломает, как надо правильно , эти все внутренние состояния opengl учесть )) Вроде сделал, выкинул часть кода, которая нужна, а может и нет. И всё равно работает ))

    Надо найти наборчик подходящих спрайтов и устроить тест, по генерации гор, по типу как в 3д делают.Чтобы горя динамически менялись...
     
  6. GreenEyesMan

    GreenEyesMan

    Регистрация:
    25 авг 2017
    Сообщения:
    3.148
    @Strategus, Существует игрушка одна - Cortex Command. Это двухмерная стратегия с нетривиальным управлением и абсолютно полной, попиксельной, разрушаемостью всего и вся. В игре можно тупо взять большую транспортную ракету, забить ее под завязку взрывчаткой и топливом, запустить в сторону врага и отключить посадочные двигатели. Светопредставление будет шикарное.

    Но за всю эту разрушаемость приходиться платить очень... нет не так: ОЧЕНЬ ДОЛГОЙ загрузкой. За время загрузки можно сходить поставить чайник, приготовить ужин, принять ванну, посадить дерево и вырастить ребенка. И это тупо ради развлекалочки с разрушаемостью (по сути в игре мало что интересного). И конечно игра начинает жутко тормозить уже через пятнадцать минут жестких зарубов - ведь пикселей на экран становиться много. Порой слишком много.
    И вот стоит ли оно того? Вопрос открытый.

    Я недавно посрался (я то не хотел, но люди выбешивают как вид) с каким-то полоумным, который наехал на меня за то, что я за оптимизацию всего и вся топлю. Дескать ему под кайф что простецкие проги жрут столько ресурсов, что проще удалить с компа все и спрятать его на полку, чем каждый месяц новую железку докупать. И вот ради чего это? Ради возможности видеть буйство спрайтов? А смысл? Человеческий глаз все равно эту мешанину не разберет.
     
    compart и Strategus нравится это.
  7. Mov AX 0xDEAD

    Mov AX 0xDEAD

    Регистрация:
    24 апр 2023
    Сообщения:
    485
    Классический "спрайт" - текстура, копируемая (синхронно) из основной памяти в видеопамять (основа 8/16-битных консолей или GDI/DirectDraw) . Opengl, натягивание текстур на вершины и прочие шейдеры мягко говоря "это другое" (асинхронный мир). И таки в Vista убили аппаратный DirectDraw вынудив разрабов перейти на Direct3D даже для вывода единственного квадрата малевича.
     
    Последнее редактирование: 21 фев 2026
    GreenEyesMan нравится это.
  8. Strategus

    Strategus

    Регистрация:
    1 мар 2024
    Сообщения:
    173
    Я вот жалею, что шейдеры появились, после массового перехода на 3д. Вот представте, если бы вначале шейдеры придумали, а потом внедряли 3д. Да были бы только пиксельные изначально, но как раз бы все алгоритмы графики из старых игр туда перекочевали, столько бы было придумано разных трюков и спецэффектов...

    Это не Виста виновата, а развитие 3д ускорителей... Даже сейчас можно создать совместимый контекст опенгл, и задействовать старый функционал, там будет и вывод пикселей, и выровненный прямоугольник (по сути спрайт). Но всё это будет реализовано силами драйвера. А уж что там будет под капотом - никто не скажет, скорее всего, везде по разному, но чаще всего новые технологии прикинутся старыми ))

    Никто не мешает, сделать всё в пикселях. Выровнять координаты, строго по пикселям. Отказаться от вращений, матриц и прочего. Как я тут выше делал, даже по сути нет вершин, они генерируются в процесс. Если хочется вообще чтобы было, ну как раньше - ну честно всё в пикселях и гарантировано. Делаешь микро квадраты(2 треугольника) 1х1 пикселя, а из них составляешь уже составляешь Спрайты. Но это уже не потянет на всех видеокартах - мощности не везде хватит ))

    Тоже бесит, что если сделать любую игру из девяностых, на самом продвинутом и раскрученном движке. То она потребует в сотни раз больше мощности и ресурсов... а то и в тысячи...
     
    compart и RunnerFx нравится это.
  9. Лорд Лотар Мессир ёж

    Лорд Лотар

    Регистрация:
    12 май 2008
    Сообщения:
    6.004
    Моддерам дума же. Я вот хотел бы например.
     
    Strategus нравится это.
  10. GreenEyesMan

    GreenEyesMan

    Регистрация:
    25 авг 2017
    Сообщения:
    3.148
    @Лорд Лотар, Зачем Вам так много, сир? Еще больше вражин на квадратный метр уровня. :)

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

    А уровень с зомби на Исландии в Blade of Agony - жуткое слайдшоу.
     
  11. Grue13 Ocelote.12

    Grue13

    Регистрация:
    26 апр 2006
    Сообщения:
    10.786
    Уважаемый @Strategus, а у вас есть что сказать по поводу этой игры:

    Ultimate Epic Battle Simulator 2



    Там вообще миллионы моделек (если на современных видеокартах) одновременно сражаются. Со спрайтами возможно было бы ещё легче, но я не знаю точно.
     
  12. Strategus

    Strategus

    Регистрация:
    1 мар 2024
    Сообщения:
    173
    Ну, в 3д играх, на большой дистанции подменяют же модель билбоардом, тот же спрайт, только всегда повёрнутый к камере.

    Мне кажется для последних 4-5 лет выпуска, вывести пару миллионов спрайтов, даже без особых оптимизаций не проблема...

    Главное то в чём, миллионы в кадре на экране не увидеть. Можно сделать скрин и посчитать и их там окажется ну 10-20 тысяч максимум. А остальное просто точки вдалеке, или вовсе их не видно. Сомневаюсь, что там идёт какой то честный расчёт, кто кого ударил и на сколько ранил. Просто рядом с камерой, отыгрывают близко к честному, а дальше, для вона имитация бурной деятельности )) Просто миллион билбоардов, летающих по кругу, можно было отобразить лет 15-17 назад, даже средствами Иррлихта (с его совсем древним opengl или девятый директом).
    Скажем так, если данные не надо формировать центральным процессором и передавать на видеокарту каждый кадр. То Видюха их может рисовать, в страшно огромных количествах, спрашиваем у гугла: Каков теоретический Pixel Fillrate и Triangle Setup Rate у видеокарты [Модель, моя супер видеокарта]? И гугл выдаст возможности, и там будут страшные цифры, но их надо разделить на 60, если хотим понять, сколько кадре выйдет. Разумеется всё это или будет статично, или будет двигаться по какой то формуле, ну или по данным, лежащим тоже на стороне видеокарты. Вычислительные шейдеры. Но это всё не для старых видеокарт. Моя тестовая вроде как и поддерживает, но изначально у неё такого не было, а значит драйвер это будет эмулировать и оно не будет быстрым, как на новых видюшках.
     
  1. На этом сайте используются файлы cookie, чтобы персонализировать содержимое, хранить Ваши предпочтения и держать Вас авторизованным в системе, если Вы зарегистрировались.
    Продолжая пользоваться данным сайтом, Вы соглашаетесь на использование нами Ваших файлов cookie.
    Скрыть объявление