浏览代码

Добавил SpriteBatch

1vanK 3 年之前
父节点
当前提交
1626b56f35

+ 14 - 0
Source/Samples/56_SpriteBatch/CMakeLists.txt

@@ -0,0 +1,14 @@
+# Copyright (c) 2008-2022 the Urho3D project
+# License: MIT
+
+# Define target name
+set (TARGET_NAME 56_SpriteBatch)
+
+# Define source files
+define_source_files (EXTRA_H_FILES ${COMMON_SAMPLE_H_FILES})
+
+# Setup target with resource copying
+setup_main_executable ()
+
+# Setup test cases
+setup_test ()

+ 276 - 0
Source/Samples/56_SpriteBatch/SpriteBatch.cpp

@@ -0,0 +1,276 @@
+#include <Urho3D/Urho3DAll.h>
+
+#include <Urho3D/Graphics/SpriteBatch.h>
+
+class Game : public Application
+{
+    URHO3D_OBJECT(Game, Application);
+
+public:
+    SharedPtr<Scene> scene_;
+    SharedPtr<Node> cameraNode_;
+    float yaw_ = 0.0f;
+    float pitch_ = 0.0f;
+    SharedPtr<SpriteBatch> worldSpaceSpriteBatch_;
+    SharedPtr<SpriteBatch> screenSpaceSpriteBatch_;
+    SharedPtr<SpriteBatch> virtualSpriteBatch_;
+    float fpsTimeCounter_ = 0.0f;
+    i32 fpsFrameCounter_ = 0;
+    i32 fpsValue_ = 0;
+    i32 currentTest_ = 1;
+    float angle_ = 0.0f;
+    float scale_ = 0.0f;
+
+    Game(Context* context) : Application(context)
+    {
+    }
+
+    void Setup()
+    {
+        engineParameters_[EP_WINDOW_RESIZABLE] = true;
+        engineParameters_[EP_FULL_SCREEN] = false;
+        engineParameters_[EP_WINDOW_WIDTH] = 1200;
+        engineParameters_[EP_WINDOW_HEIGHT] = 900;
+        engineParameters_[EP_FRAME_LIMITER] = false;
+    }
+
+    void Start()
+    {
+        //GetSubsystem<Engine>()->SetMaxFps(10);
+
+        CreateScene();
+        SetupViewport();
+        SubscribeToEvents();
+
+        XMLFile* xmlFile = GetSubsystem<ResourceCache>()->GetResource<XMLFile>("UI/DefaultStyle.xml");
+        DebugHud* debugHud = engine_->CreateDebugHud();
+        debugHud->SetDefaultStyle(xmlFile);
+
+        screenSpaceSpriteBatch_ = new SpriteBatch(context_);
+        
+        worldSpaceSpriteBatch_ = new SpriteBatch(context_);
+        worldSpaceSpriteBatch_->camera_ = cameraNode_->GetComponent<Camera>();
+        worldSpaceSpriteBatch_->compareMode_ = CMP_LESSEQUAL;
+
+        virtualSpriteBatch_ = new SpriteBatch(context_);
+        virtualSpriteBatch_->virtualScreenSize_ = IntVector2(700, 600);
+    }
+
+    void SetupViewport()
+    {
+        SharedPtr<Viewport> viewport(new Viewport(context_, scene_, cameraNode_->GetComponent<Camera>()));
+        GetSubsystem<Renderer>()->SetViewport(0, viewport);
+    }
+
+    void CreateScene()
+    {
+        ResourceCache* cache = GetSubsystem<ResourceCache>();
+
+        scene_ = new Scene(context_);
+        scene_->CreateComponent<Octree>();
+
+        Node* planeNode = scene_->CreateChild("Plane");
+        planeNode->SetScale(Vector3(100.0f, 1.0f, 100.0f));
+        StaticModel* planeObject = planeNode->CreateComponent<StaticModel>();
+        planeObject->SetModel(cache->GetResource<Model>("Models/Plane.mdl"));
+        planeObject->SetMaterial(cache->GetResource<Material>("Materials/StoneTiled.xml"));
+
+        Node* lightNode = scene_->CreateChild("DirectionalLight");
+        lightNode->SetDirection(Vector3(0.6f, -1.0f, 0.8f));
+        Light* light = lightNode->CreateComponent<Light>();
+        light->SetColor(Color(0.5f, 0.5f, 0.5f));
+        light->SetLightType(LIGHT_DIRECTIONAL);
+        light->SetCastShadows(true);
+        light->SetShadowBias(BiasParameters(0.00025f, 0.5f));
+        light->SetShadowCascade(CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f));
+        //light->SetShadowIntensity(0.5f);
+
+        Node* zoneNode = scene_->CreateChild("Zone");
+        Zone* zone = zoneNode->CreateComponent<Zone>();
+        zone->SetBoundingBox(BoundingBox(-1000.0f, 1000.0f));
+        zone->SetAmbientColor(Color(0.5f, 0.5f, 0.5f));
+        zone->SetFogColor(Color(0.4f, 0.5f, 0.8f));
+        zone->SetFogStart(100.0f);
+        zone->SetFogEnd(300.0f);
+
+        constexpr i32 NUM_OBJECTS = 20;
+        for (i32 i = 0; i < NUM_OBJECTS; ++i)
+        {
+            Node* mushroomNode = scene_->CreateChild("Mushroom");
+            mushroomNode->SetPosition(Vector3(Random(90.0f) - 45.0f, 0.0f, Random(90.0f) - 45.0f));
+            mushroomNode->SetRotation(Quaternion(0.0f, Random(360.0f), 0.0f));
+            mushroomNode->SetScale(0.5f + Random(2.0f));
+            StaticModel* mushroomObject = mushroomNode->CreateComponent<StaticModel>();
+            mushroomObject->SetModel(cache->GetResource<Model>("Models/Mushroom.mdl"));
+            mushroomObject->SetMaterial(cache->GetResource<Material>("Materials/Mushroom.xml"));
+            mushroomObject->SetCastShadows(true);
+        }
+
+        cameraNode_ = scene_->CreateChild("Camera");
+        cameraNode_->CreateComponent<Camera>();
+        cameraNode_->SetPosition(Vector3(0.0f, 2.0f, -5.0f));
+    }
+
+    void MoveCamera(float timeStep)
+    {
+        constexpr float MOVE_SPEED = 20.0f;
+        constexpr float MOUSE_SENSITIVITY = 0.1f;
+
+        Input* input = GetSubsystem<Input>();
+
+        IntVector2 mouseMove = input->GetMouseMove();
+        yaw_ += MOUSE_SENSITIVITY * mouseMove.x_;
+        pitch_ += MOUSE_SENSITIVITY * mouseMove.y_;
+        pitch_ = Clamp(pitch_, -90.0f, 90.0f);
+        cameraNode_->SetRotation(Quaternion(pitch_, yaw_, 0.0f));
+
+        if (input->GetKeyDown(KEY_W))
+            cameraNode_->Translate(Vector3::FORWARD * MOVE_SPEED * timeStep);
+        if (input->GetKeyDown(KEY_S))
+            cameraNode_->Translate(Vector3::BACK * MOVE_SPEED * timeStep);
+        if (input->GetKeyDown(KEY_A))
+            cameraNode_->Translate(Vector3::LEFT * MOVE_SPEED * timeStep);
+        if (input->GetKeyDown(KEY_D))
+            cameraNode_->Translate(Vector3::RIGHT * MOVE_SPEED * timeStep);
+
+    }
+
+    void SubscribeToEvents()
+    {
+        SubscribeToEvent(E_UPDATE, URHO3D_HANDLER(Game, HandleUpdate));
+        SubscribeToEvent(E_ENDVIEWRENDER, URHO3D_HANDLER(Game, HandleEndViewRender));
+    }
+
+    void HandleUpdate(StringHash eventType, VariantMap& eventData)
+    {
+        using namespace Update;
+        float timeStep = eventData[P_TIMESTEP].GetFloat();
+
+        Input* input = GetSubsystem<Input>();
+        Time* time = GetSubsystem<Time>();
+
+        if (input->GetKeyDown(KEY_1))
+            currentTest_ = 1;
+        else if (input->GetKeyDown(KEY_2))
+            currentTest_ = 2;
+        else if (input->GetKeyDown(KEY_3))
+            currentTest_ = 3;
+
+        if (input->GetMouseButtonDown(MOUSEB_RIGHT))
+        {
+            input->SetMouseVisible(false);
+            MoveCamera(timeStep);
+        }
+        else
+        {
+            input->SetMouseVisible(true);
+        }
+
+        fpsTimeCounter_ += timeStep;
+        fpsFrameCounter_++;
+
+        if (fpsTimeCounter_ >= 1.0f)
+        {
+            fpsTimeCounter_ = 0.0f;
+            fpsValue_ = fpsFrameCounter_;
+            fpsFrameCounter_ = 0;
+        }
+
+        angle_ += timeStep * 100.0f;
+        angle_ = fmod(angle_, 360.0f);
+
+        scale_ = 1.0f + Sin(time->GetElapsedTime() * 200.0f) * 0.3f;
+    }
+
+    bool collide_ = true;
+
+    void HandleEndViewRender(StringHash eventType, VariantMap& eventData)
+    {
+        Input* input = GetSubsystem<Input>();
+        ResourceCache* cache = GetSubsystem<ResourceCache>();
+        Graphics* graphics = GetSubsystem<Graphics>();
+
+        Texture2D* ball = cache->GetResource<Texture2D>("Urho2D/Ball.png");
+        Texture2D* head = cache->GetResource<Texture2D>("Urho2D/imp/imp_head.png");
+        Font* font = cache->GetResource<Font>("Fonts/Anonymous Pro.ttf");
+
+        // Очистка экрана. Если сцена пустая, то можно просто задать цвет зоны
+        //GetSubsystem<Graphics>()->Clear(CLEAR_COLOR, Color::GREEN);
+
+        String str;
+
+        if (currentTest_ == 1)
+        {
+            screenSpaceSpriteBatch_->SetShapeColor(0xFFFF0000);
+            screenSpaceSpriteBatch_->DrawCircle(500.f, 200.f, 200.f);
+
+            Vector2 origin = Vector2(head->GetWidth() * 0.5f, head->GetHeight() * 0.5f);
+            screenSpaceSpriteBatch_->DrawSprite(head, Vector2(200.0f, 200.0f), nullptr, 0xFFFFFFFF, angle_, origin, Vector2(scale_, scale_));
+
+            str = "QqWЙйр";
+            screenSpaceSpriteBatch_->DrawString(str, font, 20.f, Vector2(4.0f, 60.f), 0xFF00FF00, 0.0f, Vector2::ZERO, Vector2::ONE, FlipModes::None);
+            screenSpaceSpriteBatch_->DrawString(str, font, 20.f, Vector2(104.0f, 60.f), 0xFF00FF00, 0.0f, Vector2::ZERO, Vector2::ONE, FlipModes::Horizontally);
+            screenSpaceSpriteBatch_->DrawString(str, font, 20.f, Vector2(4.0f, 100.f), 0xFF00FF00, 0.0f, Vector2::ZERO, Vector2::ONE, FlipModes::Vertically);
+            screenSpaceSpriteBatch_->DrawString(str, font, 20.f, Vector2(104.0f, 100.f), 0xFF00FF00, 0.0f, Vector2::ZERO, Vector2::ONE, FlipModes::Vertically | FlipModes::Both);
+
+            // screenSpaceSpriteBatch_->Flush(); не вызываем, так как еще текст будем выводить
+        }
+        else if (currentTest_ == 2)
+        {
+            worldSpaceSpriteBatch_->DrawSprite(head, Vector2(0.0f, 0.0f), nullptr, 0xFFFFFFFF, 0.0f, Vector2::ZERO, Vector2(0.01f, 0.01f));
+            worldSpaceSpriteBatch_->Flush();
+        }
+        else if (currentTest_ == 3)
+        {
+            virtualSpriteBatch_->SetShapeColor(0x90FF0000);
+            virtualSpriteBatch_->DrawAABBSolid(Vector2::ZERO, (Vector2)virtualSpriteBatch_->virtualScreenSize_);
+            virtualSpriteBatch_->DrawSprite(head, Vector2(200.0f, 200.0f));
+            // Преобразуем координаты мыши из оконных координат в виртуальные координаты
+            Vector2 virtualMousePos = virtualSpriteBatch_->GetVirtualPos(Vector2(GetSubsystem<Input>()->GetMousePosition()));
+            virtualSpriteBatch_->SetShapeColor(0xFFFFFFFF);
+            virtualSpriteBatch_->DrawArrow({100.0f, 100.f}, virtualMousePos, 10);
+            virtualSpriteBatch_->Flush();
+        }
+
+        // Выводим индекс текущего теста
+        str = "Текущий тест: " + String(currentTest_) + " (используйте 1, 2, 3 для переключения)";
+        screenSpaceSpriteBatch_->DrawString(str, font, 20.f, Vector2(4.0f, 0.f), 0xFFFFFFFF);
+
+        // Выводим описание текущего теста
+        if (currentTest_ == 1)
+        {
+            str = "Рисование в экранном пространстве";
+            screenSpaceSpriteBatch_->DrawString(str, font, 20.f, Vector2(4.0f, 24.f), 0xFFFFFFFF);
+        }
+        else if (currentTest_ == 2)
+        {
+            str = "Рисование в пространстве сцены";
+            screenSpaceSpriteBatch_->DrawString(str, font, 20.f, Vector2(4.0f, 24.f), 0xFFFFFFFF);
+        }
+        else if (currentTest_ == 3)
+        {
+            str = "Использование виртуальных координат";
+            screenSpaceSpriteBatch_->DrawString(str, font, 20.f, Vector2(4.0f, 24.f), 0xFFFFFFFF);
+
+            str = "Синий прямоугольник - границы виртуального экрана (700x600 виртуальных пикселей)";
+            screenSpaceSpriteBatch_->DrawString(str, font, 20.f, Vector2(4.0f, 48.f), 0xFFFFFFFF);
+
+            str = "Меняйте размеры окна, чтобы увидеть, как виртуальный экран вписывается в окно";
+            screenSpaceSpriteBatch_->DrawString(str, font, 20.f, Vector2(4.0f, 72.f), 0xFFFFFFFF);
+        }
+
+        // Выводим подсказку про ПКМ
+        str = "Зажмите ПКМ для перемещения по сцене";
+        Vector2 pos{graphics->GetWidth() - 550.f, graphics->GetHeight() - 36.f}; // TODO: Добавить MeasureString
+        screenSpaceSpriteBatch_->DrawString(str, font, 20.f, pos, 0xFFFFFFFF);
+
+        // Выводим FPS
+        str = "FPS: " + String(fpsValue_);
+        pos = {10.f, graphics->GetHeight() - 56.f};
+        screenSpaceSpriteBatch_->DrawString(str, font, 40.f, pos, 0xFF0000FF);
+
+        screenSpaceSpriteBatch_->Flush();
+    }
+};
+
+URHO3D_DEFINE_APPLICATION_MAIN(Game)

