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 : }
|