LCOV - code coverage report
Current view: top level - source/graphics - Canvas2D.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 1 240 0.4 %
Date: 2023-01-19 00:18:29 Functions: 2 18 11.1 %

          Line data    Source code
       1             : /* Copyright (C) 2023 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "precompiled.h"
      19             : 
      20             : #include "Canvas2D.h"
      21             : 
      22             : #include "graphics/Color.h"
      23             : #include "graphics/ShaderManager.h"
      24             : #include "graphics/TextRenderer.h"
      25             : #include "graphics/TextureManager.h"
      26             : #include "maths/Rect.h"
      27             : #include "maths/Vector2D.h"
      28             : #include "ps/CStrInternStatic.h"
      29             : #include "renderer/Renderer.h"
      30             : 
      31             : #include <array>
      32             : 
      33             : namespace
      34             : {
      35             : 
      36             : // Array of 2D elements unrolled into 1D array.
      37             : using PlaneArray2D = std::array<float, 12>;
      38             : 
      39             : struct SBindingSlots
      40             : {
      41             :     int32_t transform;
      42             :     int32_t translation;
      43             :     int32_t colorAdd;
      44             :     int32_t colorMul;
      45             :     int32_t grayscaleFactor;
      46             :     int32_t tex;
      47             : };
      48             : 
      49           0 : inline void DrawTextureImpl(
      50             :     Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
      51             :     const CTexturePtr& texture, const PlaneArray2D& vertices, PlaneArray2D uvs,
      52             :     const CColor& multiply, const CColor& add, const float grayscaleFactor,
      53             :     const SBindingSlots& bindingSlots)
      54             : {
      55           0 :     texture->UploadBackendTextureIfNeeded(deviceCommandContext);
      56           0 :     deviceCommandContext->SetTexture(
      57           0 :         bindingSlots.tex, texture->GetBackendTexture());
      58           0 :     for (size_t idx = 0; idx < uvs.size(); idx += 2)
      59             :     {
      60           0 :         if (texture->GetWidth() > 0.0f)
      61           0 :             uvs[idx + 0] /= texture->GetWidth();
      62           0 :         if (texture->GetHeight() > 0.0f)
      63           0 :             uvs[idx + 1] /= texture->GetHeight();
      64             :     }
      65             : 
      66           0 :     deviceCommandContext->SetUniform(bindingSlots.colorAdd, add.AsFloatArray());
      67           0 :     deviceCommandContext->SetUniform(bindingSlots.colorMul, multiply.AsFloatArray());
      68           0 :     deviceCommandContext->SetUniform(bindingSlots.grayscaleFactor, grayscaleFactor);
      69             : 
      70           0 :     deviceCommandContext->SetVertexBufferData(
      71           0 :         0, vertices.data(), vertices.size() * sizeof(vertices[0]));
      72           0 :     deviceCommandContext->SetVertexBufferData(
      73           0 :         1, uvs.data(), uvs.size() * sizeof(uvs[0]));
      74             : 
      75           0 :     deviceCommandContext->Draw(0, vertices.size() / 2);
      76           0 : }
      77             : 
      78             : } // anonymous namespace
      79             : 
      80           0 : class CCanvas2D::Impl
      81             : {
      82             : public:
      83           0 :     Impl(
      84             :         const uint32_t widthInPixels, const uint32_t heightInPixels, const float scale,
      85             :         Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
      86           0 :         : WidthInPixels(widthInPixels), HeightInPixels(heightInPixels),
      87           0 :         Scale(scale), DeviceCommandContext(deviceCommandContext)
      88             :     {
      89           0 :         constexpr std::array<Renderer::Backend::SVertexAttributeFormat, 2> attributes{{
      90             :             {Renderer::Backend::VertexAttributeStream::POSITION,
      91             :                 Renderer::Backend::Format::R32G32_SFLOAT, 0, sizeof(float) * 2,
      92             :                 Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0},
      93             :             {Renderer::Backend::VertexAttributeStream::UV0,
      94             :                 Renderer::Backend::Format::R32G32_SFLOAT, 0, sizeof(float) * 2,
      95             :                 Renderer::Backend::VertexAttributeRate::PER_VERTEX, 1}
      96             :         }};
      97           0 :         m_VertexInputLayout = g_Renderer.GetVertexInputLayout(attributes);
      98           0 :     }
      99             : 
     100           0 :     void BindTechIfNeeded()
     101             :     {
     102           0 :         if (Tech)
     103           0 :             return;
     104             : 
     105           0 :         CShaderDefines defines;
     106           0 :         Tech = g_Renderer.GetShaderManager().LoadEffect(str_canvas2d, defines);
     107             :         // The canvas technique must be loaded because we can't render UI without it.
     108           0 :         ENSURE(Tech);
     109           0 :         DeviceCommandContext->SetGraphicsPipelineState(
     110           0 :             Tech->GetGraphicsPipelineState());
     111           0 :         DeviceCommandContext->BeginPass();
     112           0 :         Renderer::Backend::IShaderProgram* shader = Tech->GetShader();
     113             : 
     114           0 :         BindingSlots.transform = shader->GetBindingSlot(str_transform);
     115           0 :         BindingSlots.translation = shader->GetBindingSlot(str_translation);
     116           0 :         BindingSlots.colorAdd = shader->GetBindingSlot(str_colorAdd);
     117           0 :         BindingSlots.colorMul = shader->GetBindingSlot(str_colorMul);
     118           0 :         BindingSlots.grayscaleFactor = shader->GetBindingSlot(str_grayscaleFactor);
     119           0 :         BindingSlots.tex = shader->GetBindingSlot(str_tex);
     120             : 
     121           0 :         const CMatrix3D transform = GetTransform();
     122           0 :         TransformScale = CVector2D(transform._11, transform._22);
     123           0 :         Translation = CVector2D(transform._14, transform._24);
     124           0 :         DeviceCommandContext->SetUniform(
     125             :             BindingSlots.transform,
     126           0 :             transform._11, transform._21, transform._12, transform._22);
     127           0 :         DeviceCommandContext->SetUniform(
     128           0 :             BindingSlots.translation, Translation.AsFloatArray());
     129             : 
     130           0 :         DeviceCommandContext->SetVertexInputLayout(m_VertexInputLayout);
     131             :     }
     132             : 
     133           0 :     void UnbindTech()
     134             :     {
     135           0 :         if (!Tech)
     136           0 :             return;
     137             : 
     138           0 :         DeviceCommandContext->EndPass();
     139           0 :         Tech.reset();
     140             :     }
     141             : 
     142             :     /**
     143             :      * Returns model-view-projection matrix with (0,0) in top-left of screen.
     144             :      */
     145           0 :     CMatrix3D GetTransform()
     146             :     {
     147           0 :         const float width = static_cast<float>(WidthInPixels) / Scale;
     148           0 :         const float height = static_cast<float>(HeightInPixels) / Scale;
     149             : 
     150           0 :         CMatrix3D transform;
     151           0 :         transform.SetIdentity();
     152           0 :         transform.Scale(1.0f, -1.f, 1.0f);
     153           0 :         transform.Translate(0.0f, height, -1000.0f);
     154             : 
     155           0 :         CMatrix3D projection;
     156           0 :         projection.SetOrtho(0.f, width, 0.f, height, -1.f, 1000.f);
     157           0 :         transform = projection * transform;
     158             : 
     159           0 :         return transform;
     160             :     }
     161             : 
     162             :     uint32_t WidthInPixels = 1;
     163             :     uint32_t HeightInPixels = 1;
     164             :     float Scale = 1.0f;
     165             :     CVector2D TransformScale;
     166             :     CVector2D Translation;
     167             : 
     168             :     Renderer::Backend::IVertexInputLayout* m_VertexInputLayout = nullptr;
     169             : 
     170             :     Renderer::Backend::IDeviceCommandContext* DeviceCommandContext = nullptr;
     171             :     CShaderTechniquePtr Tech;
     172             : 
     173             :     // We assume that the shader can't be destroyed while it's bound. So these
     174             :     // bindings remain valid while the shader is alive.
     175             :     SBindingSlots BindingSlots;
     176             : };
     177             : 
     178           0 : CCanvas2D::CCanvas2D(
     179             :     const uint32_t widthInPixels, const uint32_t heightInPixels, const float scale,
     180           0 :     Renderer::Backend::IDeviceCommandContext* deviceCommandContext)
     181           0 :     : m(std::make_unique<Impl>(widthInPixels, heightInPixels, scale, deviceCommandContext))
     182             : {
     183             : 
     184           0 : }
     185             : 
     186           0 : CCanvas2D::~CCanvas2D()
     187             : {
     188           0 :     Flush();
     189           0 : }
     190             : 
     191           0 : void CCanvas2D::DrawLine(const std::vector<CVector2D>& points, const float width, const CColor& color)
     192             : {
     193           0 :     if (points.empty())
     194           0 :         return;
     195             : 
     196             :     // We could reuse the terrain line building, but it uses 3D space instead of
     197             :     // 2D. So it can be less optimal for a canvas.
     198             : 
     199             :     // Adding a single pixel line with alpha gradient to reduce the aliasing
     200             :     // effect.
     201           0 :     const float halfWidth = width * 0.5f + 1.0f;
     202             : 
     203             :     struct PointIndex
     204             :     {
     205             :         size_t index;
     206             :         float length;
     207             :         CVector2D normal;
     208             :     };
     209             :     // Normal for the last index is undefined.
     210           0 :     std::vector<PointIndex> pointsIndices;
     211           0 :     pointsIndices.reserve(points.size());
     212           0 :     pointsIndices.emplace_back(PointIndex{0, 0.0f, CVector2D()});
     213           0 :     for (size_t index = 0; index < points.size();)
     214             :     {
     215           0 :         size_t nextIndex = index + 1;
     216           0 :         CVector2D direction;
     217           0 :         float length = 0.0f;
     218           0 :         while (nextIndex < points.size())
     219             :         {
     220           0 :             direction = points[nextIndex] - points[pointsIndices.back().index];
     221           0 :             length = direction.Length();
     222           0 :             if (length >= halfWidth * 2.0f)
     223             :             {
     224           0 :                 direction /= length;
     225           0 :                 break;
     226             :             }
     227           0 :             ++nextIndex;
     228             :         }
     229           0 :         if (nextIndex == points.size())
     230           0 :             break;
     231           0 :         pointsIndices.back().length = length;
     232           0 :         pointsIndices.back().normal = CVector2D(-direction.Y, direction.X);
     233           0 :         pointsIndices.emplace_back(PointIndex{nextIndex, 0.0f, CVector2D()});
     234           0 :         index = nextIndex;
     235             :     }
     236             : 
     237           0 :     if (pointsIndices.size() <= 1)
     238           0 :         return;
     239             : 
     240           0 :     std::vector<std::array<CVector2D, 3>> vertices;
     241           0 :     std::vector<std::array<CVector2D, 3>> uvs;
     242           0 :     std::vector<u16> indices;
     243           0 :     const size_t reserveSize = 2 * pointsIndices.size() - 1;
     244           0 :     vertices.reserve(reserveSize);
     245           0 :     uvs.reserve(reserveSize);
     246           0 :     indices.reserve(reserveSize * 12);
     247             : 
     248           0 :     auto addVertices = [&vertices, &uvs, &indices, &halfWidth](const CVector2D& p1, const CVector2D& p2)
     249           0 :     {
     250           0 :         if (!vertices.empty())
     251             :         {
     252           0 :             const u16 lastVertexIndex = static_cast<u16>(vertices.size() * 3 - 1);
     253           0 :             ENSURE(lastVertexIndex >= 2);
     254             :             // First vertical half of the segment.
     255           0 :             indices.emplace_back(lastVertexIndex - 2);
     256           0 :             indices.emplace_back(lastVertexIndex - 1);
     257           0 :             indices.emplace_back(lastVertexIndex + 2);
     258           0 :             indices.emplace_back(lastVertexIndex - 2);
     259           0 :             indices.emplace_back(lastVertexIndex + 2);
     260           0 :             indices.emplace_back(lastVertexIndex + 1);
     261             :             // Second vertical half of the segment.
     262           0 :             indices.emplace_back(lastVertexIndex - 1);
     263           0 :             indices.emplace_back(lastVertexIndex);
     264           0 :             indices.emplace_back(lastVertexIndex + 3);
     265           0 :             indices.emplace_back(lastVertexIndex - 1);
     266           0 :             indices.emplace_back(lastVertexIndex + 3);
     267           0 :             indices.emplace_back(lastVertexIndex + 2);
     268             :         }
     269           0 :         vertices.emplace_back(std::array<CVector2D, 3>{p1, (p1 + p2) / 2.0f, p2});
     270           0 :         uvs.emplace_back(std::array<CVector2D, 3>{
     271             :             CVector2D(0.0f, 0.0f),
     272           0 :             CVector2D(std::max(1.0f, halfWidth - 1.0f), 0.0f),
     273             :             CVector2D(0.0f, 0.0f)});
     274           0 :     };
     275             : 
     276           0 :     addVertices(
     277           0 :         points[pointsIndices.front().index] - pointsIndices.front().normal * halfWidth,
     278           0 :         points[pointsIndices.front().index] + pointsIndices.front().normal * halfWidth);
     279             :     // For each pair of adjacent segments we need to add smooth transition.
     280           0 :     for (size_t index = 0; index + 2 < pointsIndices.size(); ++index)
     281             :     {
     282           0 :         const PointIndex& pointIndex = pointsIndices[index];
     283           0 :         const PointIndex& nextPointIndex = pointsIndices[index + 1];
     284             :         // Angle between adjacent segments.
     285           0 :         const float cosAlpha = pointIndex.normal.Dot(nextPointIndex.normal);
     286           0 :         constexpr float EPS = 1e-3f;
     287             :         // Use a simple segment if adjacent segments are almost codirectional.
     288           0 :         if (cosAlpha > 1.0f - EPS)
     289             :         {
     290           0 :             addVertices(
     291           0 :                 points[pointIndex.index] - pointIndex.normal * halfWidth,
     292           0 :                 points[pointIndex.index] + pointIndex.normal * halfWidth);
     293             :         }
     294             :         else
     295             :         {
     296           0 :             addVertices(
     297           0 :                 points[nextPointIndex.index] - pointIndex.normal * halfWidth,
     298           0 :                 points[nextPointIndex.index] + pointIndex.normal * halfWidth);
     299             :             // Average normal between adjacent segments. We might want to rotate it but
     300             :             // for now we assume that it's enough for current line widths.
     301             :             const CVector2D normal = cosAlpha < -1.0f + EPS
     302           0 :                 ? CVector2D(pointIndex.normal.Y, -pointIndex.normal.X)
     303           0 :                 : ((pointIndex.normal + nextPointIndex.normal) / 2.0f).Normalized();
     304           0 :             addVertices(
     305           0 :                 points[nextPointIndex.index] - normal * halfWidth,
     306           0 :                 points[nextPointIndex.index] + normal * halfWidth);
     307           0 :             addVertices(
     308           0 :                 points[nextPointIndex.index] - nextPointIndex.normal * halfWidth,
     309           0 :                 points[nextPointIndex.index] + nextPointIndex.normal * halfWidth);
     310             :         }
     311             :         // We use 16-bit indices, it means that we can't use more than 64K vertices.
     312           0 :         const size_t requiredFreeSpace = 3 * 4;
     313           0 :         if (vertices.size() * 3 + requiredFreeSpace >= 65536)
     314           0 :             break;
     315             :     }
     316           0 :     addVertices(
     317           0 :         points[pointsIndices.back().index] - pointsIndices[pointsIndices.size() - 2].normal * halfWidth,
     318           0 :         points[pointsIndices.back().index] + pointsIndices[pointsIndices.size() - 2].normal * halfWidth);
     319             : 
     320           0 :     m->BindTechIfNeeded();
     321             : 
     322           0 :     m->DeviceCommandContext->SetTexture(
     323           0 :         m->BindingSlots.tex,
     324           0 :         g_Renderer.GetTextureManager().GetAlphaGradientTexture()->GetBackendTexture());
     325           0 :     const CColor colorAdd(0.0f, 0.0f, 0.0f, 0.0f);
     326           0 :     m->DeviceCommandContext->SetUniform(
     327           0 :         m->BindingSlots.colorAdd, colorAdd.AsFloatArray());
     328           0 :     m->DeviceCommandContext->SetUniform(
     329           0 :         m->BindingSlots.colorMul, color.AsFloatArray());
     330           0 :     m->DeviceCommandContext->SetUniform(
     331           0 :         m->BindingSlots.grayscaleFactor, 0.0f);
     332             : 
     333           0 :     m->DeviceCommandContext->SetVertexBufferData(0, vertices.data(), vertices.size() * sizeof(vertices[0]));
     334           0 :     m->DeviceCommandContext->SetVertexBufferData(1, uvs.data(), uvs.size() * sizeof(uvs[0]));
     335             : 
     336           0 :     m->DeviceCommandContext->SetIndexBufferData(indices.data(), indices.size() * sizeof(indices[0]));
     337           0 :     m->DeviceCommandContext->DrawIndexed(0, indices.size(), 0);
     338             : }
     339             : 
     340           0 : void CCanvas2D::DrawRect(const CRect& rect, const CColor& color)
     341             : {
     342           0 :     const PlaneArray2D uvs
     343             :     {
     344             :         0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
     345             :         0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f
     346             :     };
     347             :     const PlaneArray2D vertices =
     348             :     {
     349           0 :         rect.left, rect.bottom,
     350           0 :         rect.right, rect.bottom,
     351           0 :         rect.right, rect.top,
     352           0 :         rect.left, rect.bottom,
     353           0 :         rect.right, rect.top,
     354           0 :         rect.left, rect.top
     355           0 :     };
     356             : 
     357           0 :     m->BindTechIfNeeded();
     358           0 :     DrawTextureImpl(
     359           0 :         m->DeviceCommandContext,
     360           0 :         g_Renderer.GetTextureManager().GetTransparentTexture(),
     361           0 :         vertices, uvs, CColor(0.0f, 0.0f, 0.0f, 0.0f), color, 0.0f,
     362           0 :         m->BindingSlots);
     363           0 : }
     364             : 
     365           0 : void CCanvas2D::DrawTexture(const CTexturePtr& texture, const CRect& destination)
     366             : {
     367           0 :     DrawTexture(texture,
     368           0 :         destination, CRect(0, 0, texture->GetWidth(), texture->GetHeight()),
     369           0 :         CColor(1.0f, 1.0f, 1.0f, 1.0f), CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f);
     370           0 : }
     371             : 
     372           0 : void CCanvas2D::DrawTexture(
     373             :     const CTexturePtr& texture, const CRect& destination, const CRect& source,
     374             :     const CColor& multiply, const CColor& add, const float grayscaleFactor)
     375             : {
     376             :     const PlaneArray2D uvs =
     377             :     {
     378           0 :         source.left, source.bottom,
     379           0 :         source.right, source.bottom,
     380           0 :         source.right, source.top,
     381           0 :         source.left, source.bottom,
     382           0 :         source.right, source.top,
     383           0 :         source.left, source.top
     384           0 :     };
     385             :     const PlaneArray2D vertices =
     386             :     {
     387           0 :         destination.left, destination.bottom,
     388           0 :         destination.right, destination.bottom,
     389           0 :         destination.right, destination.top,
     390           0 :         destination.left, destination.bottom,
     391           0 :         destination.right, destination.top,
     392           0 :         destination.left, destination.top
     393           0 :     };
     394             : 
     395           0 :     m->BindTechIfNeeded();
     396           0 :     DrawTextureImpl(
     397           0 :         m->DeviceCommandContext, texture, vertices, uvs,
     398           0 :         multiply, add, grayscaleFactor, m->BindingSlots);
     399           0 : }
     400             : 
     401           0 : void CCanvas2D::DrawRotatedTexture(
     402             :     const CTexturePtr& texture, const CRect& destination, const CRect& source,
     403             :     const CColor& multiply, const CColor& add, const float grayscaleFactor,
     404             :     const CVector2D& origin, const float angle)
     405             : {
     406             :     const PlaneArray2D uvs =
     407             :     {
     408           0 :         source.left, source.bottom,
     409           0 :         source.right, source.bottom,
     410           0 :         source.right, source.top,
     411           0 :         source.left, source.bottom,
     412           0 :         source.right, source.top,
     413           0 :         source.left, source.top
     414           0 :     };
     415             :     std::array<CVector2D, 6> corners =
     416             :     {
     417             :         destination.BottomLeft(),
     418             :         destination.BottomRight(),
     419             :         destination.TopRight(),
     420             :         destination.BottomLeft(),
     421             :         destination.TopRight(),
     422             :         destination.TopLeft()
     423           0 :     };
     424             :     PlaneArray2D vertices;
     425             :     static_assert(vertices.size() == corners.size() * 2, "We need two coordinates from each corner.");
     426           0 :     auto it = vertices.begin();
     427           0 :     for (const CVector2D& corner : corners)
     428             :     {
     429           0 :         const CVector2D vertex = origin + (corner - origin).Rotated(angle);
     430           0 :         *it++ = vertex.X;
     431           0 :         *it++ = vertex.Y;
     432             :     }
     433             : 
     434           0 :     m->BindTechIfNeeded();
     435           0 :     DrawTextureImpl(
     436           0 :         m->DeviceCommandContext, texture, vertices, uvs,
     437           0 :         multiply, add, grayscaleFactor, m->BindingSlots);
     438           0 : }
     439             : 
     440           0 : void CCanvas2D::DrawText(CTextRenderer& textRenderer)
     441             : {
     442           0 :     m->BindTechIfNeeded();
     443             : 
     444           0 :     m->DeviceCommandContext->SetUniform(
     445           0 :         m->BindingSlots.grayscaleFactor, 0.0f);
     446             : 
     447           0 :     textRenderer.Render(
     448           0 :         m->DeviceCommandContext, m->Tech->GetShader(), m->TransformScale, m->Translation);
     449           0 : }
     450             : 
     451           0 : void CCanvas2D::Flush()
     452             : {
     453           0 :     m->UnbindTech();
     454           3 : }

Generated by: LCOV version 1.13