+ 2 - 0
Source/Samples/99_Benchmark/AppStateManager.cpp

@@ -6,6 +6,7 @@
 #include "AppState_Benchmark01.h"
 #include "AppState_Benchmark02.h"
 #include "AppState_Benchmark03.h"
+#include "AppState_Benchmark04.h"
 #include "AppState_MainScreen.h"
 #include "AppState_ResultScreen.h"
 
@@ -21,6 +22,7 @@ AppStateManager::AppStateManager(Context* context)
     appStates_.Insert({APPSTATEID_BENCHMARK01, MakeShared<AppState_Benchmark01>(context_)});
     appStates_.Insert({APPSTATEID_BENCHMARK02, MakeShared<AppState_Benchmark02>(context_)});
     appStates_.Insert({APPSTATEID_BENCHMARK03, MakeShared<AppState_Benchmark03>(context_)});
+    appStates_.Insert({APPSTATEID_BENCHMARK04, MakeShared<AppState_Benchmark04>(context_)});
 }
 
 void AppStateManager::Apply()

+ 2 - 1
Source/Samples/99_Benchmark/AppStateManager.h

@@ -7,7 +7,7 @@
 
 #include <Urho3D/Container/HashMap.h>
 
-using AppStateId = unsigned;
+using AppStateId = u32;
 
 inline constexpr AppStateId APPSTATEID_NULL = 0;
 inline constexpr AppStateId APPSTATEID_MAINSCREEN = 1;
@@ -15,6 +15,7 @@ inline constexpr AppStateId APPSTATEID_RESULTSCREEN = 2;
 inline constexpr AppStateId APPSTATEID_BENCHMARK01 = 3;
 inline constexpr AppStateId APPSTATEID_BENCHMARK02 = 4;
 inline constexpr AppStateId APPSTATEID_BENCHMARK03 = 5;
