Line data Source code
1 : /* Copyright (C) 2022 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 "GUIRenderer.h"
21 :
22 : #include "graphics/Canvas2D.h"
23 : #include "graphics/TextureManager.h"
24 : #include "gui/CGUI.h"
25 : #include "gui/CGUISprite.h"
26 : #include "gui/SettingTypes/CGUIColor.h"
27 : #include "i18n/L10n.h"
28 : #include "lib/tex/tex.h"
29 : #include "lib/utf8.h"
30 : #include "ps/CLogger.h"
31 : #include "ps/CStrInternStatic.h"
32 : #include "ps/Filesystem.h"
33 : #include "renderer/Renderer.h"
34 :
35 : using namespace GUIRenderer;
36 :
37 0 : DrawCalls::DrawCalls()
38 : {
39 0 : }
40 :
41 : // DrawCalls needs to be copyable, so it can be used in other copyable types.
42 : // But actually copying data is hard, since we'd need to avoid losing track of
43 : // who owns various pointers, so instead we just return an empty list.
44 : // The list should get filled in again (by GUIRenderer::UpdateDrawCallCache)
45 : // before it's used for rendering. (TODO: Is this class actually used safely
46 : // in practice?)
47 :
48 0 : DrawCalls::DrawCalls(const DrawCalls&)
49 0 : : std::vector<SDrawCall>()
50 : {
51 0 : }
52 :
53 0 : DrawCalls& DrawCalls::operator=(const DrawCalls&)
54 : {
55 0 : return *this;
56 : }
57 :
58 :
59 0 : void GUIRenderer::UpdateDrawCallCache(const CGUI& pGUI, DrawCalls& Calls, const CStr& SpriteName, const CRect& Size, std::map<CStr, std::unique_ptr<const CGUISprite>>& Sprites)
60 : {
61 : // This is called only when something has changed (like the size of the
62 : // sprite), so it doesn't need to be particularly efficient.
63 :
64 : // Clean up the old data
65 0 : Calls.clear();
66 :
67 : // If this object has zero size, there's nothing to render. (This happens
68 : // with e.g. tooltips that have zero size before they're first drawn, so
69 : // it isn't necessarily an error.)
70 0 : if (Size.left == Size.right && Size.top == Size.bottom)
71 0 : return;
72 :
73 0 : std::map<CStr, std::unique_ptr<const CGUISprite>>::iterator it(Sprites.find(SpriteName));
74 0 : if (it == Sprites.end())
75 : {
76 : /*
77 : * Sprite not found. Check whether this a special sprite,
78 : * and if so create a new sprite:
79 : * "stretched:filename.ext" - stretched image
80 : * "stretched:grayscale:filename.ext" - stretched grayscale image.
81 : * "cropped:0.5, 0.25" - stretch this ratio (x,y) of the top left of the image
82 : * "color:r g b a" - solid color
83 : * > "textureAsMask" - when using color, use the (optional) texture alpha channel as mask.
84 : * These can be combined, but they must be separated by a ":"
85 : * so you can have a white overlay over an stretched grayscale image with:
86 : * "grayscale:color:255 255 255 100:stretched:filename.ext"
87 : */
88 : // Check that this can be a special sprite.
89 0 : if (SpriteName.ReverseFind(":") == -1 && SpriteName.Find("color(") == -1)
90 : {
91 0 : LOGERROR("Trying to use a sprite that doesn't exist (\"%s\").", SpriteName.c_str());
92 0 : return;
93 : }
94 :
95 0 : auto sprite = std::make_unique<CGUISprite>();
96 0 : VfsPath TextureName = VfsPath("art/textures/ui") / wstring_from_utf8(SpriteName.AfterLast(":"));
97 0 : if (SpriteName.Find("stretched:") != -1)
98 : {
99 : // TODO: Should check (nicely) that this is a valid file?
100 0 : auto image = std::make_unique<SGUIImage>();
101 :
102 0 : image->m_TextureName = TextureName;
103 0 : if (SpriteName.Find("grayscale:") != -1)
104 : {
105 0 : image->m_Effects = std::make_shared<SGUIImageEffects>();
106 0 : image->m_Effects->m_Greyscale = true;
107 : }
108 :
109 0 : sprite->AddImage(std::move(image));
110 : }
111 0 : else if (SpriteName.Find("cropped:") != -1)
112 : {
113 : // TODO: Should check (nicely) that this is a valid file?
114 0 : auto image = std::make_unique<SGUIImage>();
115 :
116 0 : const bool centered = SpriteName.Find("center:") != -1;
117 :
118 0 : CStr info = SpriteName.AfterLast("cropped:").BeforeFirst(":");
119 0 : double xRatio = info.BeforeFirst(",").ToDouble();
120 0 : double yRatio = info.AfterLast(",").ToDouble();
121 : const CRect percentSize = centered
122 0 : ? CRect(50 - 50 / xRatio, 50 - 50 / yRatio, 50 + 50 / xRatio, 50 + 50 / yRatio)
123 0 : : CRect(0, 0, 100 / xRatio, 100 / yRatio);
124 0 : image->m_TextureSize = CGUISize(CRect(0, 0, 0, 0), percentSize);
125 0 : image->m_TextureName = TextureName;
126 :
127 0 : if (SpriteName.Find("grayscale:") != -1)
128 : {
129 0 : image->m_Effects = std::make_shared<SGUIImageEffects>();
130 0 : image->m_Effects->m_Greyscale = true;
131 : }
132 :
133 0 : sprite->AddImage(std::move(image));
134 : }
135 0 : if (SpriteName.Find("color:") != -1)
136 : {
137 0 : CStrW value = wstring_from_utf8(SpriteName.AfterLast("color:").BeforeFirst(":"));
138 :
139 0 : auto image = std::make_unique<SGUIImage>();
140 : CGUIColor* color;
141 :
142 : // If we are using a mask, this is an effect.
143 : // Otherwise we can fallback to the "back color" attribute
144 : // TODO: we are assuming there is a filename here.
145 0 : if (SpriteName.Find("textureAsMask:") != -1)
146 : {
147 0 : image->m_TextureName = TextureName;
148 0 : image->m_Effects = std::make_shared<SGUIImageEffects>();
149 0 : color = &image->m_Effects->m_SolidColor;
150 : }
151 : else
152 0 : color = &image->m_BackColor;
153 :
154 : // Check color is valid
155 0 : if (!CGUI::ParseString<CGUIColor>(&pGUI, value, *color))
156 : {
157 0 : LOGERROR("GUI: Error parsing sprite 'color' (\"%s\")", utf8_from_wstring(value));
158 0 : return;
159 : }
160 :
161 0 : sprite->AddImage(std::move(image));
162 : }
163 :
164 0 : if (sprite->m_Images.empty())
165 : {
166 0 : LOGERROR("Trying to use a sprite that doesn't exist (\"%s\").", SpriteName.c_str());
167 0 : return;
168 : }
169 :
170 0 : it = Sprites.emplace(SpriteName, std::move(sprite)).first;
171 : }
172 :
173 0 : Calls.reserve(it->second->m_Images.size());
174 :
175 : // Iterate through all the sprite's images, loading the texture and
176 : // calculating the texture coordinates
177 0 : std::vector<std::unique_ptr<SGUIImage>>::const_iterator cit;
178 0 : for (cit = it->second->m_Images.begin(); cit != it->second->m_Images.end(); ++cit)
179 : {
180 0 : SDrawCall Call(cit->get()); // pointers are safe since we never modify sprites/images after startup
181 :
182 0 : CRect ObjectSize = (*cit)->m_Size.GetSize(Size);
183 :
184 0 : if (ObjectSize.GetWidth() == 0.0 || ObjectSize.GetHeight() == 0.0)
185 : {
186 : // Zero sized object. Don't report as an error, since it's common for e.g. hitpoint bars.
187 0 : continue; // i.e. don't continue with this image
188 : }
189 :
190 0 : Call.m_Vertices = ObjectSize;
191 0 : if ((*cit)->m_RoundCoordinates)
192 : {
193 : // Round the vertex coordinates to integers, to avoid ugly filtering artifacts
194 0 : Call.m_Vertices.left = (int)(Call.m_Vertices.left + 0.5f);
195 0 : Call.m_Vertices.right = (int)(Call.m_Vertices.right + 0.5f);
196 0 : Call.m_Vertices.top = (int)(Call.m_Vertices.top + 0.5f);
197 0 : Call.m_Vertices.bottom = (int)(Call.m_Vertices.bottom + 0.5f);
198 : }
199 :
200 0 : bool hasTexture = false;
201 0 : if (!(*cit)->m_TextureName.empty())
202 : {
203 0 : CTextureProperties textureProps(g_L10n.LocalizePath((*cit)->m_TextureName));
204 0 : textureProps.SetAddressMode((*cit)->m_AddressMode);
205 0 : textureProps.SetIgnoreQuality(true);
206 0 : CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps);
207 0 : texture->Prefetch();
208 0 : hasTexture = true;
209 0 : Call.m_Texture = texture;
210 0 : Call.m_ObjectSize = ObjectSize;
211 : }
212 :
213 0 : Call.m_BackColor = &(*cit)->m_BackColor;
214 0 : Call.m_GrayscaleFactor = 0.0f;
215 0 : if (!hasTexture)
216 : {
217 0 : Call.m_ColorAdd = *Call.m_BackColor;
218 0 : Call.m_ColorMultiply = CColor(0.0f, 0.0f, 0.0f, 0.0f);
219 0 : Call.m_Texture = g_Renderer.GetTextureManager().GetTransparentTexture();
220 : }
221 0 : else if ((*cit)->m_Effects)
222 : {
223 0 : if ((*cit)->m_Effects->m_AddColor != CGUIColor())
224 : {
225 0 : const CColor color = (*cit)->m_Effects->m_AddColor;
226 0 : Call.m_ColorAdd = CColor(color.r, color.g, color.b, 0.0f);
227 0 : Call.m_ColorMultiply = CColor(1.0f, 1.0f, 1.0f, 1.0f);
228 : }
229 0 : else if ((*cit)->m_Effects->m_Greyscale)
230 : {
231 0 : Call.m_ColorAdd = CColor(0.0f, 0.0f, 0.0f, 0.0f);
232 0 : Call.m_ColorMultiply = CColor(1.0f, 1.0f, 1.0f, 1.0f);
233 0 : Call.m_GrayscaleFactor = 1.0f;
234 : }
235 0 : else if ((*cit)->m_Effects->m_SolidColor != CGUIColor())
236 : {
237 0 : const CColor color = (*cit)->m_Effects->m_SolidColor;
238 0 : Call.m_ColorAdd = CColor(color.r, color.g, color.b, 0.0f);
239 0 : Call.m_ColorMultiply = CColor(0.0f, 0.0f, 0.0f, color.a);
240 : }
241 : else /* Slight confusion - why no effects? */
242 : {
243 0 : Call.m_ColorAdd = CColor(0.0f, 0.0f, 0.0f, 0.0f);
244 0 : Call.m_ColorMultiply = CColor(1.0f, 1.0f, 1.0f, 1.0f);
245 : }
246 : }
247 : else
248 : {
249 0 : Call.m_ColorAdd = CColor(0.0f, 0.0f, 0.0f, 0.0f);
250 0 : Call.m_ColorMultiply = CColor(1.0f, 1.0f, 1.0f, 1.0f);
251 : }
252 :
253 0 : Calls.push_back(Call);
254 : }
255 : }
256 :
257 0 : CRect SDrawCall::ComputeTexCoords() const
258 : {
259 0 : float TexWidth = m_Texture->GetWidth();
260 0 : float TexHeight = m_Texture->GetHeight();
261 :
262 0 : if (!TexWidth || !TexHeight)
263 0 : return CRect(0, 0, 1, 1);
264 :
265 : // Textures are positioned by defining a rectangular block of the
266 : // texture (usually the whole texture), and a rectangular block on
267 : // the screen. The texture is positioned to make those blocks line up.
268 :
269 : // Get the screen's position/size for the block
270 0 : CRect BlockScreen = m_Image->m_TextureSize.GetSize(m_ObjectSize);
271 :
272 0 : if (m_Image->m_FixedHAspectRatio)
273 0 : BlockScreen.right = BlockScreen.left + BlockScreen.GetHeight() * m_Image->m_FixedHAspectRatio;
274 :
275 : // Get the texture's position/size for the block:
276 0 : CRect BlockTex;
277 :
278 : // "real_texture_placement" overrides everything
279 0 : if (m_Image->m_TexturePlacementInFile != CRect())
280 0 : BlockTex = m_Image->m_TexturePlacementInFile;
281 : // Use the whole texture
282 : else
283 0 : BlockTex = CRect(0, 0, TexWidth, TexHeight);
284 :
285 : // When rendering, BlockTex will be transformed onto BlockScreen.
286 : // Also, TexCoords will be transformed onto ObjectSize (giving the
287 : // UV coords at each vertex of the object). We know everything
288 : // except for TexCoords, so calculate it:
289 :
290 0 : CVector2D translation(BlockTex.TopLeft()-BlockScreen.TopLeft());
291 0 : float ScaleW = BlockTex.GetWidth()/BlockScreen.GetWidth();
292 0 : float ScaleH = BlockTex.GetHeight()/BlockScreen.GetHeight();
293 :
294 : CRect TexCoords (
295 : // Resize (translating to/from the origin, so the
296 : // topleft corner stays in the same place)
297 0 : (m_ObjectSize-m_ObjectSize.TopLeft())
298 0 : .Scale(ScaleW, ScaleH)
299 0 : + m_ObjectSize.TopLeft()
300 : // Translate from BlockTex to BlockScreen
301 : + translation
302 0 : );
303 :
304 : // The tex coords need to be scaled so that (texwidth,texheight) is
305 : // mapped onto (1,1)
306 0 : TexCoords.left /= TexWidth;
307 0 : TexCoords.right /= TexWidth;
308 0 : TexCoords.top /= TexHeight;
309 0 : TexCoords.bottom /= TexHeight;
310 :
311 0 : return TexCoords;
312 : }
313 :
314 0 : void GUIRenderer::Draw(DrawCalls& Calls, CCanvas2D& canvas)
315 : {
316 0 : if (Calls.empty())
317 0 : return;
318 :
319 : // Called every frame, to draw the object (based on cached calculations)
320 :
321 : // Iterate through each DrawCall, and execute whatever drawing code is being called
322 0 : for (DrawCalls::const_iterator cit = Calls.begin(); cit != Calls.end(); ++cit)
323 : {
324 : // A hack to get a correct backend texture size.
325 0 : cit->m_Texture->UploadBackendTextureIfNeeded(g_Renderer.GetDeviceCommandContext());
326 :
327 0 : CRect texCoords = cit->ComputeTexCoords().Scale(
328 0 : cit->m_Texture->GetWidth(), cit->m_Texture->GetHeight());
329 :
330 : // Ensure the quad has the correct winding order
331 0 : CRect rect = cit->m_Vertices;
332 0 : if (rect.right < rect.left)
333 : {
334 0 : std::swap(rect.right, rect.left);
335 0 : std::swap(texCoords.right, texCoords.left);
336 : }
337 0 : if (rect.bottom < rect.top)
338 : {
339 0 : std::swap(rect.bottom, rect.top);
340 0 : std::swap(texCoords.bottom, texCoords.top);
341 : }
342 :
343 0 : canvas.DrawTexture(cit->m_Texture,
344 0 : rect, texCoords, cit->m_ColorMultiply, cit->m_ColorAdd, cit->m_GrayscaleFactor);
345 : }
346 : }
|