+inline constexpr AppStateId APPSTATEID_BENCHMARK04 = 6;
 
 class AppStateManager : public U3D::Object
 {

+ 1 - 1
Source/Samples/99_Benchmark/AppState_Base.h

@@ -17,7 +17,7 @@ public:
     URHO3D_OBJECT(AppState_Base, Object);
 
 protected:
-    U3D::String name_;
+    U3D::String name_ = "Название бенчмарка";
 
     U3D::SharedPtr<U3D::Scene> scene_;
     void LoadSceneXml(const U3D::String& path);

+ 1 - 0
Source/Samples/99_Benchmark/AppState_Benchmark01.cpp

@@ -24,6 +24,7 @@ void AppState_Benchmark01::OnEnter()
 
 void AppState_Benchmark01::OnLeave()
 {
+    UnsubscribeFromAllEvents();
     DestroyViewport();
     scene_ = nullptr;
 }

+ 1 - 0
Source/Samples/99_Benchmark/AppState_Benchmark02.cpp

@@ -97,6 +97,7 @@ void AppState_Benchmark02::OnEnter()
 
 void AppState_Benchmark02::OnLeave()
 {
+    UnsubscribeFromAllEvents();
     DestroyViewport();
     scene_ = nullptr;
 }

+ 1 - 0
Source/Samples/99_Benchmark/AppState_Benchmark03.cpp

@@ -97,6 +97,7 @@ void AppState_Benchmark03::OnEnter()
 
 void AppState_Benchmark03::OnLeave()
 {
+    UnsubscribeFromAllEvents();
     DestroyViewport();
     scene_ = nullptr;
 }

+ 85 - 0
Source/Samples/99_Benchmark/AppState_Benchmark04.cpp

@@ -0,0 +1,85 @@
+// Copyright (c) 2008-2022 the Urho3D project
+// License: MIT
+
+#include "AppState_Benchmark04.h"
+#include "AppStateManager.h"
+
+#include <Urho3D/Core/Timer.h>
+#include <Urho3D/Graphics/GraphicsEvents.h>
+#include <Urho3D/GraphicsAPI/Texture2D.h>
+#include <Urho3D/Input/Input.h>
+#include <Urho3D/Resource/ResourceCache.h>
+
+#include <Urho3D/DebugNew.h>
+
+using namespace Urho3D;
+
+void AppState_Benchmark04::OnEnter()
+{
+    assert(!scene_);
+
+    // Сцена и вьюпорт не нужны
+
+    GetSubsystem<Input>()->SetMouseVisible(false);
+    SubscribeToEvent(E_ENDALLVIEWSRENDER, URHO3D_HANDLER(AppState_Benchmark04, HandleEndAllViewsRender));
+    fpsCounter_.Clear();
+    spriteBatch_ = new SpriteBatch(context_);
+}
+
+void AppState_Benchmark04::OnLeave()
+{
+    UnsubscribeFromAllEvents();
+    spriteBatch_ = nullptr;
+}
+
+void AppState_Benchmark04::HandleEndAllViewsRender(StringHash eventType, VariantMap& eventData)
+{
+    float timeStep = GetSubsystem<Time>()->GetTimeStep();
+
+    fpsCounter_.Update(timeStep);
+    UpdateCurrentFpsElement();
+
+    if (GetSubsystem<Input>()->GetKeyDown(KEY_ESCAPE))
+    {
+        GetSubsystem<AppStateManager>()->SetRequiredAppStateId(APPSTATEID_MAINSCREEN);
+        return;
+    }
+
+    if (fpsCounter_.GetTotalTime() >= 25.f)
+    {
+        GetSubsystem<AppStateManager>()->SetRequiredAppStateId(APPSTATEID_RESULTSCREEN);
+        return;
+    }
+
+    angle_ += timeStep * 100.0f;
+    angle_ = fmod(angle_, 360.0f);
+
+    scale_ += timeStep;
+
+    Graphics* graphics = GetSubsystem<Graphics>();
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    Texture2D* ball = GetSubsystem<ResourceCache>()->GetResource<Texture2D>("Urho2D/Ball.png");
+    Texture2D* head = cache->GetResource<Texture2D>("Urho2D/imp/imp_head.png");
+
+    GetSubsystem<Graphics>()->Clear(CLEAR_COLOR, Color::GREEN);
+
+    for (int i = 0; i < 20000; i++)
+    {
+        spriteBatch_->DrawSprite(ball,
+            Vector2(Random(0.0f, (float)(graphics->GetWidth() - ball->GetWidth())), Random(0.0f, (float)graphics->GetHeight() - ball->GetHeight())), nullptr, 0xFFFFFFFF);
+    }
+
+    spriteBatch_->DrawSprite(head, Vector2(200.0f, 200.0f), nullptr, 0xFFFFFFFF, 0.0f, Vector2::ZERO, Vector2::ONE, FlipModes::Both);
+
+    float scale = cos(scale_) + 1.0f; // cos возвращает значения в диапазоне [-1, 1], значит scale будет в диапазоне [0, 2].
+    Vector2 origin = Vector2(head->GetWidth() * 0.5f, head->GetHeight() * 0.5f);
+    spriteBatch_->DrawSprite(head, Vector2(400.0f, 300.0f), nullptr, 0xFFFFFFFF, angle_, origin, Vector2(scale, scale));
+
+    spriteBatch_->DrawString("Отзеркаленный текст", cache->GetResource<Font>("Fonts/Anonymous Pro.ttf"), 40.0f,
+        Vector2(250.0f, 200.0f), 0xFF0000FF, 0.0f, Vector2::ZERO, Vector2::ONE, FlipModes::Both);
+
+    spriteBatch_->DrawString("Некий текст", cache->GetResource<Font>("Fonts/Anonymous Pro.ttf"), 40.0f,
+        Vector2(400.0f, 300.0f), 0xFFFF0000, angle_, Vector2::ZERO, Vector2(scale, scale));
+
+    spriteBatch_->Flush();
+}

+ 31 - 0
Source/Samples/99_Benchmark/AppState_Benchmark04.h

@@ -0,0 +1,31 @@
+// Copyright (c) 2008-2022 the Urho3D project
+// License: MIT
+
+#pragma once
+
+#include "AppState_Base.h"
+
+#include <Urho3D/Graphics/SpriteBatch.h>
+
+// Бенчмарк для SpriteBatch
+class AppState_Benchmark04 : public AppState_Base
+{
+public:
+    URHO3D_OBJECT(AppState_Benchmark04, AppState_Base);
+
+public:
+    AppState_Benchmark04(U3D::Context* context)
+        : AppState_Base(context)
+    {
+        name_ = "SpriteBatch";
+    }
+
+    void OnEnter() override;
+    void OnLeave() override;
+
+    Urho3D::SharedPtr<Urho3D::SpriteBatch> spriteBatch_;
+    float angle_ = 0.f;
+    float scale_ = 0.f;
+
+    void HandleEndAllViewsRender(U3D::StringHash eventType, U3D::VariantMap& eventData);
+};

+ 4 - 0
Source/Samples/99_Benchmark/AppState_MainScreen.cpp

@@ -20,6 +20,7 @@ static const String MAIN_SCREEN_WINDOW_STR = "Main Screen Window";
 static const String BENCHMARK_01_STR = "Benchmark 01";
 static const String BENCHMARK_02_STR = "Benchmark 02";
 static const String BENCHMARK_03_STR = "Benchmark 03";
+static const String BENCHMARK_04_STR = "Benchmark 04";
 
 void AppState_MainScreen::HandleButtonPressed(StringHash eventType, VariantMap& eventData)
 {
@@ -32,6 +33,8 @@ void AppState_MainScreen::HandleButtonPressed(StringHash eventType, VariantMap&
         appStateManager->SetRequiredAppStateId(APPSTATEID_BENCHMARK02);
     else if (pressedButton->GetName() == BENCHMARK_03_STR)
         appStateManager->SetRequiredAppStateId(APPSTATEID_BENCHMARK03);
+    else if (pressedButton->GetName() == BENCHMARK_04_STR)
+        appStateManager->SetRequiredAppStateId(APPSTATEID_BENCHMARK04);
 }
 
 void AppState_MainScreen::CreateButton(const String& name, const String& text, Window& parent)
@@ -67,6 +70,7 @@ void AppState_MainScreen::CreateGui()
     CreateButton(BENCHMARK_01_STR, appStateManager->GetName(APPSTATEID_BENCHMARK01), *window);
     CreateButton(BENCHMARK_02_STR, appStateManager->GetName(APPSTATEID_BENCHMARK02), *window);
     CreateButton(BENCHMARK_03_STR, appStateManager->GetName(APPSTATEID_BENCHMARK03), *window);
+    CreateButton(BENCHMARK_04_STR, appStateManager->GetName(APPSTATEID_BENCHMARK04), *window);
 }
 
 void AppState_MainScreen::DestroyGui()

+ 15 - 0
Source/Urho3D/AngelScript/Generated_Enums.cpp

@@ -74,6 +74,12 @@ static const u8 DrawableTypes_Zone = static_cast<u8>(DrawableTypes::Zone);
 static const u8 DrawableTypes_Geometry2D = static_cast<u8>(DrawableTypes::Geometry2D);
 static const u8 DrawableTypes_Any = static_cast<u8>(DrawableTypes::Any);
 
+// enum class FlipModes : u32 | File: ../Graphics/SpriteBatch.h
+static const u32 FlipModes_None = static_cast<u32>(FlipModes::None);
+static const u32 FlipModes_Horizontally = static_cast<u32>(FlipModes::Horizontally);
+static const u32 FlipModes_Vertically = static_cast<u32>(FlipModes::Vertically);
+static const u32 FlipModes_Both = static_cast<u32>(FlipModes::Both);
+
 // enum HatPosition : unsigned | File: ../Input/InputConstants.h
 static const unsigned HatPosition_HAT_CENTER = HAT_CENTER;
 static const unsigned HatPosition_HAT_UP = HAT_UP;
@@ -939,6 +945,15 @@ void ASRegisterGeneratedEnums(asIScriptEngine* engine)
     engine->RegisterEnumValue("FillMode", "FILL_WIREFRAME", FILL_WIREFRAME);
     engine->RegisterEnumValue("FillMode", "FILL_POINT", FILL_POINT);
 
+    // enum class FlipModes : u32 | File: ../Graphics/SpriteBatch.h
+    engine->RegisterTypedef("FlipModes", "uint");
+    engine->SetDefaultNamespace("FlipModes");
+    engine->RegisterGlobalProperty("const uint None", (void*)&FlipModes_None);
+    engine->RegisterGlobalProperty("const uint Horizontally", (void*)&FlipModes_Horizontally);
+    engine->RegisterGlobalProperty("const uint Vertically", (void*)&FlipModes_Vertically);
+    engine->RegisterGlobalProperty("const uint Both", (void*)&FlipModes_Both);
+    engine->SetDefaultNamespace("");
+
     // enum FocusMode | File: ../UI/UIElement.h
     engine->RegisterEnum("FocusMode");
     engine->RegisterEnumValue("FocusMode", "FM_NOTFOCUSABLE", FM_NOTFOCUSABLE);

+ 2 - 0
Source/Urho3D/AngelScript/Generated_Includes.h

@@ -70,6 +70,8 @@
 #include "../Graphics/RibbonTrail.h"
 #include "../Graphics/Skeleton.h"
 #include "../Graphics/Skybox.h"
+#include "../Graphics/SpriteBatch.h"
+#include "../Graphics/SpriteBatchBase.h"
 #include "../Graphics/StaticModel.h"
 #include "../Graphics/StaticModelGroup.h"
 #include "../Graphics/Tangent.h"

+ 142 - 0
Source/Urho3D/AngelScript/Generated_Members.h

@@ -12134,6 +12134,63 @@ template <class T> void RegisterMembers_ShaderPrecache(asIScriptEngine* engine,
     #endif
 }
 
+// class SpriteBatchBase | File: ../Graphics/SpriteBatchBase.h
+template <class T> void RegisterMembers_SpriteBatchBase(asIScriptEngine* engine, const char* className)
+{
+    RegisterMembers_Object<T>(engine, className);
+
+    // void SpriteBatchBase::Flush()
+    engine->RegisterObjectMethod(className, "void Flush()", AS_METHODPR(T, Flush, (), void), AS_CALL_THISCALL);
+
+    // Vector2 SpriteBatchBase::GetVirtualPos(const Vector2& realPos)
+    engine->RegisterObjectMethod(className, "Vector2 GetVirtualPos(const Vector2&in)", AS_METHODPR(T, GetVirtualPos, (const Vector2&), Vector2), AS_CALL_THISCALL);
+
+    // void SpriteBatchBase::SetShapeColor(u32 color)
+    engine->RegisterObjectMethod(className, "void SetShapeColor(uint)", AS_METHODPR(T, SetShapeColor, (u32), void), AS_CALL_THISCALL);
+
+    // void SpriteBatchBase::SetShapeColor(const Color& color)
+    engine->RegisterObjectMethod(className, "void SetShapeColor(const Color&in)", AS_METHODPR(T, SetShapeColor, (const Color&), void), AS_CALL_THISCALL);
+
+    // bool SpriteBatchBase::VirtualScreenUsed() const
+    engine->RegisterObjectMethod(className, "bool VirtualScreenUsed() const", AS_METHODPR(T, VirtualScreenUsed, () const, bool), AS_CALL_THISCALL);
+
+    // TVertex SpriteBatchBase::v0_
+    // Error: type "TVertex" can not automatically bind
+    // TVertex SpriteBatchBase::v1_
+    // Error: type "TVertex" can not automatically bind
+    // TVertex SpriteBatchBase::v2_
+    // Error: type "TVertex" can not automatically bind
+    // Texture2D* SpriteBatchBase::texture_
+    // Not registered because pointer
+    // ShaderVariation* SpriteBatchBase::vs_
+    // Not registered because pointer
+    // ShaderVariation* SpriteBatchBase::ps_
+    // Not registered because pointer
+    // QVertex SpriteBatchBase::v0_
+    // Error: type "QVertex" can not automatically bind
+    // QVertex SpriteBatchBase::v1_
+    // Error: type "QVertex" can not automatically bind
+    // QVertex SpriteBatchBase::v2_
+    // Error: type "QVertex" can not automatically bind
+    // QVertex SpriteBatchBase::v3_
+    // Error: type "QVertex" can not automatically bind
+    // Camera* SpriteBatchBase::camera_
+    // Not registered because pointer
+
+    // BlendMode SpriteBatchBase::blendMode_
+    engine->RegisterObjectProperty(className, "BlendMode blendMode", offsetof(T, blendMode_));
+
+    // CompareMode SpriteBatchBase::compareMode_
+    engine->RegisterObjectProperty(className, "CompareMode compareMode", offsetof(T, compareMode_));
+
+    // IntVector2 SpriteBatchBase::virtualScreenSize_
+    engine->RegisterObjectProperty(className, "IntVector2 virtualScreenSize", offsetof(T, virtualScreenSize_));
+
+    #ifdef REGISTER_MEMBERS_MANUAL_PART_SpriteBatchBase
+        REGISTER_MEMBERS_MANUAL_PART_SpriteBatchBase();
+    #endif
+}
+
 // class Time | File: ../Core/Timer.h
 template <class T> void RegisterMembers_Time(asIScriptEngine* engine, const char* className)
 {
@@ -14465,6 +14522,91 @@ template <class T> void RegisterMembers_Shader(asIScriptEngine* engine, const ch
     #endif
 }
 
+// class SpriteBatch | File: ../Graphics/SpriteBatch.h
+template <class T> void RegisterMembers_SpriteBatch(asIScriptEngine* engine, const char* className)
+{
+    RegisterMembers_SpriteBatchBase<T>(engine, className);
+
+    // void SpriteBatch::DrawSprite(Texture2D* texture, const Rect& destination, Rect* source = nullptr, u32 color = 0xFFFFFFFF, float rotation = 0.0f, const Vector2& origin = Vector2::ZERO, const Vector2& scale = Vector2::ONE, FlipModes flipModes = FlipModes::None)
+    // Error: type "Rect*" can not automatically bind
+    // void SpriteBatch::DrawSprite(Texture2D* texture, const Vector2& position, Rect* source = nullptr, u32 color = 0xFFFFFFFF, float rotation = 0.0f, const Vector2& origin = Vector2::ZERO, const Vector2& scale = Vector2::ONE, FlipModes flipModes = FlipModes::None)
+    // Error: type "Rect*" can not automatically bind
+
+    // void SpriteBatch::DrawAABBSolid(const Vector2& min, const Vector2& max)
+    engine->RegisterObjectMethod(className, "void DrawAABBSolid(const Vector2&in, const Vector2&in)", AS_METHODPR(T, DrawAABBSolid, (const Vector2&, const Vector2&), void), AS_CALL_THISCALL);
+
+    // void SpriteBatch::DrawAABoxBorder(float centerX, float centerY, float halfWidth, float halfHeight, float borderWidth)
+    engine->RegisterObjectMethod(className, "void DrawAABoxBorder(float, float, float, float, float)", AS_METHODPR(T, DrawAABoxBorder, (float, float, float, float, float), void), AS_CALL_THISCALL);
+
+    // void SpriteBatch::DrawAABoxSolid(const Vector2& centerPos, const Vector2& halfSize)
+    engine->RegisterObjectMethod(className, "void DrawAABoxSolid(const Vector2&in, const Vector2&in)", AS_METHODPR(T, DrawAABoxSolid, (const Vector2&, const Vector2&), void), AS_CALL_THISCALL);
+
+    // void SpriteBatch::DrawAABoxSolid(float centerX, float centerY, float halfWidth, float halfHeight)
+    engine->RegisterObjectMethod(className, "void DrawAABoxSolid(float, float, float, float)", AS_METHODPR(T, DrawAABoxSolid, (float, float, float, float), void), AS_CALL_THISCALL);
+
+    // void SpriteBatch::DrawArrow(const Vector2& start, const Vector2& end, float width)
+    engine->RegisterObjectMethod(className, "void DrawArrow(const Vector2&in, const Vector2&in, float)", AS_METHODPR(T, DrawArrow, (const Vector2&, const Vector2&, float), void), AS_CALL_THISCALL);
+
+    // void SpriteBatch::DrawCircle(const Vector2& centerPos, float radius)
+    engine->RegisterObjectMethod(className, "void DrawCircle(const Vector2&in, float)", AS_METHODPR(T, DrawCircle, (const Vector2&, float), void), AS_CALL_THISCALL);
+
+    // void SpriteBatch::DrawCircle(float centerX, float centerY, float radius)
+    engine->RegisterObjectMethod(className, "void DrawCircle(float, float, float)", AS_METHODPR(T, DrawCircle, (float, float, float), void), AS_CALL_THISCALL);
+
+    // void SpriteBatch::DrawLine(const Vector2& start, const Vector2& end, float width)
+    engine->RegisterObjectMethod(className, "void DrawLine(const Vector2&in, const Vector2&in, float)", AS_METHODPR(T, DrawLine, (const Vector2&, const Vector2&, float), void), AS_CALL_THISCALL);
+
+    // void SpriteBatch::DrawLine(float startX, float startY, float endX, float endY, float width)
+    engine->RegisterObjectMethod(className, "void DrawLine(float, float, float, float, float)", AS_METHODPR(T, DrawLine, (float, float, float, float, float), void), AS_CALL_THISCALL);
+
+    // void SpriteBatch::DrawString(const String& text, Font* font, float fontSize, const Vector2& position, u32 color = 0xFFFFFFFF, float rotation = 0.0f, const Vector2& origin = Vector2::ZERO, const Vector2& scale = Vector2::ONE, FlipModes flipModes = FlipModes::None)
+    engine->RegisterObjectMethod(className, "void DrawString(const String&in, Font@+, float, const Vector2&in, uint = 0xFFFFFFFF, float = 0.0f, const Vector2&in = Vector2::ZERO, const Vector2&in = Vector2::ONE, FlipModes = FlipModes::None)", AS_METHODPR(T, DrawString, (const String&, Font*, float, const Vector2&, u32, float, const Vector2&, const Vector2&, FlipModes), void), AS_CALL_THISCALL);
+
+    // void SpriteBatch::DrawTriangle(const Vector2& v0, const Vector2& v1, const Vector2& v2)
+    engine->RegisterObjectMethod(className, "void DrawTriangle(const Vector2&in, const Vector2&in, const Vector2&in)", AS_METHODPR(T, DrawTriangle, (const Vector2&, const Vector2&, const Vector2&), void), AS_CALL_THISCALL);
+
+    // Texture2D* SpriteBatch::texture_
+    // Not registered because pointer
+    // ShaderVariation* SpriteBatch::vs_
+    // Not registered because pointer
+    // ShaderVariation* SpriteBatch::ps_
+    // Not registered because pointer
+
+    // Rect SpriteBatch::destination_
+    engine->RegisterObjectProperty(className, "Rect destination", offsetof(T, destination_));
+
+    // Rect SpriteBatch::sourceUV_
+    engine->RegisterObjectProperty(className, "Rect sourceUV", offsetof(T, sourceUV_));
+
+    // FlipModes SpriteBatch::flipModes_
+    engine->RegisterObjectProperty(className, "FlipModes flipModes", offsetof(T, flipModes_));
+
+    // Vector2 SpriteBatch::scale_
+    engine->RegisterObjectProperty(className, "Vector2 scale", offsetof(T, scale_));
+
+    // float SpriteBatch::rotation_
+    engine->RegisterObjectProperty(className, "float rotation", offsetof(T, rotation_));
+
+    // Vector2 SpriteBatch::origin_
+    engine->RegisterObjectProperty(className, "Vector2 origin", offsetof(T, origin_));
+
+    // u32 SpriteBatch::color0_
+    engine->RegisterObjectProperty(className, "uint color0", offsetof(T, color0_));
+
+    // u32 SpriteBatch::color1_
+    engine->RegisterObjectProperty(className, "uint color1", offsetof(T, color1_));
+
+    // u32 SpriteBatch::color2_
+    engine->RegisterObjectProperty(className, "uint color2", offsetof(T, color2_));
+
+    // u32 SpriteBatch::color3_
+    engine->RegisterObjectProperty(className, "uint color3", offsetof(T, color3_));
+
+    #ifdef REGISTER_MEMBERS_MANUAL_PART_SpriteBatch
+        REGISTER_MEMBERS_MANUAL_PART_SpriteBatch();
+    #endif
+}
+
 // SharedPtr<Technique> Technique::Clone(const String& cloneName = String::EMPTY) const
 template <class T> Technique* Technique_SharedPtrlesTechniquegre_Clone_constspStringamp_template(T* _ptr, const String& cloneName)
 {

+ 6 - 0
Source/Urho3D/AngelScript/Generated_ObjectTypes.cpp

@@ -696,6 +696,9 @@ void ASRegisterGeneratedObjectTypes(asIScriptEngine* engine)
     // class ShaderPrecache | File: ../GraphicsAPI/ShaderPrecache.h
     engine->RegisterObjectType("ShaderPrecache", 0, asOBJ_REF);
 
+    // class SpriteBatchBase | File: ../Graphics/SpriteBatchBase.h
+    // Not registered because have @nobind mark
+
     // class Time | File: ../Core/Timer.h
     engine->RegisterObjectType("Time", 0, asOBJ_REF);
 
@@ -785,6 +788,9 @@ void ASRegisterGeneratedObjectTypes(asIScriptEngine* engine)
     // class Shader | File: ../GraphicsAPI/Shader.h
     engine->RegisterObjectType("Shader", 0, asOBJ_REF);
 
+    // class SpriteBatch | File: ../Graphics/SpriteBatch.h
+    // Not registered because have @nobind mark
+
     // class Technique | File: ../Graphics/Technique.h
     engine->RegisterObjectType("Technique", 0, asOBJ_REF);
 

+ 449 - 0
Source/Urho3D/Graphics/SpriteBatch.cpp

@@ -0,0 +1,449 @@
+// Copyright (c) 2008-2022 the Urho3D project
+// License: MIT
+
+#include "SpriteBatch.h"
+
+#include "../UI/FontFace.h"
+
+namespace Urho3D
+{
+
+SpriteBatch::SpriteBatch(Context* context) : SpriteBatchBase(context)
+{
+    spriteVS_ = graphics_->GetShader(VS, "Basic", "DIFFMAP VERTEXCOLOR");
+    spritePS_ = graphics_->GetShader(PS, "Basic", "DIFFMAP VERTEXCOLOR");
+    ttfTextVS_ = graphics_->GetShader(VS, "Text");
+    ttfTextPS_ = graphics_->GetShader(PS, "Text", "ALPHAMAP");
+    spriteTextVS_ = graphics_->GetShader(VS, "Text");
+    spriteTextPS_ = graphics_->GetShader(PS, "Text");
+    sdfTextVS_ = graphics_->GetShader(VS, "Text");
+    sdfTextPS_ = graphics_->GetShader(PS, "Text", "SIGNED_DISTANCE_FIELD");
+    shapeVS_ = graphics_->GetShader(VS, "Basic", "VERTEXCOLOR");
+    shapePS_ = graphics_->GetShader(PS, "Basic", "VERTEXCOLOR");
+}
+
+static Rect PosToDest(const Vector2& position, Texture2D* texture)
+{
+    // Проверки не производятся, текстура должна быть корректной
+    return Rect
+    (
+        position.x_,
+        position.y_,
+        position.x_ + texture->GetWidth(),
+        position.y_ + texture->GetHeight()
+    );
+}
+
+// Преобразует пиксельные координаты в диапазон [0, 1]
+static Rect SrcToUV(const Rect* source, Texture2D* texture)
+{
+    if (source == nullptr)
+    {
+        return Rect(Vector2::ZERO, Vector2::ONE);
+    }
+    else
+    {
+        // Проверки не производятся, текстура должна быть корректной
+        float invWidth = 1.0f / texture->GetWidth();
+        float invHeight = 1.0f / texture->GetHeight();
+        return Rect
+        (
+            source->min_.x_ * invWidth,
+            source->min_.y_ * invHeight,
+            source->max_.x_ * invWidth,
+            source->max_.y_ * invHeight
+        );
+    }
+}
+
+void SpriteBatch::DrawSprite(Texture2D* texture, const Rect& destination, Rect* source, u32 color,
+    float rotation, const Vector2& origin, const Vector2& scale, FlipModes flipModes)
+{
+    if (!texture)
+        return;
+
+    sprite_.texture_ = texture;
+    sprite_.vs_ = spriteVS_;
+    sprite_.ps_ = spritePS_;
+    sprite_.destination_ = destination;
+    sprite_.sourceUV_ = SrcToUV(source, texture);
+    sprite_.flipModes_ = flipModes;
+    sprite_.scale_ = scale;
+    sprite_.rotation_ = rotation;
+    sprite_.origin_ = origin;
+    sprite_.color0_ = color;
+    sprite_.color1_ = color;
+    sprite_.color2_ = color;
+    sprite_.color3_ = color;
+
+    DrawSpriteInternal();
+}
+
+void SpriteBatch::DrawSprite(Texture2D* texture, const Vector2& position, Rect* source, u32 color,
+    float rotation, const Vector2 &origin, const Vector2& scale, FlipModes flipModes)
+{
+    if (!texture)
+        return;
+
+    sprite_.texture_ = texture;
+    sprite_.vs_ = spriteVS_;
+    sprite_.ps_ = spritePS_;
+    sprite_.destination_ = PosToDest(position, texture);
+    sprite_.sourceUV_ = SrcToUV(source, texture);
+    sprite_.flipModes_ = flipModes;
+    sprite_.scale_ = scale;
+    sprite_.rotation_ = rotation;
+    sprite_.origin_ = origin;
+    sprite_.color0_ = color;
+    sprite_.color1_ = color;
+    sprite_.color2_ = color;
+    sprite_.color3_ = color;
+
+    DrawSpriteInternal();
+}
+
+void SpriteBatch::DrawSpriteInternal()
+{
+    quad_.vs_ = sprite_.vs_;
+    quad_.ps_ = sprite_.ps_;
+    quad_.texture_ = sprite_.texture_;
+
+    // Если спрайт не отмасштабирован и не повёрнут, то прорисовка очень проста
+    if (sprite_.rotation_ == 0.0f && sprite_.scale_ == Vector2::ONE)
+    {
+        // Сдвигаем спрайт на -origin
+        Rect resultDest(sprite_.destination_.min_ - sprite_.origin_, sprite_.destination_.max_ - sprite_.origin_);
+        
+        // Лицевая грань задаётся по часовой стрелке. Учитываем, что ось Y направлена вниз.
+        // Но нет большой разницы, так как спрайты двусторонние
+        quad_.v0_.position_ = Vector3(resultDest.min_.x_, resultDest.min_.y_, 0); // Верхний левый угол спрайта
+        quad_.v1_.position_ = Vector3(resultDest.max_.x_, resultDest.min_.y_, 0); // Верхний правый угол
+        quad_.v2_.position_ = Vector3(resultDest.max_.x_, resultDest.max_.y_, 0); // Нижний правый угол
+        quad_.v3_.position_ = Vector3(resultDest.min_.x_, resultDest.max_.y_, 0); // Нижний левый угол
+    }
+    else
+    {
+        // Масштабировать и вращать необходимо относительно центра локальных координат:
+        // 1) При стандартном origin == Vector2::ZERO, который соответствует верхнему левому углу спрайта,
+        //    локальные координаты будут Rect(ноль, размерыСпрайта),
+        //    то есть Rect(Vector2::ZERO, destination.max_ - destination.min_)
+        // 2) При ненулевом origin нужно сдвинуть на -origin
+        Rect local(-sprite_.origin_, sprite_.destination_.max_ - sprite_.destination_.min_ - sprite_.origin_);
+
+        float sin, cos;
+        SinCos(sprite_.rotation_, sin, cos);
+
+        // Нам нужна матрица, которая масштабирует и поворачивает вершину в локальных координатах, а затем
+        // смещает ее в требуемые мировые координаты.
+        // Но в матрице 3x3 последняя строка "0 0 1", умножать на которую бессмысленно.
+        // Поэтому вычисляем без матрицы для оптимизации
+        float m11 = cos * sprite_.scale_.x_; float m12 = -sin * sprite_.scale_.y_; float m13 = sprite_.destination_.min_.x_;
+        float m21 = sin * sprite_.scale_.x_; float m22 =  cos * sprite_.scale_.y_; float m23 = sprite_.destination_.min_.y_;
+        //          0                                      0                                     1
+
+        float minXm11 = local.min_.x_ * m11;
+        float minXm21 = local.min_.x_ * m21;
+        float maxXm11 = local.max_.x_ * m11;
+        float maxXm21 = local.max_.x_ * m21;
+        float minYm12 = local.min_.y_ * m12;
+        float minYm22 = local.min_.y_ * m22;
+        float maxYm12 = local.max_.y_ * m12;
+        float maxYm22 = local.max_.y_ * m22;
+
+        // transform * Vector3(local.min_.x_, local.min_.y_, 1.0f);
+        quad_.v0_.position_ = Vector3(minXm11 + minYm12 + m13,
+                                      minXm21 + minYm22 + m23,
+                                      0.0f);
+
+        // transform * Vector3(local.max_.x_, local.min_.y_, 1.0f).
+        quad_.v1_.position_ = Vector3(maxXm11 + minYm12 + m13,
+                                      maxXm21 + minYm22 + m23,
+                                      0.0f);
+
+        // transform * Vector3(local.max_.x_, local.max_.y_, 1.0f).
+        quad_.v2_.position_ = Vector3(maxXm11 + maxYm12 + m13,
+                                      maxXm21 + maxYm22 + m23,
+                                      0.0f);
+
+        // transform * Vector3(local.min_.x_, local.max_.y_, 1.0f).
+        quad_.v3_.position_ = Vector3(minXm11 + maxYm12 + m13,
+                                      minXm21 + maxYm22 + m23,
+                                      0.0f);
+    }
+
+    if (!!(sprite_.flipModes_ & FlipModes::Horizontally))
+        Swap(sprite_.sourceUV_.min_.x_, sprite_.sourceUV_.max_.x_);
+
+    if (!!(sprite_.flipModes_ & FlipModes::Vertically))
+        Swap(sprite_.sourceUV_.min_.y_, sprite_.sourceUV_.max_.y_);
+
+    quad_.v0_.color_ = sprite_.color0_;
+    quad_.v0_.uv_ = sprite_.sourceUV_.min_;
+
+    quad_.v1_.color_ = sprite_.color1_;
+    quad_.v1_.uv_ = Vector2(sprite_.sourceUV_.max_.x_, sprite_.sourceUV_.min_.y_);
+
+    quad_.v2_.color_ = sprite_.color2_;
+    quad_.v2_.uv_ = sprite_.sourceUV_.max_;
+
+    quad_.v3_.color_ = sprite_.color3_;
+    quad_.v3_.uv_ = Vector2(sprite_.sourceUV_.min_.x_, sprite_.sourceUV_.max_.y_);
+
+    AddQuad();
+}
+
+void SpriteBatch::DrawString(const String& text, Font* font, float fontSize, const Vector2& position, u32 color,
+    float rotation, const Vector2& origin, const Vector2& scale, FlipModes flipModes)
+{
+    if (text.Length() == 0)
+        return;
+
+    Vector<c32> unicodeText;
+    for (i32 i = 0; i < text.Length();)
+        unicodeText.Push(text.NextUTF8Char(i));
+
+    if (font->GetFontType() == FONT_FREETYPE)
+    {
+        sprite_.vs_ = ttfTextVS_;
+        sprite_.ps_ = ttfTextPS_;
+    }
+    else // FONT_BITMAP
+    {
+        if (font->IsSDFFont())
+        {
+            sprite_.vs_ = sdfTextVS_;
+            sprite_.ps_ = sdfTextPS_;
+        }
+        else
+        {
+            sprite_.vs_ = spriteTextVS_;
+            sprite_.ps_ = spriteTextPS_;
+        }
+    }
+
+    sprite_.flipModes_ = flipModes;
+    sprite_.scale_ = scale;
+    sprite_.rotation_ = rotation;
+    sprite_.color0_ = color;
+    sprite_.color1_ = color;
+    sprite_.color2_ = color;
+    sprite_.color3_ = color;
+
+    FontFace* face = font->GetFace(fontSize);
+    const Vector<SharedPtr<Texture2D>>& textures = face->GetTextures();
+    // По идее все текстуры одинакового размера
+    float pixelWidth = 1.0f / textures[0]->GetWidth();
+    float pixelHeight = 1.0f / textures[0]->GetHeight();
+
+    Vector2 charPos = position;
+    Vector2 charOrig = origin;
+
+    i32 i = 0;
+    i32 step = 1;
+
+    if (!!(flipModes & FlipModes::Horizontally))
+    {
+        i = unicodeText.Size() - 1;
+        step = -1;
+    }
+
+    for (; i >= 0 && i < unicodeText.Size(); i += step)
+    {
+        const FontGlyph* glyph = face->GetGlyph(unicodeText[i]);
+        float gx = (float)glyph->x_;
+        float gy = (float)glyph->y_;
+        float gw = (float)glyph->width_;
+        float gh = (float)glyph->height_;
+        float gox = (float)glyph->offsetX_;
+        float goy = (float)glyph->offsetY_;
+
+        sprite_.texture_ = textures[glyph->page_];
+        sprite_.destination_ = Rect(charPos.x_, charPos.y_, charPos.x_ + gw, charPos.y_ + gh);
+        sprite_.sourceUV_ = Rect(gx * pixelWidth, gy * pixelHeight, (gx + gw) * pixelWidth, (gy + gh) * pixelHeight);
+
+        // Модифицируем origin, а не позицию, чтобы было правильное вращение
+        sprite_.origin_ = !!(flipModes & FlipModes::Vertically) ? charOrig - Vector2(gox, face->GetRowHeight() - goy - gh) : charOrig - Vector2(gox, goy);
+
+        DrawSpriteInternal();
+
+        charOrig.x_ -= (float)glyph->advanceX_;
+    }
+}
+
+// В отличие от Sign() никогда не возвращает ноль
+template <typename T>
+T MySign(T value) { return value >= 0.0f ? 1.0f : -1.0f; }
+
+void SpriteBatch::DrawTriangle(const Vector2& v0, const Vector2& v1, const Vector2& v2)
+{
+    triangle_.v0_.position_ = Vector3(v0);
+    triangle_.v1_.position_ = Vector3(v1);
+    triangle_.v2_.position_ = Vector3(v2);
+    AddTriangle();
+}
+
+void SpriteBatch::DrawLine(const Vector2& start, const Vector2&end, float width)
+{
+    float len = (end - start).Length();
+    if (Equals(len, 0.0f))
+        return;
+
+    // Линия - это прямоугольный полигон. Когда линия не повернута (угол поворота равен нулю), она горизонтальна.
+    //   v0 ┌───────────────┐ v1
+    //start ├───────────────┤ end
+    //   v3 └───────────────┘ v2
+    // Пользователь задает координаты точек start и end, а нам нужно определить координаты вершин v0, v1, v2, v3.
+    // Легче всего вычислить СМЕЩЕНИЯ вершин v0 и v3 от точки start и смещения вершин v1 и v2 от точки end,
+    // а потом прибавить эти смещения к координатам точек start и end.
+
+    // Когда линия горизонтальна, v0 имеет смещение (0, -halfWidth) относительно точки start,
+    // а вершина v3 имеет смещение (0, halfWidth) относительно той же точки start.
+    // Аналогично v1 = (0, -halfWidth) и v2 = (0, halfWidth) относительно точки end.
+    float halfWidth = Abs(width * 0.5f);
+
+    // Так как мы оперируем смещениями, то при повороте линии вершины v0 и v3 вращаются вокруг start, а v1 и v2 - вокруг end.
+    // Итак, вращаем точку v0 с локальными координатами (0, halfWidth).
+    // {newX = oldX * cos(deltaAngle) - oldY * sin(deltaAngle) = 0 * cos(deltaAngle) - halfWidth * sin(deltaAngle)
+    // {newY = oldX * sin(deltaAngle) + oldY * cos(deltaAngle) = 0 * sin(deltaAngle) + halfWidth * cos(deltaAngle)
+    // Так как повернутая линия может оказаться в любом квадранте, при вычислениии синуса и косинуса нам важен знак.
+    len = len * MySign(end.x_ - start.x_) * MySign(end.y_ - start.y_);
+    float cos = (end.x_ - start.x_) / len; // Прилежащий катет к гипотенузе.
+    float sin = (end.y_ - start.y_) / len; // Противолежащий катет к гипотенузе.
+    Vector2 offset = Vector2(-halfWidth * sin, halfWidth * cos);
+
+    // Так как противоположные стороны параллельны, то можно не делать повторных вычислений:
+    // смещение v0 всегда равно смещению v1, смещение v3 = смещению v2.
+    // К тому же смещения вершин v0, v1 отличаются от смещений вершин v3, v2 только знаком (противоположны).
+    Vector2 v0 = Vector2(start.x_ + offset.x_, start.y_ + offset.y_);
+    Vector2 v1 = Vector2(end.x_ + offset.x_, end.y_ + offset.y_);
+    Vector2 v2 = Vector2(end.x_ - offset.x_, end.y_ - offset.y_);
+    Vector2 v3 = Vector2(start.x_ - offset.x_, start.y_ - offset.y_);
+
+    DrawTriangle(v0, v1, v2);
+    DrawTriangle(v2, v3, v0);
+}
+
+void SpriteBatch::DrawLine(float startX, float startY, float endX, float endY, float width)
+{
+    DrawLine(Vector2(startX, startY), Vector2(endX, endY), width);
+}
+
+void SpriteBatch::DrawAABoxSolid(const Vector2& centerPos, const Vector2& halfSize)
+{
+    DrawAABoxSolid(centerPos.x_, centerPos.y_, halfSize.x_, halfSize.y_);
+}
+
+void SpriteBatch::DrawAABBSolid(const Vector2& min, const Vector2& max)
+{
+    Vector2 rightTop = Vector2(max.x_, min.y_); // Правый верхний угол
+    Vector2 leftBot = Vector2(min.x_, max.y_); // Левый нижний
+
+    DrawTriangle(min, rightTop, max);
+    DrawTriangle(leftBot, min, max);
+}
+
+void SpriteBatch::DrawAABoxSolid(float centerX, float centerY, float halfWidth, float halfHeight)
+{
+    if (halfWidth < M_EPSILON || halfHeight < M_EPSILON)
+        return;
+
+    Vector2 v0 = Vector2(centerX - halfWidth, centerY - halfHeight); // Левый верхний угол
+    Vector2 v1 = Vector2(centerX + halfWidth, centerY - halfHeight); // Правый верхний
+    Vector2 v2 = Vector2(centerX + halfWidth, centerY + halfHeight); // Правый нижний
+    Vector2 v3 = Vector2(centerX - halfWidth, centerY + halfHeight); // Левый нижний
+
+    DrawTriangle(v0, v1, v2);
+    DrawTriangle(v2, v3, v0);
+}
+
+void SpriteBatch::DrawAABoxBorder(float centerX, float centerY, float halfWidth, float halfHeight, float borderWidth)
+{
+    if (borderWidth < M_EPSILON || halfWidth < M_EPSILON || halfHeight < M_EPSILON)
+        return;
+
+    float halfBorderWidth = borderWidth * 0.5f;
+
+    // Тут нужно обработать случай, когда толщина границы больше размера AABB
+
+    // Верхняя граница
+    float y = centerY - halfHeight + halfBorderWidth;
+    DrawLine(centerX - halfWidth, y, centerX + halfWidth, y, borderWidth);
+
+    // Нижняя граница
+    y = centerY + halfHeight - halfBorderWidth;
+    DrawLine(centerX - halfWidth, y, centerX + halfWidth, y, borderWidth);
+
+    // При отрисовке боковых границ не перекрываем верхнюю и нижнюю, чтобы нормально отрисовывалось в случае полупрозрачного цвета
+
+    // Левая граница
+    float x = centerX - halfWidth + halfBorderWidth;
+    DrawLine(x, centerY - halfHeight + borderWidth, x, centerY + halfHeight - borderWidth, borderWidth);
+
+    // Правая граница
+    x = centerX + halfWidth - halfBorderWidth;
+    DrawLine(x, centerY - halfHeight + borderWidth, x, centerY + halfHeight - borderWidth, borderWidth);
+}
+
+void SpriteBatch::DrawCircle(const Vector2& centerPos, float radius)
+{
+    const int numPoints = 40;
+    Vector2 points[numPoints];
+
+    for (int i = 0; i < numPoints; ++i)
+    {
+        float angle = 360.0f * (float)i / (float)numPoints;
+        float cos, sin;
+        SinCos(angle, sin, cos);
+        points[i] = Vector2(cos, sin) * radius + centerPos;
+    }
+
+    for (int i = 1; i < numPoints; ++i)
+        DrawTriangle(points[i], points[i - 1], centerPos);
+
+    // Рисуем последний сегмент
+    DrawTriangle(points[0], points[numPoints - 1], centerPos);
+}
+
+void SpriteBatch::DrawCircle(float centerX, float centerY, float radius)
+{
+    DrawCircle(Vector2(centerX, centerY), radius);
+}
+
+// Поворачивает вектор по часовой стрелке на 90 градусов
+static Vector2 RotatePlus90(const Vector2& v)
+{
+    Vector2 result(-v.y_, v.x_);
+    return result;
+}
+
+// Поворачивает вектор по часовой стрелке на -90 градусов
+static Vector2 RotateMinus90(const Vector2& v)
+{
+    Vector2 result(v.y_, -v.x_);
+    return result;
+}
+
+void SpriteBatch::DrawArrow(const Vector2& start, const Vector2& end, float width)
+{
+    // TODO: настроить Doxygen на поддержку картинок и тут ссылку на картинку
+    Vector2 vec = end - start;
+
+    float len = vec.Length();
+    if (len < M_EPSILON)
+        return;
+
+    Vector2 dir = vec.Normalized();
+
+    // TODO: Обработать случай, когда вектор короткий
+    float headLen = width * 2; // Длина наконечника
+    float shaftLen = len - headLen; // Длина древка
+    Vector2 headStart = dir * shaftLen + start; // Начало наконечника
+    Vector2 head = dir * headLen; // Вектор от точки headStart до точки end
+    Vector2 headTop = RotateMinus90(head) + headStart;
+    Vector2 headBottom = RotatePlus90(head) + headStart;
+    DrawLine(start, headStart, width);
+    DrawTriangle(headStart, headTop, end);
+    DrawTriangle(headStart, headBottom, end);
+}
+
+}

+ 110 - 0
Source/Urho3D/Graphics/SpriteBatch.h

@@ -0,0 +1,110 @@
+// Copyright (c) 2008-2022 the Urho3D project
+// License: MIT
+
+// Работает в двух режимах - рендеринг фигур и рендеринг спрайтов.
+// Если после рисования спрайтов рисуется какая-либо фигура (или наоборот) автоматически вызывается Flush().
+
+#pragma once
+
+#include "SpriteBatchBase.h"
+
+#include "../UI/Font.h"
+
+namespace Urho3D
+{
+
+/// Режимы зеркального отображения спрайтов и текста
+enum class FlipModes : u32
+{
+    None = 0,
+    Horizontally = 1,
+    Vertically = 2,
+    Both = Horizontally | Vertically
+};
+URHO3D_FLAGS(FlipModes);
+
+/// @nobindtemp
+class URHO3D_API SpriteBatch : public SpriteBatchBase
+{
+    URHO3D_OBJECT(SpriteBatch, SpriteBatchBase);
+
+    // ============================ Рисование фигур с помощью функции AddTriangle() ============================
+ 
+public:
+
+    void DrawTriangle(const Vector2& v0, const Vector2& v1, const Vector2& v2);
+
+    void DrawLine(const Vector2& start, const Vector2&end, float width);
+    void DrawLine(float startX, float startY, float endX, float endY, float width);
+
+    void DrawAABBSolid(const Vector2& min, const Vector2& max);
+    void DrawAABoxSolid(const Vector2& centerPos, const Vector2& halfSize);
+    void DrawAABoxSolid(float centerX, float centerY, float halfWidth, float halfHeight);
+    
+    void DrawCircle(const Vector2& centerPos, float radius);
+    void DrawCircle(float centerX, float centerY, float radius);
+
+    // Граница рисуется по внутреннему периметру (не выходит за пределы AABox)
+    void DrawAABoxBorder(float centerX, float centerY, float halfWidth, float halfHeight, float borderWidth);
+
+    void DrawArrow(const Vector2& start, const Vector2& end, float width);
+
+    // ========================== Рисование спрайтов и текста с помощью функции AddQuad() ==========================
+
+public:
+
+    /// color - цвет в формате 0xAABBGGRR
+    void DrawSprite(Texture2D* texture, const Rect& destination, Rect* source = nullptr, u32 color = 0xFFFFFFFF,
+        float rotation = 0.0f, const Vector2& origin = Vector2::ZERO, const Vector2& scale = Vector2::ONE, FlipModes flipModes = FlipModes::None);
+
+    /// color - цвет в формате 0xAABBGGRR
+    void DrawSprite(Texture2D* texture, const Vector2& position, Rect* source = nullptr, u32 color = 0xFFFFFFFF,
+        float rotation = 0.0f, const Vector2 &origin = Vector2::ZERO, const Vector2& scale = Vector2::ONE, FlipModes flipModes = FlipModes::None);
+
+    /// color - цвет в формате 0xAABBGGRR
+    void DrawString(const String& text, Font* font, float fontSize, const Vector2& position, u32 color = 0xFFFFFFFF,
+        float rotation = 0.0f, const Vector2& origin = Vector2::ZERO, const Vector2& scale = Vector2::ONE, FlipModes flipModes = FlipModes::None);
+
+private:
+
+    // Кэширование шейдеров. Инициализируются в конструкторе
+    ShaderVariation* spriteVS_;
+    ShaderVariation* spritePS_;
+    ShaderVariation* ttfTextVS_;
+    ShaderVariation* ttfTextPS_;
+    ShaderVariation* spriteTextVS_;
+    ShaderVariation* spriteTextPS_;
+    ShaderVariation* sdfTextVS_;
+    ShaderVariation* sdfTextPS_;
+    ShaderVariation* shapeVS_;
+    ShaderVariation* shapePS_;
+
+    // Данные для функции DrawSpriteInternal()
+    struct
+    {
+        Texture2D* texture_;
+        ShaderVariation* vs_;
+        ShaderVariation* ps_;
+        Rect destination_;
+        Rect sourceUV_; // Текстурные координаты в диапазоне [0, 1]
+        FlipModes flipModes_;
+        Vector2 scale_;
+        float rotation_;
+        Vector2 origin_;
+        u32 color0_;
+        u32 color1_;
+        u32 color2_;
+        u32 color3_;
+    } sprite_;
+
+    // Перед вызовом этой функции нужно заполнить структуру sprite_
+    void DrawSpriteInternal();
+
+    // ========================================= Остальное =========================================
+
+public:
+
+    SpriteBatch(Context* context);
+};
+
+}

+ 249 - 0
Source/Urho3D/Graphics/SpriteBatchBase.cpp

@@ -0,0 +1,249 @@
+// Copyright (c) 2008-2022 the Urho3D project
+// License: MIT
+
+#include "SpriteBatchBase.h"
+
+#include "Graphics.h"
+#include "Camera.h"
+
+namespace Urho3D
+{
+
+void SpriteBatchBase::AddTriangle()
+{
+    // Рендерили четырёхугольники, а теперь нужно рендерить треугольники
+    if (qNumVertices_ > 0)
+        Flush();
+
+    memcpy(tVertices_ + tNumVertices_, &triangle_, sizeof(triangle_));
+    tNumVertices_ += VERTICES_PER_TRIANGLE;
+
+    // Если после добавления вершин мы заполнили массив до предела, то рендерим порцию
+    if (tNumVertices_ == MAX_TRIANGLES_IN_PORTION * VERTICES_PER_TRIANGLE)
+        Flush();
+}
+
+void SpriteBatchBase::SetShapeColor(u32 color)
+{
+    triangle_.v0_.color_ = color;
+    triangle_.v1_.color_ = color;
+    triangle_.v2_.color_ = color;
+}
+
+void SpriteBatchBase::SetShapeColor(const Color& color)
+{
+    SetShapeColor(color.ToU32());
+}
+
+void SpriteBatchBase::AddQuad()
+{
+    // Рендерили треугольники, а теперь нужно рендерить четырехугольники
+    if (tNumVertices_ > 0)
+        Flush();
+
+    if (quad_.texture_ != qCurrentTexture_ || quad_.vs_ != qCurrentVS_ || quad_.ps_ != qCurrentPS_)
+    {
+        Flush();
+
+        qCurrentVS_ = quad_.vs_;
+        qCurrentPS_ = quad_.ps_;
+        qCurrentTexture_ = quad_.texture_;
+    }
+
+    memcpy(qVertices_ + qNumVertices_, &(quad_.v0_), sizeof(QVertex) * VERTICES_PER_QUAD);
+    qNumVertices_ += VERTICES_PER_QUAD;
+
+    // Если после добавления вершин мы заполнили массив до предела, то рендерим порцию
+    if (qNumVertices_ == MAX_QUADS_IN_PORTION * VERTICES_PER_QUAD)
+        Flush();
+}
+
+IntRect SpriteBatchBase::GetViewportRect()
+{
+    if (!VirtualScreenUsed())
+        return IntRect(0, 0, graphics_->GetWidth(), graphics_->GetHeight());
+
+    float realAspect = (float)graphics_->GetWidth() / graphics_->GetHeight();
+    float virtualAspect = (float)virtualScreenSize_.x_ / virtualScreenSize_.y_;
+
+    float virtualScreenScale;
+    if (realAspect > virtualAspect)
+    {
+        // Окно шире, чем надо. Будут пустые полосы по бокам
+        virtualScreenScale = (float)graphics_->GetHeight() / virtualScreenSize_.y_;
+    }
+    else
+    {
+        // Высота окна больше, чем надо. Будут пустые полосы сверху и снизу
+        virtualScreenScale = (float)graphics_->GetWidth() / virtualScreenSize_.x_;
+    }
+
+    i32 viewportWidth = (i32)(virtualScreenSize_.x_ * virtualScreenScale);
+    i32 viewportHeight = (i32)(virtualScreenSize_.y_ * virtualScreenScale);
+
+    // Центрируем вьюпорт
+    i32 viewportX = (graphics_->GetWidth() - viewportWidth) / 2;
+    i32 viewportY = (graphics_->GetHeight() - viewportHeight) / 2;
+
+    return IntRect(viewportX, viewportY, viewportWidth + viewportX, viewportHeight + viewportY);
+}
+
+Vector2 SpriteBatchBase::GetVirtualPos(const Vector2& realPos)
+{
+    if (!VirtualScreenUsed())
+        return realPos;
+
+    IntRect viewportRect = GetViewportRect();
+    float factor = (float)virtualScreenSize_.x_ / viewportRect.Width();
+
+    float virtualX = (realPos.x_ - viewportRect.left_) * factor;
+    float virtualY = (realPos.y_ - viewportRect.top_) * factor;
+
+    return Vector2(virtualX, virtualY);
+}
+
+void SpriteBatchBase::UpdateViewProjMatrix()
+{
+    if (camera_)
+    {
+        Matrix4 matrix = camera_->GetGPUProjection() * camera_->GetView();
+        graphics_->SetShaderParameter(VSP_VIEWPROJ, matrix);
+        return;
+    }
+
+    i32 width;
+    i32 height;
+
+    if (VirtualScreenUsed())
+    {
+        width = virtualScreenSize_.x_;
+        height = virtualScreenSize_.y_;
+    }
+    else
+    {
+        width = graphics_->GetWidth();
+        height = graphics_->GetHeight();
+    }
+
+    float pixelWidth = 2.0f / width; // Двойка, так как длина отрезка [-1, 1] равна двум
+    float pixelHeight = 2.0f / height;
+
+    Matrix4 matrix = Matrix4(pixelWidth,  0.0f,         0.0f, -1.0f,
+                             0.0f,       -pixelHeight,  0.0f,  1.0f,
+                             0.0f,        0.0f,         1.0f,  0.0f,
+                             0.0f,        0.0f,         0.0f,  1.0f);
+
+    graphics_->SetShaderParameter(VSP_VIEWPROJ, matrix);
+}
+
+using GpuIndex16 = u16;
+
+SpriteBatchBase::SpriteBatchBase(Context* context) : Object(context)
+{
+    graphics_ = GetSubsystem<Graphics>();
+    
+    qIndexBuffer_ = new IndexBuffer(context_);
+    qIndexBuffer_->SetShadowed(true);
+
+    // Индексный буфер всегда содержит набор четырёхугольников, поэтому его можно сразу заполнить
+    qIndexBuffer_->SetSize(MAX_QUADS_IN_PORTION * INDICES_PER_QUAD, false);
+    GpuIndex16* buffer = (GpuIndex16*)qIndexBuffer_->Lock(0, qIndexBuffer_->GetIndexCount());
+    for (i32 i = 0; i < MAX_QUADS_IN_PORTION; i++)
+    {
+        // Первый треугольник четырёхугольника
+        buffer[i * INDICES_PER_QUAD + 0] = i * VERTICES_PER_QUAD + 0;
+        buffer[i * INDICES_PER_QUAD + 1] = i * VERTICES_PER_QUAD + 1;
+        buffer[i * INDICES_PER_QUAD + 2] = i * VERTICES_PER_QUAD + 2;
+
+        // Второй треугольник
+        buffer[i * INDICES_PER_QUAD + 3] = i * VERTICES_PER_QUAD + 2;
+        buffer[i * INDICES_PER_QUAD + 4] = i * VERTICES_PER_QUAD + 3;
+        buffer[i * INDICES_PER_QUAD + 5] = i * VERTICES_PER_QUAD + 0;
+    }
+    qIndexBuffer_->Unlock();
+
+    qVertexBuffer_ = new VertexBuffer(context_);
+    qVertexBuffer_->SetSize(MAX_QUADS_IN_PORTION * VERTICES_PER_QUAD, VertexElements::Position | VertexElements::Color | VertexElements::TexCoord1, true);
+
+    tVertexBuffer_ = new VertexBuffer(context_);
+    tVertexBuffer_->SetSize(MAX_TRIANGLES_IN_PORTION * VERTICES_PER_TRIANGLE, VertexElements::Position | VertexElements::Color, true);
+    tVertexShader_ = graphics_->GetShader(VS, "TriangleBatch");
+    tPixelShader_ = graphics_->GetShader(PS, "TriangleBatch");
+    SetShapeColor(Color::WHITE);
+}
+
+void SpriteBatchBase::Flush()
+{
+    if (tNumVertices_ > 0)
+    {
+        graphics_->ResetRenderTargets();
+        graphics_->ClearParameterSources();
+        graphics_->SetCullMode(CULL_NONE);
+        graphics_->SetDepthWrite(false);
+        graphics_->SetStencilTest(false);
+        graphics_->SetScissorTest(false);
+        graphics_->SetColorWrite(true);
+        graphics_->SetDepthTest(compareMode_);
+        graphics_->SetBlendMode(blendMode_);
+        graphics_->SetViewport(GetViewportRect());
+
+        graphics_->SetIndexBuffer(nullptr);
+        graphics_->SetVertexBuffer(tVertexBuffer_);
+        graphics_->SetTexture(0, nullptr);
+
+        // Параметры шейдеров нужно задавать после указания шейдеров
+        graphics_->SetShaders(tVertexShader_, tPixelShader_);
+        graphics_->SetShaderParameter(VSP_MODEL, Matrix3x4::IDENTITY);
+        UpdateViewProjMatrix();
+
+        // Копируем накопленную геометрию в память видеокарты
+        TVertex* buffer = (TVertex*)tVertexBuffer_->Lock(0, tNumVertices_, true);
+        memcpy(buffer, tVertices_, tNumVertices_ * sizeof(TVertex));
+        tVertexBuffer_->Unlock();
+
+        // И отрисовываем её
+        graphics_->Draw(TRIANGLE_LIST, 0, tNumVertices_);
+
+        // Начинаем новую порцию
+        tNumVertices_ = 0;
+    }
+
+    else if (qNumVertices_ > 0)
+    {
+        graphics_->ResetRenderTargets();
+        graphics_->ClearParameterSources();
+        graphics_->SetCullMode(CULL_NONE);
+        graphics_->SetDepthWrite(false);
+        graphics_->SetStencilTest(false);
+        graphics_->SetScissorTest(false);
+        graphics_->SetColorWrite(true);
+        graphics_->SetDepthTest(compareMode_);
+        graphics_->SetBlendMode(blendMode_);
+        graphics_->SetViewport(GetViewportRect());
+
+        graphics_->SetIndexBuffer(qIndexBuffer_);
+        graphics_->SetVertexBuffer(qVertexBuffer_);
+        graphics_->SetTexture(0, qCurrentTexture_);
+
+        // Параметры шейдеров нужно задавать после указания шейдеров
+        graphics_->SetShaders(qCurrentVS_, qCurrentPS_);
+        graphics_->SetShaderParameter(VSP_MODEL, Matrix3x4::IDENTITY);
+        UpdateViewProjMatrix();
+        // Мы используем только цвета вершин. Но это значение требует шейдер Basic
+        graphics_->SetShaderParameter(PSP_MATDIFFCOLOR, Color::WHITE);
+
+        // Копируем накопленную геометрию в память видеокарты
+        QVertex* buffer = (QVertex*)qVertexBuffer_->Lock(0, qNumVertices_, true);
+        memcpy(buffer, qVertices_, qNumVertices_ * sizeof(QVertex));
+        qVertexBuffer_->Unlock();
+
+        // И отрисовываем её
+        i32 numQuads = qNumVertices_ / VERTICES_PER_QUAD;
+        graphics_->Draw(TRIANGLE_LIST, 0, numQuads * INDICES_PER_QUAD, 0, qNumVertices_);
+
+        // Начинаем новую порцию
+        qNumVertices_ = 0;
+    }
+}
+
+}

+ 172 - 0
Source/Urho3D/Graphics/SpriteBatchBase.h

@@ -0,0 +1,172 @@
+// Copyright (c) 2008-2022 the Urho3D project
+// License: MIT
+
+// Класс SpriteBatch разбит на части для более легкого восприятия кода
+
+#pragma once
+
+#include "../Core/Object.h"
+#include "../Graphics/Graphics.h"
+#include "../GraphicsAPI/IndexBuffer.h"
+#include "../GraphicsAPI/ShaderVariation.h"
+#include "../GraphicsAPI/Texture2D.h"
+#include "../GraphicsAPI/VertexBuffer.h"
+
+namespace Urho3D
+{
+
+/// @nobindtemp
+class URHO3D_API SpriteBatchBase : public Object
+{
+    URHO3D_OBJECT(SpriteBatchBase, Object);
+
+    // ============================ Пакетный рендеринг треугольников ============================
+
+private:
+
+    // Максимальное число треугольников в порции
+    inline static constexpr i32 MAX_TRIANGLES_IN_PORTION = 600;
+
+    // Число вершин в треугольнике
+    inline static constexpr i32 VERTICES_PER_TRIANGLE = 3;
+
+    // Атрибуты вершин треугольников
+    struct TVertex
+    {
+        Vector3 position_;
+        u32 color_; // Цвет в формате 0xAABBGGRR
+    };
+
+    // Текущая порция треугольников
+    TVertex tVertices_[MAX_TRIANGLES_IN_PORTION * VERTICES_PER_TRIANGLE];
+
+    // Число вершин в массиве tVertices_
+    i32 tNumVertices_ = 0;
+
+    // Шейдеры для рендеринга треугольников. Инициализируются в конструкторе
+    ShaderVariation* tVertexShader_;
+    ShaderVariation* tPixelShader_;
+
+    // Вершинный буфер для треугольников (индексный буфер не используется)
+    SharedPtr<VertexBuffer> tVertexBuffer_;
+
+protected:
+
+    // Данные для функции AddTriangle().
+    // Заполняем заранее выделенную память, вместо передачи кучи аргументов в функцию
+    struct
+    {
+        TVertex v0_, v1_, v2_;
+    } triangle_;
+
+    // Добавляет 3 вершины в массив tVertices_. Вызывает Flush(), если массив полон.
+    // Перед вызовом этой функции необходимо заполнить структуру triangle_
+    void AddTriangle();
+
+public:
+
+    /// Указывает цвет для следующих треугольников (в формате 0xAABBGGRR)
+    void SetShapeColor(u32 color);
+
+    /// Указывает цвет для следующих треугольников
+    void SetShapeColor(const Color& color);
+
+    // ============================ Пакетный рендеринг четырехугольников ============================
+
+private:
+
+    // Максимальное число четырёхугольников в порции
+    inline static constexpr i32 MAX_QUADS_IN_PORTION = 500;
+
+    // Четырёхугольник состоит из двух треугольников, а значит у него 6 вершин.
+    // То есть каждый четырёхугольник занимает 6 элементов в индексном буфере
+    inline static constexpr i32 INDICES_PER_QUAD = 6;
+
+    // Две вершины четырёхугольника идентичны для обоих треугольников, поэтому
+    // в вершинном буфере каждый четырёхугольник занимает 4 элемента
+    inline static constexpr i32 VERTICES_PER_QUAD = 4;
+
+    // Атрибуты вершин четырёхугольников
+    struct QVertex
+    {
+        Vector3 position_;
+        u32 color_; // Цвет в формате 0xAABBGGRR
+        Vector2 uv_;
+    };
+
+    // Текущая порция четырёхугольников
+    QVertex qVertices_[MAX_QUADS_IN_PORTION * VERTICES_PER_QUAD];
+
+    // Число вершин в массиве qVertices_
+    i32 qNumVertices_ = 0;
+
+    // Текущая текстура
+    Texture2D* qCurrentTexture_ = nullptr;
+
+    // Текущие шейдеры
+    ShaderVariation* qCurrentVS_ = nullptr;
+    ShaderVariation* qCurrentPS_ = nullptr;
+
+    // Буферы
+    SharedPtr<IndexBuffer> qIndexBuffer_;
+    SharedPtr<VertexBuffer> qVertexBuffer_;
+
+protected:
+
+    // Данные для функции AddQuad().
+    // Заполняем заранее выделенную память, вместо передачи кучи аргументов в функцию
+    struct
+    {
+        Texture2D* texture_;
+        ShaderVariation* vs_;
+        ShaderVariation* ps_;
+        QVertex v0_, v1_, v2_, v3_;
+    } quad_;
+
+    // Добавляет 4 вершины в массив quads_.
+    // Если массив полон или требуемые шейдеры или текстура отличаются от текущих, то автоматически
+    // происходит вызов функции Flush() (то есть начинается новая порция).
+    // Перед вызовом этой функции необходимо заполнить структуру quad_
+    void AddQuad();
+
+    // ============================ Общее ============================
+
+private:
+
+    void UpdateViewProjMatrix();
+    IntRect GetViewportRect();
+
+protected:
+
+    // Кешируем доступ к подсистеме. Инициализируется в конструкторе
+    Graphics* graphics_ = nullptr;
+
+public:
+
+    // Режим наложения
+    BlendMode blendMode_ = BLEND_ALPHA;
+
+    // Если использовать CMP_LESSEQUAL, то будет учитываться содержимое буфера глубины
+    // (но сам SpriteBatch ничего не пишет в буфер глубины).
+    // При CMP_ALWAYS каждый выводимый спрайт будет перекрывать прежде отрисованные пиксели
+    CompareMode compareMode_ = CMP_ALWAYS;
+
+    // Если определена камера, то SpriteBatch рисует в мировых координатах
+    Camera* camera_ = nullptr;
+
+    // Размеры виртуального экрана. Если одна из координат <= 0, то используются реальные размеры экрана
+    IntVector2 virtualScreenSize_ = IntVector2(0, 0);
+
+    bool VirtualScreenUsed() const { return virtualScreenSize_.x_ > 0 && virtualScreenSize_.y_ > 0; }
+
+    // Конструктор
+    SpriteBatchBase(Context* context);
+
+    // Рендерит накопленную геометрию (то есть текущую порцию)
+    void Flush();
+
+    // Переводит реальные координаты в виртуальные. Используется для курсора мыши
+    Vector2 GetVirtualPos(const Vector2& realPos);
+};
+
+} // namespace Urho3D

+ 17 - 0
bin/CoreData/Shaders/GLSL/TriangleBatch.glsl

@@ -0,0 +1,17 @@
+#include "Uniforms.glsl"
+#include "Transform.glsl"
+
+varying vec4 vColor;
+
+void VS()
+{
+    mat4 modelMatrix = iModelMatrix;
+    vec3 worldPos = GetWorldPos(modelMatrix);
+    gl_Position = GetClipPos(worldPos);
+    vColor = iColor;
+}
+
+void PS()
+{
+    gl_FragColor = vColor;
+}