Index: ps/trunk/source/gui/CChart.cpp =================================================================== --- ps/trunk/source/gui/CChart.cpp (revision 22604) +++ ps/trunk/source/gui/CChart.cpp (revision 22605) @@ -1,332 +1,333 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "CChart.h" #include "gui/CGUIColor.h" +#include "gui/GUIMatrix.h" #include "graphics/ShaderManager.h" #include "i18n/L10n.h" #include "lib/ogl.h" #include "ps/CLogger.h" #include "renderer/Renderer.h" #include "third_party/cppformat/format.h" #include CChart::CChart(CGUI* pGUI) : IGUIObject(pGUI), IGUITextOwner(pGUI) { AddSetting("axis_color"); AddSetting("axis_width"); AddSetting("buffer_zone"); AddSetting("font"); AddSetting("format_x"); AddSetting("format_y"); AddSetting("series_color"); AddSetting("series"); AddSetting("text_align"); GUI::GetSetting(this, "axis_width", m_AxisWidth); GUI::GetSetting(this, "format_x", m_FormatX); GUI::GetSetting(this, "format_y", m_FormatY); } CChart::~CChart() { } void CChart::HandleMessage(SGUIMessage& Message) { // TODO: implement zoom switch (Message.type) { case GUIM_SETTINGS_UPDATED: { GUI::GetSetting(this, "axis_width", m_AxisWidth); GUI::GetSetting(this, "format_x", m_FormatX); GUI::GetSetting(this, "format_y", m_FormatY); UpdateSeries(); break; } } } void CChart::DrawLine(const CShaderProgramPtr& shader, const CGUIColor& color, const std::vector& vertices) const { shader->Uniform(str_color, color); shader->VertexPointer(3, GL_FLOAT, 0, &vertices[0]); shader->AssertPointersBound(); glEnable(GL_LINE_SMOOTH); glLineWidth(1.1f); if (!g_Renderer.m_SkipSubmit) glDrawArrays(GL_LINE_STRIP, 0, vertices.size() / 3); glLineWidth(1.0f); glDisable(GL_LINE_SMOOTH); } void CChart::DrawTriangleStrip(const CShaderProgramPtr& shader, const CGUIColor& color, const std::vector& vertices) const { shader->Uniform(str_color, color); shader->VertexPointer(3, GL_FLOAT, 0, &vertices[0]); shader->AssertPointersBound(); if (!g_Renderer.m_SkipSubmit) glDrawArrays(GL_TRIANGLE_STRIP, 0, vertices.size() / 3); } void CChart::DrawAxes(const CShaderProgramPtr& shader) const { const float bz = GetBufferedZ(); CRect rect = GetChartRect(); std::vector vertices; vertices.reserve(30); #define ADD(x, y) vertices.push_back(x); vertices.push_back(y); vertices.push_back(bz + 0.5f); ADD(m_CachedActualSize.right, m_CachedActualSize.bottom); ADD(rect.right + m_AxisWidth, rect.bottom); ADD(m_CachedActualSize.left, m_CachedActualSize.bottom); ADD(rect.left, rect.bottom); ADD(m_CachedActualSize.left, m_CachedActualSize.top); ADD(rect.left, rect.top - m_AxisWidth); #undef ADD CGUIColor axis_color(0.5f, 0.5f, 0.5f, 1.f); GUI::GetSetting(this, "axis_color", axis_color); DrawTriangleStrip(shader, axis_color, vertices); } void CChart::Draw() { PROFILE3("render chart"); if (!GetGUI()) return; if (m_Series.empty()) return; const float bz = GetBufferedZ(); CRect rect = GetChartRect(); const float width = rect.GetWidth(); const float height = rect.GetHeight(); // Disable depth updates to prevent apparent z-fighting-related issues // with some drivers causing units to get drawn behind the texture. glDepthMask(0); // Setup the render state CMatrix3D transform = GetDefaultGuiMatrix(); CShaderDefines lineDefines; CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid, g_Renderer.GetSystemShaderDefines(), lineDefines); tech->BeginPass(); CShaderProgramPtr shader = tech->GetShader(); shader->Uniform(str_transform, transform); CVector2D scale(width / (m_RightTop.X - m_LeftBottom.X), height / (m_RightTop.Y - m_LeftBottom.Y)); for (const CChartData& data : m_Series) { if (data.m_Points.empty()) continue; std::vector vertices; for (const CVector2D& point : data.m_Points) { if (fabs(point.X) != std::numeric_limits::infinity() && fabs(point.Y) != std::numeric_limits::infinity()) { vertices.push_back(rect.left + (point.X - m_LeftBottom.X) * scale.X); vertices.push_back(rect.bottom - (point.Y - m_LeftBottom.Y) * scale.Y); vertices.push_back(bz + 0.5f); } else { DrawLine(shader, data.m_Color, vertices); vertices.clear(); } } if (!vertices.empty()) DrawLine(shader, data.m_Color, vertices); } if (m_AxisWidth > 0) DrawAxes(shader); tech->EndPass(); // Reset depth mask glDepthMask(1); for (size_t i = 0; i < m_TextPositions.size(); ++i) DrawText(i, CGUIColor(1.f, 1.f, 1.f, 1.f), m_TextPositions[i], bz + 0.5f); } CRect CChart::GetChartRect() const { return CRect( m_CachedActualSize.TopLeft() + CPos(m_AxisWidth, m_AxisWidth), m_CachedActualSize.BottomRight() - CPos(m_AxisWidth, m_AxisWidth) ); } void CChart::UpdateSeries() { CGUISeries* pSeries; GUI::GetSettingPointer(this, "series", pSeries); CGUIList* pSeriesColor; GUI::GetSettingPointer(this, "series_color", pSeriesColor); m_Series.clear(); m_Series.resize(pSeries->m_Series.size()); for (size_t i = 0; i < pSeries->m_Series.size(); ++i) { CChartData& data = m_Series[i]; if (i < pSeriesColor->m_Items.size() && !GUI::ParseColor(pSeriesColor->m_Items[i].GetOriginalString(), data.m_Color, 0)) LOGWARNING("GUI: Error parsing 'series_color' (\"%s\")", utf8_from_wstring(pSeriesColor->m_Items[i].GetOriginalString())); data.m_Points = pSeries->m_Series[i]; } UpdateBounds(); SetupText(); } void CChart::SetupText() { if (!GetGUI()) return; for (SGUIText* t : m_GeneratedTexts) delete t; m_GeneratedTexts.clear(); m_TextPositions.clear(); if (m_Series.empty()) return; CStrW font; if (GUI::GetSetting(this, "font", font) != PSRETURN_OK || font.empty()) font = L"default"; float buffer_zone = 0.f; GUI::GetSetting(this, "buffer_zone", buffer_zone); // Add Y-axis GUI::GetSetting(this, "format_y", m_FormatY); const float height = GetChartRect().GetHeight(); // TODO: split values depend on the format; if (m_EqualY) { // We don't need to generate many items for equal values AddFormattedValue(m_FormatY, m_RightTop.Y, font, buffer_zone); m_TextPositions.emplace_back(GetChartRect().TopLeft()); } else for (int i = 0; i < 3; ++i) { AddFormattedValue(m_FormatY, m_RightTop.Y - (m_RightTop.Y - m_LeftBottom.Y) / 3.f * i, font, buffer_zone); m_TextPositions.emplace_back(GetChartRect().TopLeft() + CPos(0.f, height / 3.f * i)); } // Add X-axis GUI::GetSetting(this, "format_x", m_FormatX); const float width = GetChartRect().GetWidth(); if (m_EqualX) { CSize text_size = AddFormattedValue(m_FormatX, m_RightTop.X, font, buffer_zone); m_TextPositions.emplace_back(GetChartRect().BottomRight() - text_size); } else for (int i = 0; i < 3; ++i) { CSize text_size = AddFormattedValue(m_FormatX, m_RightTop.X - (m_RightTop.X - m_LeftBottom.X) / 3 * i, font, buffer_zone); m_TextPositions.emplace_back(GetChartRect().BottomRight() - text_size - CPos(width / 3 * i, 0.f)); } } CSize CChart::AddFormattedValue(const CStrW& format, const float value, const CStrW& font, const float buffer_zone) { // TODO: we need to catch cases with equal formatted values. CGUIString gui_str; if (format == L"DECIMAL2") { wchar_t buffer[64]; swprintf(buffer, 64, L"%.2f", value); gui_str.SetValue(buffer); } else if (format == L"INTEGER") { wchar_t buffer[64]; swprintf(buffer, 64, L"%d", std::lround(value)); gui_str.SetValue(buffer); } else if (format == L"DURATION_SHORT") { const int seconds = value; wchar_t buffer[64]; swprintf(buffer, 64, L"%d:%02d", seconds / 60, seconds % 60); gui_str.SetValue(buffer); } else if (format == L"PERCENTAGE") { wchar_t buffer[64]; swprintf(buffer, 64, L"%d%%", std::lround(value)); gui_str.SetValue(buffer); } else { LOGERROR("Unsupported chart format: " + format.EscapeToPrintableASCII()); return CSize(); } SGUIText* text = new SGUIText(); *text = GetGUI()->GenerateText(gui_str, font, 0, buffer_zone, this); AddText(text); return text->m_Size; } void CChart::UpdateBounds() { if (m_Series.empty() || m_Series[0].m_Points.empty()) { m_LeftBottom = m_RightTop = CVector2D(0.f, 0.f); return; } m_LeftBottom = m_RightTop = m_Series[0].m_Points[0]; for (const CChartData& data : m_Series) for (const CVector2D& point : data.m_Points) { if (fabs(point.X) != std::numeric_limits::infinity() && point.X < m_LeftBottom.X) m_LeftBottom.X = point.X; if (fabs(point.Y) != std::numeric_limits::infinity() && point.Y < m_LeftBottom.Y) m_LeftBottom.Y = point.Y; if (fabs(point.X) != std::numeric_limits::infinity() && point.X > m_RightTop.X) m_RightTop.X = point.X; if (fabs(point.Y) != std::numeric_limits::infinity() && point.Y > m_RightTop.Y) m_RightTop.Y = point.Y; } m_EqualY = m_RightTop.Y == m_LeftBottom.Y; if (m_EqualY) m_RightTop.Y += 1; m_EqualX = m_RightTop.X == m_LeftBottom.X; if (m_EqualX) m_RightTop.X += 1; } Index: ps/trunk/source/gui/GUIMatrix.cpp =================================================================== --- ps/trunk/source/gui/GUIMatrix.cpp (nonexistent) +++ ps/trunk/source/gui/GUIMatrix.cpp (revision 22605) @@ -0,0 +1,42 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "GUIMatrix.h" + +#include "maths/Matrix3D.h" + +extern int g_xres, g_yres; +extern float g_GuiScale; + +CMatrix3D GetDefaultGuiMatrix() +{ + float xres = g_xres / g_GuiScale; + float yres = g_yres / g_GuiScale; + + CMatrix3D m; + m.SetIdentity(); + m.Scale(1.0f, -1.f, 1.0f); + m.Translate(0.0f, yres, -1000.0f); + + CMatrix3D proj; + proj.SetOrtho(0.f, xres, 0.f, yres, -1.f, 1000.f); + m = proj * m; + + return m; +} Property changes on: ps/trunk/source/gui/GUIMatrix.cpp ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/gui/GUIMatrix.h =================================================================== --- ps/trunk/source/gui/GUIMatrix.h (nonexistent) +++ ps/trunk/source/gui/GUIMatrix.h (revision 22605) @@ -0,0 +1,28 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_GUIMATRIX +#define INCLUDED_GUIMATRIX + +class CMatrix3D; + +/** + * Model-view-projection matrix with (0,0) in top-left of screen + */ +CMatrix3D GetDefaultGuiMatrix(); + +#endif // INCLUDED_GUIMATRIX Property changes on: ps/trunk/source/gui/GUIMatrix.h ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/gui/GUIRenderer.cpp =================================================================== --- ps/trunk/source/gui/GUIRenderer.cpp (revision 22604) +++ ps/trunk/source/gui/GUIRenderer.cpp (revision 22605) @@ -1,467 +1,468 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "GUIRenderer.h" #include "graphics/ShaderManager.h" #include "graphics/TextureManager.h" #include "gui/CGUIColor.h" #include "gui/GUIutil.h" +#include "gui/GUIMatrix.h" #include "i18n/L10n.h" #include "lib/ogl.h" #include "lib/utf8.h" #include "lib/res/h_mgr.h" #include "lib/tex/tex.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "renderer/Renderer.h" using namespace GUIRenderer; DrawCalls::DrawCalls() { } // DrawCalls needs to be copyable, so it can be used in other copyable types. // But actually copying data is hard, since we'd need to avoid losing track of // who owns various pointers, so instead we just return an empty list. // The list should get filled in again (by GUIRenderer::UpdateDrawCallCache) // before it's used for rendering. (TODO: Is this class actually used safely // in practice?) DrawCalls::DrawCalls(const DrawCalls&) : std::vector() { } DrawCalls& DrawCalls::operator=(const DrawCalls&) { return *this; } void GUIRenderer::UpdateDrawCallCache(DrawCalls& Calls, const CStr& SpriteName, const CRect& Size, int CellID, std::map& Sprites) { // This is called only when something has changed (like the size of the // sprite), so it doesn't need to be particularly efficient. // Clean up the old data Calls.clear(); // If this object has zero size, there's nothing to render. (This happens // with e.g. tooltips that have zero size before they're first drawn, so // it isn't necessarily an error.) if (Size.left == Size.right && Size.top == Size.bottom) return; std::map::iterator it(Sprites.find(SpriteName)); if (it == Sprites.end()) { /* * Sprite not found. Check whether this a special sprite, * and if so create a new sprite: * "stretched:filename.ext" - stretched image * "stretched:grayscale:filename.ext" - stretched grayscale image. * "cropped:0.5, 0.25" - stretch this ratio (x,y) of the top left of the image * "color:r g b a" - solid color * > "textureAsMask" - when using color, use the (optional) texture alpha channel as mask. * These can be combined, but they must be separated by a ":" * so you can have a white overlay over an stretched grayscale image with: * "grayscale:color:255 255 255 100:stretched:filename.ext" */ // Check that this can be a special sprite. if (SpriteName.ReverseFind(":") == -1 && SpriteName.Find("color(") == -1) { LOGERROR("Trying to use a sprite that doesn't exist (\"%s\").", SpriteName.c_str()); return; } CGUISprite* Sprite = new CGUISprite; VfsPath TextureName = VfsPath("art/textures/ui") / wstring_from_utf8(SpriteName.AfterLast(":")); if (SpriteName.Find("stretched:") != -1) { // TODO: Should check (nicely) that this is a valid file? SGUIImage* Image = new SGUIImage; Image->m_TextureName = TextureName; // Allow grayscale images for disabled portraits if (SpriteName.Find("grayscale:") != -1) { Image->m_Effects = new SGUIImageEffects; Image->m_Effects->m_Greyscale = true; } CClientArea ca(CRect(0, 0, 0, 0), CRect(0, 0, 100, 100)); Image->m_Size = ca; Image->m_TextureSize = ca; Sprite->AddImage(Image); Sprites[SpriteName] = Sprite; } else if (SpriteName.Find("cropped:") != -1) { // TODO: Should check (nicely) that this is a valid file? SGUIImage* Image = new SGUIImage; CStr info = SpriteName.AfterLast("cropped:").BeforeFirst(":"); double xRatio = info.BeforeFirst(",").ToDouble(); double yRatio = info.AfterLast(",").ToDouble(); Image->m_TextureName = TextureName; CClientArea ca(CRect(0, 0, 0, 0), CRect(0, 0, 100, 100)); CClientArea cb(CRect(0, 0, 0, 0), CRect(0, 0, 100/xRatio, 100/yRatio)); Image->m_Size = ca; Image->m_TextureSize = cb; Sprite->AddImage(Image); Sprites[SpriteName] = Sprite; } if (SpriteName.Find("color:") != -1) { CStrW value = wstring_from_utf8(SpriteName.AfterLast("color:").BeforeFirst(":")); CGUIColor color; // Check color is valid if (!GUI::ParseString(value, color)) { LOGERROR("GUI: Error parsing sprite 'color' (\"%s\")", utf8_from_wstring(value)); return; } SGUIImage* Image = new SGUIImage; // If we are using a mask, this is an effect. // Otherwise we can fallback to the "back color" attribute // TODO: we are assuming there is a filename here. if (SpriteName.Find("textureAsMask:") != -1) { Image->m_TextureName = TextureName; Image->m_Effects = new SGUIImageEffects; Image->m_Effects->m_SolidColor = color; } else Image->m_BackColor = color; CClientArea ca(CRect(0, 0, 0, 0), CRect(0, 0, 100, 100)); Image->m_Size = ca; Image->m_TextureSize = ca; Sprite->AddImage(Image); Sprites[SpriteName] = Sprite; } it = Sprites.find(SpriteName); // Otherwise, just complain and give up: if (it == Sprites.end()) { SAFE_DELETE(Sprite); LOGERROR("Trying to use a sprite that doesn't exist (\"%s\").", SpriteName.c_str()); return; } } Calls.reserve(it->second->m_Images.size()); // Iterate through all the sprite's images, loading the texture and // calculating the texture coordinates std::vector::const_iterator cit; for (cit = it->second->m_Images.begin(); cit != it->second->m_Images.end(); ++cit) { SDrawCall Call(*cit); // pointers are safe since we never modify sprites/images after startup CRect ObjectSize = (*cit)->m_Size.GetClientArea(Size); if (ObjectSize.GetWidth() == 0.0 || ObjectSize.GetHeight() == 0.0) { // Zero sized object. Don't report as an error, since it's common for e.g. hitpoint bars. continue; // i.e. don't continue with this image } Call.m_Vertices = ObjectSize; if ((*cit)->m_RoundCoordinates) { // Round the vertex coordinates to integers, to avoid ugly filtering artifacts Call.m_Vertices.left = (int)(Call.m_Vertices.left + 0.5f); Call.m_Vertices.right = (int)(Call.m_Vertices.right + 0.5f); Call.m_Vertices.top = (int)(Call.m_Vertices.top + 0.5f); Call.m_Vertices.bottom = (int)(Call.m_Vertices.bottom + 0.5f); } if (!(*cit)->m_TextureName.empty()) { CTextureProperties textureProps(g_L10n.LocalizePath((*cit)->m_TextureName)); textureProps.SetWrap((*cit)->m_WrapMode); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); texture->Prefetch(); Call.m_HasTexture = true; Call.m_Texture = texture; Call.m_EnableBlending = false; // will be overridden if the texture has an alpha channel Call.m_ObjectSize = ObjectSize; Call.m_CellID = CellID; } else { Call.m_HasTexture = false; // Enable blending if it's transparent (allowing a little error in the calculations) Call.m_EnableBlending = !(fabs((*cit)->m_BackColor.a - 1.0f) < 0.0000001f); } Call.m_BackColor = (*cit)->m_BackColor; Call.m_BorderColor = (*cit)->m_Border ? (*cit)->m_BorderColor : CGUIColor(); Call.m_DeltaZ = (*cit)->m_DeltaZ; if (!Call.m_HasTexture) { Call.m_Shader = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid); } else if ((*cit)->m_Effects) { if ((*cit)->m_Effects->m_AddColor != CGUIColor()) { Call.m_Shader = g_Renderer.GetShaderManager().LoadEffect(str_gui_add); Call.m_ShaderColorParameter = (*cit)->m_Effects->m_AddColor; // Always enable blending if something's being subtracted from // the alpha channel if ((*cit)->m_Effects->m_AddColor.a < 0.f) Call.m_EnableBlending = true; } else if ((*cit)->m_Effects->m_Greyscale) { Call.m_Shader = g_Renderer.GetShaderManager().LoadEffect(str_gui_grayscale); } else if ((*cit)->m_Effects->m_SolidColor != CGUIColor()) { Call.m_Shader = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid_mask); Call.m_ShaderColorParameter = (*cit)->m_Effects->m_SolidColor; Call.m_EnableBlending = !(fabs((*cit)->m_Effects->m_SolidColor.a - 1.0f) < 0.0000001f); } else /* Slight confusion - why no effects? */ { Call.m_Shader = g_Renderer.GetShaderManager().LoadEffect(str_gui_basic); } } else { Call.m_Shader = g_Renderer.GetShaderManager().LoadEffect(str_gui_basic); } Calls.push_back(Call); } } CRect SDrawCall::ComputeTexCoords() const { float TexWidth = m_Texture->GetWidth(); float TexHeight = m_Texture->GetHeight(); if (!TexWidth || !TexHeight) return CRect(0, 0, 1, 1); // Textures are positioned by defining a rectangular block of the // texture (usually the whole texture), and a rectangular block on // the screen. The texture is positioned to make those blocks line up. // Get the screen's position/size for the block CRect BlockScreen = m_Image->m_TextureSize.GetClientArea(m_ObjectSize); if (m_Image->m_FixedHAspectRatio) BlockScreen.right = BlockScreen.left + BlockScreen.GetHeight() * m_Image->m_FixedHAspectRatio; // Get the texture's position/size for the block: CRect BlockTex; // "real_texture_placement" overrides everything if (m_Image->m_TexturePlacementInFile != CRect()) { BlockTex = m_Image->m_TexturePlacementInFile; } // Check whether this sprite has "cell_size" set (and non-zero) else if ((int)m_Image->m_CellSize.cx) { int cols = (int)TexWidth / (int)m_Image->m_CellSize.cx; if (cols == 0) cols = 1; // avoid divide-by-zero int col = m_CellID % cols; int row = m_CellID / cols; BlockTex = CRect(m_Image->m_CellSize.cx*col, m_Image->m_CellSize.cy*row, m_Image->m_CellSize.cx*(col+1), m_Image->m_CellSize.cy*(row+1)); } // Use the whole texture else BlockTex = CRect(0, 0, TexWidth, TexHeight); // When rendering, BlockTex will be transformed onto BlockScreen. // Also, TexCoords will be transformed onto ObjectSize (giving the // UV coords at each vertex of the object). We know everything // except for TexCoords, so calculate it: CPos translation (BlockTex.TopLeft()-BlockScreen.TopLeft()); float ScaleW = BlockTex.GetWidth()/BlockScreen.GetWidth(); float ScaleH = BlockTex.GetHeight()/BlockScreen.GetHeight(); CRect TexCoords ( // Resize (translating to/from the origin, so the // topleft corner stays in the same place) (m_ObjectSize-m_ObjectSize.TopLeft()) .Scale(ScaleW, ScaleH) + m_ObjectSize.TopLeft() // Translate from BlockTex to BlockScreen + translation ); // The tex coords need to be scaled so that (texwidth,texheight) is // mapped onto (1,1) TexCoords.left /= TexWidth; TexCoords.right /= TexWidth; TexCoords.top /= TexHeight; TexCoords.bottom /= TexHeight; return TexCoords; } void GUIRenderer::Draw(DrawCalls& Calls, float Z) { // Called every frame, to draw the object (based on cached calculations) // TODO: batching by shader/texture/etc would be nice CMatrix3D matrix = GetDefaultGuiMatrix(); glDisable(GL_BLEND); // Set LOD bias so mipmapped textures are prettier #if CONFIG2_GLES #warning TODO: implement GUI LOD bias for GLES #else glTexEnvf(GL_TEXTURE_FILTER_CONTROL, GL_TEXTURE_LOD_BIAS, -1.f); #endif // Iterate through each DrawCall, and execute whatever drawing code is being called for (DrawCalls::const_iterator cit = Calls.begin(); cit != Calls.end(); ++cit) { cit->m_Shader->BeginPass(); CShaderProgramPtr shader = cit->m_Shader->GetShader(); shader->Uniform(str_transform, matrix); if (cit->m_HasTexture) { shader->Uniform(str_color, cit->m_ShaderColorParameter); shader->BindTexture(str_tex, cit->m_Texture); if (cit->m_EnableBlending || cit->m_Texture->HasAlpha()) // (shouldn't call HasAlpha before BindTexture) { glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_BLEND); } CRect TexCoords = cit->ComputeTexCoords(); // Ensure the quad has the correct winding order, and update texcoords to match CRect Verts = cit->m_Vertices; if (Verts.right < Verts.left) { std::swap(Verts.right, Verts.left); std::swap(TexCoords.right, TexCoords.left); } if (Verts.bottom < Verts.top) { std::swap(Verts.bottom, Verts.top); std::swap(TexCoords.bottom, TexCoords.top); } std::vector data; #define ADD(u, v, x, y, z) STMT(data.push_back(u); data.push_back(v); data.push_back(x); data.push_back(y); data.push_back(z)) ADD(TexCoords.left, TexCoords.bottom, Verts.left, Verts.bottom, Z + cit->m_DeltaZ); ADD(TexCoords.right, TexCoords.bottom, Verts.right, Verts.bottom, Z + cit->m_DeltaZ); ADD(TexCoords.right, TexCoords.top, Verts.right, Verts.top, Z + cit->m_DeltaZ); ADD(TexCoords.right, TexCoords.top, Verts.right, Verts.top, Z + cit->m_DeltaZ); ADD(TexCoords.left, TexCoords.top, Verts.left, Verts.top, Z + cit->m_DeltaZ); ADD(TexCoords.left, TexCoords.bottom, Verts.left, Verts.bottom, Z + cit->m_DeltaZ); #undef ADD shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 5*sizeof(float), &data[0]); shader->VertexPointer(3, GL_FLOAT, 5*sizeof(float), &data[2]); glDrawArrays(GL_TRIANGLES, 0, 6); } else { shader->Uniform(str_color, cit->m_BackColor); if (cit->m_EnableBlending) { glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_BLEND); } // Ensure the quad has the correct winding order CRect Verts = cit->m_Vertices; if (Verts.right < Verts.left) std::swap(Verts.right, Verts.left); if (Verts.bottom < Verts.top) std::swap(Verts.bottom, Verts.top); std::vector data; #define ADD(x, y, z) STMT(data.push_back(x); data.push_back(y); data.push_back(z)) ADD(Verts.left, Verts.bottom, Z + cit->m_DeltaZ); ADD(Verts.right, Verts.bottom, Z + cit->m_DeltaZ); ADD(Verts.right, Verts.top, Z + cit->m_DeltaZ); ADD(Verts.right, Verts.top, Z + cit->m_DeltaZ); ADD(Verts.left, Verts.top, Z + cit->m_DeltaZ); ADD(Verts.left, Verts.bottom, Z + cit->m_DeltaZ); shader->VertexPointer(3, GL_FLOAT, 3*sizeof(float), &data[0]); glDrawArrays(GL_TRIANGLES, 0, 6); if (cit->m_BorderColor != CGUIColor()) { shader->Uniform(str_color, cit->m_BorderColor); data.clear(); ADD(Verts.left + 0.5f, Verts.top + 0.5f, Z + cit->m_DeltaZ); ADD(Verts.right - 0.5f, Verts.top + 0.5f, Z + cit->m_DeltaZ); ADD(Verts.right - 0.5f, Verts.bottom - 0.5f, Z + cit->m_DeltaZ); ADD(Verts.left + 0.5f, Verts.bottom - 0.5f, Z + cit->m_DeltaZ); shader->VertexPointer(3, GL_FLOAT, 3*sizeof(float), &data[0]); glDrawArrays(GL_LINE_LOOP, 0, 4); } #undef ADD } cit->m_Shader->EndPass(); glDisable(GL_BLEND); } #if CONFIG2_GLES #warning TODO: implement GUI LOD bias for GLES #else glTexEnvf(GL_TEXTURE_FILTER_CONTROL, GL_TEXTURE_LOD_BIAS, 0.f); #endif } Index: ps/trunk/source/gui/GUIutil.cpp =================================================================== --- ps/trunk/source/gui/GUIutil.cpp (revision 22604) +++ ps/trunk/source/gui/GUIutil.cpp (revision 22605) @@ -1,430 +1,409 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "GUIutil.h" #include "gui/GUI.h" #include "gui/GUIManager.h" -#include "maths/Matrix3D.h" #include "ps/CLogger.h" -#include "ps/GameSetup/Config.h" - -extern int g_xres, g_yres; template CGUISetting::CGUISetting(IGUIObject& pObject, const CStr& Name) : m_pSetting(T()), m_Name(Name), m_pObject(pObject) { } template bool CGUISetting::FromString(const CStrW& Value, const bool& SkipMessage) { T settingValue; if (!GUI::ParseString(Value, settingValue)) return false; GUI::SetSetting(&m_pObject, m_Name, settingValue, SkipMessage); return true; }; template bool CGUISetting::FromJSVal(JSContext* cx, JS::HandleValue Value) { T settingValue; if (!ScriptInterface::FromJSVal(cx, Value, settingValue)) return false; GUI::SetSetting(&m_pObject, m_Name, settingValue); return true; }; template void CGUISetting::ToJSVal(JSContext* cx, JS::MutableHandleValue Value) { ScriptInterface::ToJSVal(cx, Value, m_pSetting); }; template <> bool __ParseString(const CStrW& Value, bool& Output) { if (Value == L"true") Output = true; else if (Value == L"false") Output = false; else return false; return true; } template <> bool __ParseString(const CStrW& Value, int& Output) { Output = Value.ToInt(); return true; } template <> bool __ParseString(const CStrW& Value, u32& Output) { Output = Value.ToUInt(); return true; } template <> bool __ParseString(const CStrW& Value, float& Output) { Output = Value.ToFloat(); return true; } template <> bool __ParseString(const CStrW& Value, CRect& Output) { const unsigned int NUM_COORDS = 4; float coords[NUM_COORDS]; std::wstringstream stream; stream.str(Value); // Parse each coordinate for (unsigned int i = 0; i < NUM_COORDS; ++i) { if (stream.eof()) { LOGWARNING("Too few CRect parameters (min %i). Your input: '%s'", NUM_COORDS, Value.ToUTF8().c_str()); return false; } stream >> coords[i]; if ((stream.rdstate() & std::wstringstream::failbit) != 0) { LOGWARNING("Unable to parse CRect parameters. Your input: '%s'", Value.ToUTF8().c_str()); return false; } } if (!stream.eof()) { LOGWARNING("Too many CRect parameters (max %i). Your input: '%s'", NUM_COORDS, Value.ToUTF8().c_str()); return false; } // Finally the rectangle values Output = CRect(coords[0], coords[1], coords[2], coords[3]); return true; } template <> bool __ParseString(const CStrW& Value, CClientArea& Output) { return Output.SetClientArea(Value.ToUTF8()); } template <> bool GUI::ParseColor(const CStrW& Value, CGUIColor& Output, int DefaultAlpha) { return Output.ParseString(Value.ToUTF8(), DefaultAlpha); } template <> bool __ParseString(const CStrW& Value, CGUIColor& Output) { return Output.ParseString(Value.ToUTF8()); } template <> bool __ParseString(const CStrW& Value, CSize& Output) { const unsigned int NUM_COORDS = 2; float coords[NUM_COORDS]; std::wstringstream stream; stream.str(Value); // Parse each coordinate for (unsigned int i = 0; i < NUM_COORDS; ++i) { if (stream.eof()) { LOGWARNING("Too few CSize parameters (min %i). Your input: '%s'", NUM_COORDS, Value.ToUTF8().c_str()); return false; } stream >> coords[i]; if ((stream.rdstate() & std::wstringstream::failbit) != 0) { LOGWARNING("Unable to parse CSize parameters. Your input: '%s'", Value.ToUTF8().c_str()); return false; } } Output.cx = coords[0]; Output.cy = coords[1]; if (!stream.eof()) { LOGWARNING("Too many CSize parameters (max %i). Your input: '%s'", NUM_COORDS, Value.ToUTF8().c_str()); return false; } return true; } template <> bool __ParseString(const CStrW& Value, CPos& Output) { const unsigned int NUM_COORDS = 2; float coords[NUM_COORDS]; std::wstringstream stream; stream.str(Value); // Parse each coordinate for (unsigned int i = 0; i < NUM_COORDS; ++i) { if (stream.eof()) { LOGWARNING("Too few CPos parameters (min %i). Your input: '%s'", NUM_COORDS, Value.ToUTF8().c_str()); return false; } stream >> coords[i]; if ((stream.rdstate() & std::wstringstream::failbit) != 0) { LOGWARNING("Unable to parse CPos parameters. Your input: '%s'", Value.ToUTF8().c_str()); return false; } } Output.x = coords[0]; Output.y = coords[1]; if (!stream.eof()) { LOGWARNING("Too many CPos parameters (max %i). Your input: '%s'", NUM_COORDS, Value.ToUTF8().c_str()); return false; } return true; } template <> bool __ParseString(const CStrW& Value, EAlign& Output) { if (Value == L"left") Output = EAlign_Left; else if (Value == L"center") Output = EAlign_Center; else if (Value == L"right") Output = EAlign_Right; else return false; return true; } template <> bool __ParseString(const CStrW& Value, EVAlign& Output) { if (Value == L"top") Output = EVAlign_Top; else if (Value == L"center") Output = EVAlign_Center; else if (Value == L"bottom") Output = EVAlign_Bottom; else return false; return true; } template <> bool __ParseString(const CStrW& Value, CGUIString& Output) { Output.SetValue(Value); return true; } template <> bool __ParseString(const CStrW& Value, CStr& Output) { // Do very little. Output = Value.ToUTF8(); return true; } template <> bool __ParseString(const CStrW& Value, CStrW& Output) { Output = Value; return true; } template <> bool __ParseString(const CStrW& Value, CGUISpriteInstance& Output) { Output = CGUISpriteInstance(Value.ToUTF8()); return true; } template <> bool __ParseString(const CStrW& UNUSED(Value), CGUIList& UNUSED(Output)) { return false; } template <> bool __ParseString(const CStrW& UNUSED(Value), CGUISeries& UNUSED(Output)) { return false; } -CMatrix3D GetDefaultGuiMatrix() -{ - float xres = g_xres / g_GuiScale; - float yres = g_yres / g_GuiScale; - - CMatrix3D m; - m.SetIdentity(); - m.Scale(1.0f, -1.f, 1.0f); - m.Translate(0.0f, yres, -1000.0f); - - CMatrix3D proj; - proj.SetOrtho(0.f, xres, 0.f, yres, -1.f, 1000.f); - m = proj * m; - - return m; -} - template PSRETURN GUI::GetSettingPointer(const IGUIObject* pObject, const CStr& Setting, T*& Value) { ENSURE(pObject != NULL); std::map::const_iterator it = pObject->m_Settings.find(Setting); if (it == pObject->m_Settings.end()) { LOGWARNING("setting %s was not found on object %s", Setting.c_str(), pObject->GetPresentableName().c_str()); return PSRETURN_GUI_InvalidSetting; } if (it->second == nullptr) return PSRETURN_GUI_InvalidSetting; // Get value Value = &(static_cast* >(it->second)->m_pSetting); return PSRETURN_OK; } template PSRETURN GUI::GetSetting(const IGUIObject* pObject, const CStr& Setting, T& Value) { T* v = NULL; PSRETURN ret = GetSettingPointer(pObject, Setting, v); if (ret == PSRETURN_OK) Value = *v; return ret; } template PSRETURN GUI::SetSetting(IGUIObject* pObject, const CStr& Setting, T& Value, const bool& SkipMessage) { return SetSettingWrap(pObject, Setting, Value, SkipMessage, [&pObject, &Setting, &Value]() { static_cast* >(pObject->m_Settings[Setting])->m_pSetting = std::move(Value); }); } template PSRETURN GUI::SetSetting(IGUIObject* pObject, const CStr& Setting, const T& Value, const bool& SkipMessage) { return SetSettingWrap(pObject, Setting, Value, SkipMessage, [&pObject, &Setting, &Value]() { static_cast* >(pObject->m_Settings[Setting])->m_pSetting = Value; }); } // Helper function for SetSetting template bool IsBoolTrue(const T&) { return false; } template <> bool IsBoolTrue(const bool& v) { return v; } template PSRETURN GUI::SetSettingWrap(IGUIObject* pObject, const CStr& Setting, const T& Value, const bool& SkipMessage, const std::function& valueSet) { ENSURE(pObject != NULL); if (!pObject->SettingExists(Setting)) { LOGWARNING("setting %s was not found on object %s", Setting.c_str(), pObject->GetPresentableName().c_str()); return PSRETURN_GUI_InvalidSetting; } valueSet(); // Some settings needs special attention at change // If setting was "size", we need to re-cache itself and all children if (Setting == "size") { RecurseObject(0, pObject, &IGUIObject::UpdateCachedSize); } else if (Setting == "hidden") { // Hiding an object requires us to reset it and all children if (IsBoolTrue(Value)) RecurseObject(0, pObject, &IGUIObject::ResetStates); } if (!SkipMessage) { SGUIMessage msg(GUIM_SETTINGS_UPDATED, Setting); pObject->HandleMessage(msg); } return PSRETURN_OK; } // Instantiate templated functions: #define TYPE(T) \ template PSRETURN GUI::GetSettingPointer(const IGUIObject* pObject, const CStr& Setting, T*& Value); \ template PSRETURN GUI::GetSetting(const IGUIObject* pObject, const CStr& Setting, T& Value); \ template PSRETURN GUI::SetSetting(IGUIObject* pObject, const CStr& Setting, T& Value, const bool& SkipMessage); \ template PSRETURN GUI::SetSetting(IGUIObject* pObject, const CStr& Setting, const T& Value, const bool& SkipMessage); \ template class CGUISetting; \ #define GUITYPE_IGNORE_CGUISpriteInstance #include "GUItypes.h" #undef GUITYPE_IGNORE_CGUISpriteInstance #undef TYPE // Don't instantiate GetSetting - this will cause linker errors if // you attempt to retrieve a sprite using GetSetting, since that copies the sprite // and will mess up the caching performed by DrawSprite. You have to use GetSettingPointer // instead. (This is mainly useful to stop me accidentally using the wrong function.) template PSRETURN GUI::GetSettingPointer(const IGUIObject* pObject, const CStr& Setting, CGUISpriteInstance*& Value); template PSRETURN GUI::SetSetting(IGUIObject* pObject, const CStr& Setting, CGUISpriteInstance& Value, const bool& SkipMessage); template class CGUISetting; Index: ps/trunk/source/gui/GUIutil.h =================================================================== --- ps/trunk/source/gui/GUIutil.h (revision 22604) +++ ps/trunk/source/gui/GUIutil.h (revision 22605) @@ -1,361 +1,357 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* GUI util --Overview-- Contains help class GUI<>, which gives us templated parameter to all functions within GUI. --More info-- Check GUI.h */ #ifndef INCLUDED_GUIUTIL #define INCLUDED_GUIUTIL #include "gui/CGUI.h" #include "gui/CGUISprite.h" #include "gui/GUIbase.h" #include "gui/IGUIObject.h" #include class CClientArea; class CGUIString; -class CMatrix3D; template class GUI; class IGUISetting { public: virtual ~IGUISetting() {}; /** * Parses the given string and assigns to the setting value. Used for parsing XML attributes. */ virtual bool FromString(const CStrW& Value, const bool& SkipMessage) = 0; /** * Parses the given JS::Value using ScriptInterface::FromJSVal and assigns it to the setting data. */ virtual bool FromJSVal(JSContext* cx, JS::HandleValue Value) = 0; /** * Converts the setting data to a JS::Value using ScriptInterface::ToJSVal. */ virtual void ToJSVal(JSContext* cx, JS::MutableHandleValue Value) = 0; }; template class CGUISetting : public IGUISetting { friend class GUI; public: CGUISetting(IGUIObject& pObject, const CStr& Name); /** * Parses the given string and assigns to the setting value. Used for parsing XML attributes. */ bool FromString(const CStrW& Value, const bool& SkipMessage) override; /** * Parses the given JS::Value using ScriptInterface::FromJSVal and assigns it to the setting data. */ bool FromJSVal(JSContext* cx, JS::HandleValue Value) override; /** * Converts the setting data to a JS::Value using ScriptInterface::ToJSVal. */ void ToJSVal(JSContext* cx, JS::MutableHandleValue Value) override; private: /** * The object that stores this setting. */ IGUIObject& m_pObject; /** * Property name identifying the setting. */ const CStr m_Name; /** * Holds the value of the setting.. */ T m_pSetting; }; template bool __ParseString(const CStrW& Value, T& tOutput); -// Model-view-projection matrix with (0,0) in top-left of screen -CMatrix3D GetDefaultGuiMatrix(); - struct SGUIMessage; /** * Includes static functions that needs one template * argument. * * int is only to please functions that doesn't even use T * and are only within this class because it's convenient */ template class GUI { // Private functions further ahead friend class CGUI; friend class IGUIObject; public: // Like GetSetting (below), but doesn't make a copy of the value // (so it can be modified later) static PSRETURN GetSettingPointer(const IGUIObject* pObject, const CStr& Setting, T*& Value); /** * Retrieves a setting by name from object pointer * * @param pObject Object pointer * @param Setting Setting by name * @param Value Stores value here, note type T! */ static PSRETURN GetSetting(const IGUIObject* pObject, const CStr& Setting, T& Value); /** * Sets a value by name using a real datatype as input. * * This is the official way of setting a setting, no other * way should only cautiously be used! * * This variant will use the move-assignment. * * @param pObject Object pointer * @param Setting Setting by name * @param Value Sets value to this, note type T! * @param SkipMessage Does not send a GUIM_SETTINGS_UPDATED if true */ static PSRETURN SetSetting(IGUIObject* pObject, const CStr& Setting, T& Value, const bool& SkipMessage = false); /** * This variant will copy the value. */ static PSRETURN SetSetting(IGUIObject* pObject, const CStr& Setting, const T& Value, const bool& SkipMessage = false); /** * This will return the value of the first sprite if it's not null, * if it is null, it will return the value of the second sprite, if * that one is null, then null it is. * * @param prim Primary sprite that should be used * @param sec Secondary sprite if Primary should fail * @return Resulting string */ static const CGUISpriteInstance& FallBackSprite(const CGUISpriteInstance& prim, const CGUISpriteInstance& sec) { return (prim.IsEmpty() ? sec : prim); } /** * Same principle as FallBackSprite * * @param prim Primary color that should be used * @param sec Secondary color if Primary should fail * @return Resulting color * @see FallBackSprite */ static CGUIColor FallBackColor(const CGUIColor& prim, const CGUIColor& sec) { // CGUIColor() == null. return ((prim!=CGUIColor())?(prim):(sec)); } /** * Sets a value by setting and object name using a real * datatype as input. * * This is just a wrapper for __ParseString() which really * works the magic. * * @param Value The value in string form, like "0 0 100% 100%" * @param tOutput Parsed value of type T * @return True at success. * * @see __ParseString() */ static bool ParseString(const CStrW& Value, T& tOutput) { return __ParseString(Value, tOutput); } static bool ParseColor(const CStrW& Value, CGUIColor& tOutput, int DefaultAlpha); private: /** * Changes the value of the setting by calling the valueSet functon that performs either a copy or move assignment. * Updates some internal data depending on the setting changed. */ static PSRETURN SetSettingWrap(IGUIObject* pObject, const CStr& Setting, const T& Value, const bool& SkipMessage, const std::function& valueSet); // templated typedef of function pointer typedef void (IGUIObject::*void_Object_pFunction_argT)(const T& arg); typedef void (IGUIObject::*void_Object_pFunction_argRefT)(T& arg); typedef void (IGUIObject::*void_Object_pFunction)(); typedef void (IGUIObject::*void_Object_pFunction_argTJS)(const T& arg, JS::HandleValueArray paramData); /** * If you want to call a IGUIObject-function * on not just an object, but also on ALL of their children * you want to use this recursion system. * It recurses an object calling a function on itself * and all children (and so forth). * * Restrictions:\n * You can also set restrictions, so that if the recursion * reaches an objects with certain setup, it just doesn't * call the function on the object, nor it's children for * that matter. i.e. it cuts that object off from the * recursion tree. What setups that can cause restrictions * are hardcoded and specific. Check out the defines * GUIRR_* for all different setups. * * Error reports are either logged or thrown out of RecurseObject. * Always use it with try/catch! * * @param RR Recurse Restrictions, set to 0 if no restrictions * @param pObject Top object, this is where the iteration starts * @param pFunc Function to recurse * @param Argument Argument for pFunc of type T * @throws PSERROR Depends on what pFunc might throw. PSERROR is standard. * Itself doesn't throw anything. */ static void RecurseObject(int RR, IGUIObject* pObject, void_Object_pFunction_argT pFunc, const T& Argument) { // TODO Gee: Don't run this for the base object. if (CheckIfRestricted(RR, pObject)) return; (pObject->*pFunc)(Argument); // Iterate children for (IGUIObject* const& obj : *pObject) RecurseObject(RR, obj, pFunc, Argument); } /** * Argument is reference. * * @see RecurseObject() */ static void RecurseObject(int RR, IGUIObject* pObject, void_Object_pFunction_argRefT pFunc, T& Argument) { if (CheckIfRestricted(RR, pObject)) return; (pObject->*pFunc)(Argument); // Iterate children for (IGUIObject* const& obj : *pObject) RecurseObject(RR, obj, pFunc, Argument); } static void RecurseObject(int RR, IGUIObject* pObject, void_Object_pFunction_argTJS pFunc, const T& Argument, JS::HandleValueArray paramData) { if (CheckIfRestricted(RR, pObject)) return; (pObject->*pFunc)(Argument, paramData); // Iterate children for (IGUIObject* const& obj : *pObject) RecurseObject(RR, obj, pFunc, Argument, paramData); } /** * With no argument. * * @see RecurseObject() */ static void RecurseObject(int RR, IGUIObject* pObject, void_Object_pFunction pFunc) { if (CheckIfRestricted(RR, pObject)) return; (pObject->*pFunc)(); // Iterate children for (IGUIObject* const& obj : *pObject) RecurseObject(RR, obj, pFunc); } /** * Checks restrictions for the iteration, for instance if * you tell the recursor to avoid all hidden objects, it * will, and this function checks a certain object's * restriction values. * * @param RR What kind of restriction, for instance hidden or disabled * @param pObject Object * @return true if restricted */ static bool CheckIfRestricted(int RR, IGUIObject* pObject) { // Statically initialise some strings, so we don't have to do // lots of allocation every time this function is called static const CStr strHidden("hidden"); static const CStr strEnabled("enabled"); static const CStr strGhost("ghost"); if (RR & GUIRR_HIDDEN) { bool hidden = true; GUI::GetSetting(pObject, strHidden, hidden); if (hidden) return true; } if (RR & GUIRR_DISABLED) { bool enabled = false; GUI::GetSetting(pObject, strEnabled, enabled); if (!enabled) return true; } if (RR & GUIRR_GHOST) { bool ghost = true; GUI::GetSetting(pObject, strGhost, ghost); if (ghost) return true; } // false means not restricted return false; } }; #endif // INCLUDED_GUIUTIL Index: ps/trunk/source/gui/MiniMap.cpp =================================================================== --- ps/trunk/source/gui/MiniMap.cpp (revision 22604) +++ ps/trunk/source/gui/MiniMap.cpp (revision 22605) @@ -1,709 +1,710 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include #include "MiniMap.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/MiniPatch.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerritoryTexture.h" #include "gui/GUI.h" #include "gui/GUIManager.h" +#include "gui/GUIMatrix.h" #include "lib/bits.h" #include "lib/external_libraries/libsdl.h" #include "lib/ogl.h" #include "lib/timer.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/World.h" #include "ps/XML/Xeromyces.h" #include "renderer/Renderer.h" #include "renderer/WaterManager.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpMinimap.h" #include "simulation2/system/ParamNode.h" extern bool g_GameRestarted; // Set max drawn entities to UINT16_MAX for now, which is more than enough // TODO: we should be cleverer about drawing them to reduce clutter const u16 MAX_ENTITIES_DRAWN = 65535; static unsigned int ScaleColor(unsigned int color, float x) { unsigned int r = unsigned(float(color & 0xff) * x); unsigned int g = unsigned(float((color>>8) & 0xff) * x); unsigned int b = unsigned(float((color>>16) & 0xff) * x); return (0xff000000 | b | g<<8 | r<<16); } CMiniMap::CMiniMap(CGUI* pGUI) : IGUIObject(pGUI), m_TerrainTexture(0), m_TerrainData(0), m_MapSize(0), m_Terrain(0), m_TerrainDirty(true), m_MapScale(1.f), m_EntitiesDrawn(0), m_IndexArray(GL_STATIC_DRAW), m_VertexArray(GL_DYNAMIC_DRAW), m_NextBlinkTime(0.0), m_PingDuration(25.0), m_BlinkState(false), m_WaterHeight(0.0) { AddSetting("tooltip"); AddSetting("tooltip_style"); m_Clicking = false; m_MouseHovering = false; // Register Relax NG validator CXeromyces::AddValidator(g_VFS, "pathfinder", "simulation/data/pathfinder.rng"); // Get the maximum height for unit passage in water. CParamNode externalParamNode; CParamNode::LoadXML(externalParamNode, L"simulation/data/pathfinder.xml", "pathfinder"); const CParamNode pathingSettings = externalParamNode.GetChild("Pathfinder").GetChild("PassabilityClasses"); if (pathingSettings.GetChild("default").IsOk() && pathingSettings.GetChild("default").GetChild("MaxWaterDepth").IsOk()) m_ShallowPassageHeight = pathingSettings.GetChild("default").GetChild("MaxWaterDepth").ToFloat(); else m_ShallowPassageHeight = 0.0f; m_AttributePos.type = GL_FLOAT; m_AttributePos.elems = 2; m_VertexArray.AddAttribute(&m_AttributePos); m_AttributeColor.type = GL_UNSIGNED_BYTE; m_AttributeColor.elems = 4; m_VertexArray.AddAttribute(&m_AttributeColor); m_VertexArray.SetNumVertices(MAX_ENTITIES_DRAWN); m_VertexArray.Layout(); m_IndexArray.SetNumVertices(MAX_ENTITIES_DRAWN); m_IndexArray.Layout(); VertexArrayIterator index = m_IndexArray.GetIterator(); for (u16 i = 0; i < MAX_ENTITIES_DRAWN; ++i) *index++ = i; m_IndexArray.Upload(); m_IndexArray.FreeBackingStore(); VertexArrayIterator attrPos = m_AttributePos.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); for (u16 i = 0; i < MAX_ENTITIES_DRAWN; ++i) { (*attrColor)[0] = 0; (*attrColor)[1] = 0; (*attrColor)[2] = 0; (*attrColor)[3] = 0; ++attrColor; (*attrPos)[0] = -10000.0f; (*attrPos)[1] = -10000.0f; ++attrPos; } m_VertexArray.Upload(); double blinkDuration = 1.0; // Tests won't have config initialised if (CConfigDB::IsInitialised()) { CFG_GET_VAL("gui.session.minimap.pingduration", m_PingDuration); CFG_GET_VAL("gui.session.minimap.blinkduration", blinkDuration); } m_HalfBlinkDuration = blinkDuration/2; } CMiniMap::~CMiniMap() { Destroy(); } void CMiniMap::HandleMessage(SGUIMessage& Message) { switch (Message.type) { case GUIM_MOUSE_PRESS_LEFT: if (m_MouseHovering) { SetCameraPos(); m_Clicking = true; } break; case GUIM_MOUSE_RELEASE_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPos(); m_Clicking = false; break; case GUIM_MOUSE_DBLCLICK_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPos(); m_Clicking = false; break; case GUIM_MOUSE_ENTER: m_MouseHovering = true; break; case GUIM_MOUSE_LEAVE: m_Clicking = false; m_MouseHovering = false; break; case GUIM_MOUSE_RELEASE_RIGHT: CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 1); break; case GUIM_MOUSE_DBLCLICK_RIGHT: CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 2); break; case GUIM_MOUSE_MOTION: if (m_MouseHovering && m_Clicking) SetCameraPos(); break; case GUIM_MOUSE_WHEEL_DOWN: case GUIM_MOUSE_WHEEL_UP: Message.Skip(); break; default: break; } } bool CMiniMap::MouseOver() { // Get the mouse position. CPos mousePos = GetMousePos(); // Get the position of the center of the minimap. CPos minimapCenter = CPos(m_CachedActualSize.left + m_CachedActualSize.GetWidth() / 2.0, m_CachedActualSize.bottom - m_CachedActualSize.GetHeight() / 2.0); // Take the magnitude of the difference of the mouse position and minimap center. double distFromCenter = sqrt(pow((mousePos.x - minimapCenter.x), 2) + pow((mousePos.y - minimapCenter.y), 2)); // If the distance is less then the radius of the minimap (half the width) the mouse is over the minimap. if (distFromCenter < m_CachedActualSize.GetWidth() / 2.0) return true; else return false; } void CMiniMap::GetMouseWorldCoordinates(float& x, float& z) { // Determine X and Z according to proportion of mouse position and minimap CPos mousePos = GetMousePos(); float px = (mousePos.x - m_CachedActualSize.left) / m_CachedActualSize.GetWidth(); float py = (m_CachedActualSize.bottom - mousePos.y) / m_CachedActualSize.GetHeight(); float angle = GetAngle(); // Scale world coordinates for shrunken square map x = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(px-0.5) - sin(angle)*(py-0.5)) + 0.5); z = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(py-0.5) + sin(angle)*(px-0.5)) + 0.5); } void CMiniMap::SetCameraPos() { CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); CVector3D target; GetMouseWorldCoordinates(target.X, target.Z); target.Y = terrain->GetExactGroundLevel(target.X, target.Z); g_Game->GetView()->MoveCameraTarget(target); } float CMiniMap::GetAngle() { CVector3D cameraIn = m_Camera->m_Orientation.GetIn(); return -atan2(cameraIn.X, cameraIn.Z); } void CMiniMap::FireWorldClickEvent(int UNUSED(button), int UNUSED(clicks)) { JSContext* cx = g_GUI->GetActiveGUI()->GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); float x, z; GetMouseWorldCoordinates(x, z); JS::RootedValue coords(cx); g_GUI->GetActiveGUI()->GetScriptInterface()->CreateObject(&coords, "x", x, "z", z); JS::AutoValueVector paramData(cx); paramData.append(coords); ScriptEvent("worldclick", paramData); } // This sets up and draws the rectangle on the minimap // which represents the view of the camera in the world. void CMiniMap::DrawViewRect(CMatrix3D transform) { // Compute the camera frustum intersected with a fixed-height plane. // Use the water height as a fixed base height, which should be the lowest we can go float h = g_Renderer.GetWaterManager()->m_WaterHeight; const float width = m_CachedActualSize.GetWidth(); const float height = m_CachedActualSize.GetHeight(); const float invTileMapSize = 1.0f / float(TERRAIN_TILE_SIZE * m_MapSize); CVector3D hitPt[4]; hitPt[0] = m_Camera->GetWorldCoordinates(0, g_Renderer.GetHeight(), h); hitPt[1] = m_Camera->GetWorldCoordinates(g_Renderer.GetWidth(), g_Renderer.GetHeight(), h); hitPt[2] = m_Camera->GetWorldCoordinates(g_Renderer.GetWidth(), 0, h); hitPt[3] = m_Camera->GetWorldCoordinates(0, 0, h); float ViewRect[4][2]; for (int i = 0; i < 4; ++i) { // convert to minimap space ViewRect[i][0] = (width * hitPt[i].X * invTileMapSize); ViewRect[i][1] = (height * hitPt[i].Z * invTileMapSize); } float viewVerts[] = { ViewRect[0][0], -ViewRect[0][1], ViewRect[1][0], -ViewRect[1][1], ViewRect[2][0], -ViewRect[2][1], ViewRect[3][0], -ViewRect[3][1] }; // Enable Scissoring to restrict the rectangle to only the minimap. glScissor( m_CachedActualSize.left * g_GuiScale, g_Renderer.GetHeight() - m_CachedActualSize.bottom * g_GuiScale, width * g_GuiScale, height * g_GuiScale); glEnable(GL_SCISSOR_TEST); glLineWidth(2.0f); CShaderDefines lineDefines; lineDefines.Add(str_MINIMAP_LINE, str_1); CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), lineDefines); tech->BeginPass(); CShaderProgramPtr shader = tech->GetShader(); shader->Uniform(str_transform, transform); shader->Uniform(str_color, 1.0f, 0.3f, 0.3f, 1.0f); shader->VertexPointer(2, GL_FLOAT, 0, viewVerts); shader->AssertPointersBound(); if (!g_Renderer.m_SkipSubmit) glDrawArrays(GL_LINE_LOOP, 0, 4); tech->EndPass(); glLineWidth(1.0f); glDisable(GL_SCISSOR_TEST); } struct MinimapUnitVertex { u8 r, g, b, a; float x, y; }; // Adds a vertex to the passed VertexArray static void inline addVertex(const MinimapUnitVertex& v, VertexArrayIterator& attrColor, VertexArrayIterator& attrPos) { (*attrColor)[0] = v.r; (*attrColor)[1] = v.g; (*attrColor)[2] = v.b; (*attrColor)[3] = v.a; ++attrColor; (*attrPos)[0] = v.x; (*attrPos)[1] = v.y; ++attrPos; } void CMiniMap::DrawTexture(CShaderProgramPtr shader, float coordMax, float angle, float x, float y, float x2, float y2, float z) { // Rotate the texture coordinates (0,0)-(coordMax,coordMax) around their center point (m,m) // Scale square maps to fit in circular minimap area const float s = sin(angle) * m_MapScale; const float c = cos(angle) * m_MapScale; const float m = coordMax / 2.f; float quadTex[] = { m*(-c + s + 1.f), m*(-c + -s + 1.f), m*(c + s + 1.f), m*(-c + s + 1.f), m*(c + -s + 1.f), m*(c + s + 1.f), m*(c + -s + 1.f), m*(c + s + 1.f), m*(-c + -s + 1.f), m*(c + -s + 1.f), m*(-c + s + 1.f), m*(-c + -s + 1.f) }; float quadVerts[] = { x, y, z, x2, y, z, x2, y2, z, x2, y2, z, x, y2, z, x, y, z }; shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 0, quadTex); shader->VertexPointer(3, GL_FLOAT, 0, quadVerts); shader->AssertPointersBound(); if (!g_Renderer.m_SkipSubmit) glDrawArrays(GL_TRIANGLES, 0, 6); } // TODO: render the minimap in a framebuffer and just draw the frambuffer texture // most of the time, updating the framebuffer twice a frame. // Here it updates as ping-pong either texture or vertex array each sec to lower gpu stalling // (those operations cause a gpu sync, which slows down the way gpu works) void CMiniMap::Draw() { PROFILE3("render minimap"); // The terrain isn't actually initialized until the map is loaded, which // happens when the game is started, so abort until then. if (!(GetGUI() && g_Game && g_Game->IsGameStarted())) return; CSimulation2* sim = g_Game->GetSimulation2(); CmpPtr cmpRangeManager(*sim, SYSTEM_ENTITY); ENSURE(cmpRangeManager); // Set our globals in case they hadn't been set before m_Camera = g_Game->GetView()->GetCamera(); m_Terrain = g_Game->GetWorld()->GetTerrain(); m_Width = (u32)(m_CachedActualSize.right - m_CachedActualSize.left); m_Height = (u32)(m_CachedActualSize.bottom - m_CachedActualSize.top); m_MapSize = m_Terrain->GetVerticesPerSide(); m_TextureSize = (GLsizei)round_up_to_pow2((size_t)m_MapSize); m_MapScale = (cmpRangeManager->GetLosCircular() ? 1.f : 1.414f); if (!m_TerrainTexture || g_GameRestarted) CreateTextures(); // only update 2x / second // (note: since units only move a few pixels per second on the minimap, // we can get away with infrequent updates; this is slow) // TODO: Update all but camera at same speed as simulation static double last_time; const double cur_time = timer_Time(); const bool doUpdate = cur_time - last_time > 0.5; if (doUpdate) { last_time = cur_time; if (m_TerrainDirty || m_WaterHeight != g_Renderer.GetWaterManager()->m_WaterHeight) RebuildTerrainTexture(); } const float x = m_CachedActualSize.left, y = m_CachedActualSize.bottom; const float x2 = m_CachedActualSize.right, y2 = m_CachedActualSize.top; const float z = GetBufferedZ(); const float texCoordMax = (float)(m_MapSize - 1) / (float)m_TextureSize; const float angle = GetAngle(); const float unitScale = (cmpRangeManager->GetLosCircular() ? 1.f : m_MapScale/2.f); // Disable depth updates to prevent apparent z-fighting-related issues // with some drivers causing units to get drawn behind the texture. glDepthMask(0); CShaderProgramPtr shader; CShaderTechniquePtr tech; CShaderDefines baseDefines; baseDefines.Add(str_MINIMAP_BASE, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), baseDefines); tech->BeginPass(); shader = tech->GetShader(); // Draw the main textured quad shader->BindTexture(str_baseTex, m_TerrainTexture); const CMatrix3D baseTransform = GetDefaultGuiMatrix(); CMatrix3D baseTextureTransform; baseTextureTransform.SetIdentity(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, baseTextureTransform); DrawTexture(shader, texCoordMax, angle, x, y, x2, y2, z); // Draw territory boundaries glEnable(GL_BLEND); CTerritoryTexture& territoryTexture = g_Game->GetView()->GetTerritoryTexture(); shader->BindTexture(str_baseTex, territoryTexture.GetTexture()); const CMatrix3D* territoryTransform = territoryTexture.GetMinimapTextureMatrix(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, *territoryTransform); DrawTexture(shader, 1.0f, angle, x, y, x2, y2, z); tech->EndPass(); // Draw the LOS quad in black, using alpha values from the LOS texture CLOSTexture& losTexture = g_Game->GetView()->GetLOSTexture(); CShaderDefines losDefines; losDefines.Add(str_MINIMAP_LOS, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), losDefines); tech->BeginPass(); shader = tech->GetShader(); shader->BindTexture(str_baseTex, losTexture.GetTexture()); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); const CMatrix3D* losTransform = losTexture.GetMinimapTextureMatrix(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, *losTransform); DrawTexture(shader, 1.0f, angle, x, y, x2, y2, z); tech->EndPass(); glDisable(GL_BLEND); PROFILE_START("minimap units"); CShaderDefines pointDefines; pointDefines.Add(str_MINIMAP_POINT, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), pointDefines); tech->BeginPass(); shader = tech->GetShader(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_pointSize, 3.f); CMatrix3D unitMatrix; unitMatrix.SetIdentity(); // Center the minimap on the origin of the axis of rotation. unitMatrix.Translate(-(x2 - x) / 2.f, -(y2 - y) / 2.f, 0.f); // Rotate the map. unitMatrix.RotateZ(angle); // Scale square maps to fit. unitMatrix.Scale(unitScale, unitScale, 1.f); // Move the minimap back to it's starting position. unitMatrix.Translate((x2 - x) / 2.f, (y2 - y) / 2.f, 0.f); // Move the minimap to it's final location. unitMatrix.Translate(x, y, z); // Apply the gui matrix. unitMatrix *= GetDefaultGuiMatrix(); // Load the transform into the shader. shader->Uniform(str_transform, unitMatrix); const float sx = (float)m_Width / ((m_MapSize - 1) * TERRAIN_TILE_SIZE); const float sy = (float)m_Height / ((m_MapSize - 1) * TERRAIN_TILE_SIZE); CSimulation2::InterfaceList ents = sim->GetEntitiesWithInterface(IID_Minimap); if (doUpdate) { VertexArrayIterator attrPos = m_AttributePos.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); m_EntitiesDrawn = 0; MinimapUnitVertex v; std::vector pingingVertices; pingingVertices.reserve(MAX_ENTITIES_DRAWN / 2); if (cur_time > m_NextBlinkTime) { m_BlinkState = !m_BlinkState; m_NextBlinkTime = cur_time + m_HalfBlinkDuration; } entity_pos_t posX, posZ; for (CSimulation2::InterfaceList::const_iterator it = ents.begin(); it != ents.end(); ++it) { ICmpMinimap* cmpMinimap = static_cast(it->second); if (cmpMinimap->GetRenderData(v.r, v.g, v.b, posX, posZ)) { ICmpRangeManager::ELosVisibility vis = cmpRangeManager->GetLosVisibility(it->first, g_Game->GetSimulation2()->GetSimContext().GetCurrentDisplayedPlayer()); if (vis != ICmpRangeManager::VIS_HIDDEN) { v.a = 255; v.x = posX.ToFloat() * sx; v.y = -posZ.ToFloat() * sy; // Check minimap pinging to indicate something if (m_BlinkState && cmpMinimap->CheckPing(cur_time, m_PingDuration)) { v.r = 255; // ping color is white v.g = 255; v.b = 255; pingingVertices.push_back(v); } else { addVertex(v, attrColor, attrPos); ++m_EntitiesDrawn; } } } } // Add the pinged vertices at the end, so they are drawn on top for (size_t v = 0; v < pingingVertices.size(); ++v) { addVertex(pingingVertices[v], attrColor, attrPos); ++m_EntitiesDrawn; } ENSURE(m_EntitiesDrawn < MAX_ENTITIES_DRAWN); m_VertexArray.Upload(); } m_VertexArray.PrepareForRendering(); if (m_EntitiesDrawn > 0) { #if !CONFIG2_GLES if (g_Renderer.GetRenderPath() == CRenderer::RP_SHADER) glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); #endif u8* indexBase = m_IndexArray.Bind(); u8* base = m_VertexArray.Bind(); const GLsizei stride = (GLsizei)m_VertexArray.GetStride(); shader->VertexPointer(2, GL_FLOAT, stride, base + m_AttributePos.offset); shader->ColorPointer(4, GL_UNSIGNED_BYTE, stride, base + m_AttributeColor.offset); shader->AssertPointersBound(); if (!g_Renderer.m_SkipSubmit) glDrawElements(GL_POINTS, (GLsizei)(m_EntitiesDrawn), GL_UNSIGNED_SHORT, indexBase); g_Renderer.GetStats().m_DrawCalls++; CVertexBuffer::Unbind(); #if !CONFIG2_GLES if (g_Renderer.GetRenderPath() == CRenderer::RP_SHADER) glDisable(GL_VERTEX_PROGRAM_POINT_SIZE); #endif } tech->EndPass(); DrawViewRect(unitMatrix); PROFILE_END("minimap units"); // Reset depth mask glDepthMask(1); } void CMiniMap::CreateTextures() { Destroy(); // Create terrain texture glGenTextures(1, &m_TerrainTexture); g_Renderer.BindTexture(0, m_TerrainTexture); // Initialise texture with solid black, for the areas we don't // overwrite with glTexSubImage2D later u32* texData = new u32[m_TextureSize * m_TextureSize]; for (ssize_t i = 0; i < m_TextureSize * m_TextureSize; ++i) texData[i] = 0xFF000000; glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_TextureSize, m_TextureSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, texData); delete[] texData; m_TerrainData = new u32[(m_MapSize - 1) * (m_MapSize - 1)]; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // Rebuild and upload both of them RebuildTerrainTexture(); } void CMiniMap::RebuildTerrainTexture() { u32 x = 0; u32 y = 0; u32 w = m_MapSize - 1; u32 h = m_MapSize - 1; m_WaterHeight = g_Renderer.GetWaterManager()->m_WaterHeight; m_TerrainDirty = false; for (u32 j = 0; j < h; ++j) { u32* dataPtr = m_TerrainData + ((y + j) * (m_MapSize - 1)) + x; for (u32 i = 0; i < w; ++i) { float avgHeight = ( m_Terrain->GetVertexGroundLevel((int)i, (int)j) + m_Terrain->GetVertexGroundLevel((int)i+1, (int)j) + m_Terrain->GetVertexGroundLevel((int)i, (int)j+1) + m_Terrain->GetVertexGroundLevel((int)i+1, (int)j+1) ) / 4.0f; if (avgHeight < m_WaterHeight && avgHeight > m_WaterHeight - m_ShallowPassageHeight) { // shallow water *dataPtr++ = 0xffc09870; } else if (avgHeight < m_WaterHeight) { // Set water as constant color for consistency on different maps *dataPtr++ = 0xffa07850; } else { int hmap = ((int)m_Terrain->GetHeightMap()[(y + j) * m_MapSize + x + i]) >> 8; int val = (hmap / 3) + 170; u32 color = 0xFFFFFFFF; CMiniPatch* mp = m_Terrain->GetTile(x + i, y + j); if (mp) { CTerrainTextureEntry* tex = mp->GetTextureEntry(); if (tex) { // If the texture can't be loaded yet, set the dirty flags // so we'll try regenerating the terrain texture again soon if(!tex->GetTexture()->TryLoad()) m_TerrainDirty = true; color = tex->GetBaseColor(); } } *dataPtr++ = ScaleColor(color, float(val) / 255.0f); } } } // Upload the texture g_Renderer.BindTexture(0, m_TerrainTexture); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_MapSize - 1, m_MapSize - 1, GL_RGBA, GL_UNSIGNED_BYTE, m_TerrainData); } void CMiniMap::Destroy() { if (m_TerrainTexture) { glDeleteTextures(1, &m_TerrainTexture); m_TerrainTexture = 0; } SAFE_ARRAY_DELETE(m_TerrainData); } Index: ps/trunk/source/gui/MiniMap.h =================================================================== --- ps/trunk/source/gui/MiniMap.h (revision 22604) +++ ps/trunk/source/gui/MiniMap.h (revision 22605) @@ -1,114 +1,114 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_MINIMAP #define INCLUDED_MINIMAP #include "gui/GUI.h" #include "renderer/VertexArray.h" - class CCamera; +class CMatrix3D; class CTerrain; class CMiniMap : public IGUIObject { GUI_OBJECT(CMiniMap) public: CMiniMap(CGUI* pGUI); virtual ~CMiniMap(); protected: virtual void Draw(); /** * @see IGUIObject#HandleMessage() */ virtual void HandleMessage(SGUIMessage& Message); /** * @see IGUIObject#MouseOver() */ virtual bool MouseOver(); // create the minimap textures void CreateTextures(); // rebuild the terrain texture map void RebuildTerrainTexture(); // destroy and free any memory and textures void Destroy(); void SetCameraPos(); void FireWorldClickEvent(int button, int clicks); // the terrain we are mini-mapping const CTerrain* m_Terrain; const CCamera* m_Camera; //Whether or not the mouse is currently down bool m_Clicking; // minimap texture handles GLuint m_TerrainTexture; // texture data u32* m_TerrainData; // whether we need to regenerate the terrain texture bool m_TerrainDirty; ssize_t m_Width, m_Height; // map size ssize_t m_MapSize; // texture size GLsizei m_TextureSize; // 1.f if map is circular or 1.414f if square (to shrink it inside the circle) float m_MapScale; // maximal water height to allow the passage of a unit (for underwater shallows). float m_ShallowPassageHeight; float m_WaterHeight; void DrawTexture(CShaderProgramPtr shader, float coordMax, float angle, float x, float y, float x2, float y2, float z); void DrawViewRect(CMatrix3D transform); void GetMouseWorldCoordinates(float& x, float& z); float GetAngle(); VertexIndexArray m_IndexArray; VertexArray m_VertexArray; VertexArray::Attribute m_AttributePos; VertexArray::Attribute m_AttributeColor; size_t m_EntitiesDrawn; double m_PingDuration; double m_HalfBlinkDuration; double m_NextBlinkTime; bool m_BlinkState; }; #endif // INCLUDED_MINIMAP Index: ps/trunk/source/ps/CConsole.cpp =================================================================== --- ps/trunk/source/ps/CConsole.cpp (revision 22604) +++ ps/trunk/source/ps/CConsole.cpp (revision 22605) @@ -1,695 +1,696 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * Implements the in-game console with scripting support. */ #include "precompiled.h" #include #include "CConsole.h" #include "graphics/FontMetrics.h" #include "graphics/ShaderManager.h" #include "graphics/TextRenderer.h" -#include "gui/GUIutil.h" +#include "gui/CGUI.h" #include "gui/GUIManager.h" +#include "gui/GUIMatrix.h" #include "lib/ogl.h" #include "lib/sysdep/clipboard.h" #include "lib/timer.h" #include "lib/utf8.h" #include "maths/MathUtil.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptInterface.h" CConsole* g_Console = 0; CConsole::CConsole() { m_bToggle = false; m_bVisible = false; m_fVisibleFrac = 0.0f; m_szBuffer = new wchar_t[CONSOLE_BUFFER_SIZE]; FlushBuffer(); m_iMsgHistPos = 1; m_charsPerPage = 0; m_prevTime = 0.0; m_bCursorVisState = true; m_cursorBlinkRate = 0.5; InsertMessage("[ 0 A.D. Console v0.14 ]"); InsertMessage(""); } CConsole::~CConsole() { delete[] m_szBuffer; } void CConsole::SetSize(float X, float Y, float W, float H) { m_fX = X; m_fY = Y; m_fWidth = W; m_fHeight = H; } void CConsole::UpdateScreenSize(int w, int h) { float height = h * 0.6f; SetSize(0, 0, w / g_GuiScale, height / g_GuiScale); } void CConsole::ToggleVisible() { m_bToggle = true; m_bVisible = !m_bVisible; // TODO: this should be based on input focus, not visibility if (m_bVisible) SDL_StartTextInput(); else SDL_StopTextInput(); } void CConsole::SetVisible(bool visible) { if (visible != m_bVisible) m_bToggle = true; m_bVisible = visible; if (visible) { m_prevTime = 0.0; m_bCursorVisState = false; } } void CConsole::SetCursorBlinkRate(double rate) { m_cursorBlinkRate = rate; } void CConsole::FlushBuffer() { // Clear the buffer and set the cursor and length to 0 memset(m_szBuffer, '\0', sizeof(wchar_t) * CONSOLE_BUFFER_SIZE); m_iBufferPos = m_iBufferLength = 0; } void CConsole::Update(const float deltaRealTime) { if(m_bToggle) { const float AnimateTime = .30f; const float Delta = deltaRealTime / AnimateTime; if(m_bVisible) { m_fVisibleFrac += Delta; if(m_fVisibleFrac > 1.0f) { m_fVisibleFrac = 1.0f; m_bToggle = false; } } else { m_fVisibleFrac -= Delta; if(m_fVisibleFrac < 0.0f) { m_fVisibleFrac = 0.0f; m_bToggle = false; } } } } //Render Manager. void CConsole::Render() { if (! (m_bVisible || m_bToggle) ) return; PROFILE3_GPU("console"); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); CShaderTechniquePtr solidTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid); solidTech->BeginPass(); CShaderProgramPtr solidShader = solidTech->GetShader(); CMatrix3D transform = GetDefaultGuiMatrix(); // animation: slide in from top of screen const float DeltaY = (1.0f - m_fVisibleFrac) * m_fHeight; transform.PostTranslate(m_fX, m_fY - DeltaY, 0.0f); // move to window position solidShader->Uniform(str_transform, transform); DrawWindow(solidShader); solidTech->EndPass(); CShaderTechniquePtr textTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text); textTech->BeginPass(); CTextRenderer textRenderer(textTech->GetShader()); textRenderer.Font(CStrIntern(CONSOLE_FONT)); textRenderer.SetTransform(transform); DrawHistory(textRenderer); DrawBuffer(textRenderer); textRenderer.Render(); textTech->EndPass(); glDisable(GL_BLEND); } void CConsole::DrawWindow(CShaderProgramPtr& shader) { float boxVerts[] = { m_fWidth, 0.0f, 1.0f, 0.0f, 1.0f, m_fHeight-1.0f, m_fWidth, m_fHeight-1.0f }; shader->VertexPointer(2, GL_FLOAT, 0, boxVerts); // Draw Background // Set the color to a translucent blue shader->Uniform(str_color, 0.0f, 0.0f, 0.5f, 0.6f); shader->AssertPointersBound(); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); // Draw Border // Set the color to a translucent yellow shader->Uniform(str_color, 0.5f, 0.5f, 0.0f, 0.6f); shader->AssertPointersBound(); glDrawArrays(GL_LINE_LOOP, 0, 4); if (m_fHeight > m_iFontHeight + 4) { float lineVerts[] = { 0.0f, m_fHeight - (float)m_iFontHeight - 4.0f, m_fWidth, m_fHeight - (float)m_iFontHeight - 4.0f }; shader->VertexPointer(2, GL_FLOAT, 0, lineVerts); shader->AssertPointersBound(); glDrawArrays(GL_LINES, 0, 2); } } void CConsole::DrawHistory(CTextRenderer& textRenderer) { int i = 1; std::deque::iterator Iter; //History iterator std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory textRenderer.Color(1.0f, 1.0f, 1.0f); for (Iter = m_deqMsgHistory.begin(); Iter != m_deqMsgHistory.end() && (((i - m_iMsgHistPos + 1) * m_iFontHeight) < m_fHeight); ++Iter) { if (i >= m_iMsgHistPos) textRenderer.Put(9.0f, m_fHeight - (float)m_iFontOffset - (float)m_iFontHeight * (i - m_iMsgHistPos + 1), Iter->c_str()); i++; } } // Renders the buffer to the screen. void CConsole::DrawBuffer(CTextRenderer& textRenderer) { if (m_fHeight < m_iFontHeight) return; CMatrix3D savedTransform = textRenderer.GetTransform(); textRenderer.Translate(2.0f, m_fHeight - (float)m_iFontOffset + 1.0f, 0.0f); textRenderer.Color(1.0f, 1.0f, 0.0f); textRenderer.PutAdvance(L"]"); textRenderer.Color(1.0f, 1.0f, 1.0f); if (m_iBufferPos == 0) DrawCursor(textRenderer); for (int i = 0; i < m_iBufferLength; i++) { textRenderer.PrintfAdvance(L"%lc", m_szBuffer[i]); if (m_iBufferPos-1 == i) DrawCursor(textRenderer); } textRenderer.SetTransform(savedTransform); } void CConsole::DrawCursor(CTextRenderer& textRenderer) { if (m_cursorBlinkRate > 0.0) { // check if the cursor visibility state needs to be changed double currTime = timer_Time(); if ((currTime - m_prevTime) >= m_cursorBlinkRate) { m_bCursorVisState = !m_bCursorVisState; m_prevTime = currTime; } } else { // Should always be visible m_bCursorVisState = true; } if(m_bCursorVisState) { // Slightly translucent yellow textRenderer.Color(1.0f, 1.0f, 0.0f, 0.8f); // Cursor character is chosen to be an underscore textRenderer.Put(0.0f, 0.0f, L"_"); // Revert to the standard text color textRenderer.Color(1.0f, 1.0f, 1.0f); } } //Inserts a character into the buffer. void CConsole::InsertChar(const int szChar, const wchar_t cooked) { static int iHistoryPos = -1; if (!m_bVisible) return; switch (szChar) { case SDLK_RETURN: iHistoryPos = -1; m_iMsgHistPos = 1; ProcessBuffer(m_szBuffer); FlushBuffer(); return; case SDLK_TAB: // Auto Complete return; case SDLK_BACKSPACE: if (IsEmpty() || IsBOB()) return; if (m_iBufferPos == m_iBufferLength) m_szBuffer[m_iBufferPos - 1] = '\0'; else { for (int j = m_iBufferPos-1; j < m_iBufferLength-1; j++) m_szBuffer[j] = m_szBuffer[j+1]; // move chars to left m_szBuffer[m_iBufferLength-1] = '\0'; } m_iBufferPos--; m_iBufferLength--; return; case SDLK_DELETE: if (IsEmpty() || IsEOB()) return; if (m_iBufferPos == m_iBufferLength-1) { m_szBuffer[m_iBufferPos] = '\0'; m_iBufferLength--; } else { if (g_keys[SDLK_RCTRL] || g_keys[SDLK_LCTRL]) { // Make Ctrl-Delete delete up to end of line m_szBuffer[m_iBufferPos] = '\0'; m_iBufferLength = m_iBufferPos; } else { // Delete just one char and move the others left for(int j=m_iBufferPos; j lock(m_Mutex); // needed for safe access to m_deqMsgHistory int linesShown = (int)m_fHeight/m_iFontHeight - 4; m_iMsgHistPos = clamp((int)m_deqMsgHistory.size() - linesShown, 1, (int)m_deqMsgHistory.size()); } else { m_iBufferPos = 0; } return; case SDLK_END: if (g_keys[SDLK_RCTRL] || g_keys[SDLK_LCTRL]) { m_iMsgHistPos = 1; } else { m_iBufferPos = m_iBufferLength; } return; case SDLK_LEFT: if (m_iBufferPos) m_iBufferPos--; return; case SDLK_RIGHT: if (m_iBufferPos != m_iBufferLength) m_iBufferPos++; return; // BEGIN: Buffer History Lookup case SDLK_UP: if (m_deqBufHistory.size() && iHistoryPos != (int)m_deqBufHistory.size() - 1) { iHistoryPos++; SetBuffer(m_deqBufHistory.at(iHistoryPos).c_str()); m_iBufferPos = m_iBufferLength; } return; case SDLK_DOWN: if (m_deqBufHistory.size()) { if (iHistoryPos > 0) { iHistoryPos--; SetBuffer(m_deqBufHistory.at(iHistoryPos).c_str()); m_iBufferPos = m_iBufferLength; } else if (iHistoryPos == 0) { iHistoryPos--; FlushBuffer(); } } return; // END: Buffer History Lookup // BEGIN: Message History Lookup case SDLK_PAGEUP: { std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory if (m_iMsgHistPos != (int)m_deqMsgHistory.size()) m_iMsgHistPos++; return; } case SDLK_PAGEDOWN: if (m_iMsgHistPos != 1) m_iMsgHistPos--; return; // END: Message History Lookup default: //Insert a character if (IsFull()) return; if (cooked == 0) return; if (IsEOB()) //are we at the end of the buffer? m_szBuffer[m_iBufferPos] = cooked; //cat char onto end else { //we need to insert int i; for(i=m_iBufferLength; i>m_iBufferPos; i--) m_szBuffer[i] = m_szBuffer[i-1]; // move chars to right m_szBuffer[i] = cooked; } m_iBufferPos++; m_iBufferLength++; return; } } void CConsole::InsertMessage(const std::string& message) { // (TODO: this text-wrapping is rubbish since we now use variable-width fonts) //Insert newlines to wraparound text where needed std::wstring wrapAround = wstring_from_utf8(message.c_str()); std::wstring newline(L"\n"); size_t oldNewline=0; size_t distance; //make sure everything has been initialized if ( m_charsPerPage != 0 ) { while ( oldNewline+m_charsPerPage < wrapAround.length() ) { distance = wrapAround.find(newline, oldNewline) - oldNewline; if ( distance > m_charsPerPage ) { oldNewline += m_charsPerPage; wrapAround.insert( oldNewline++, newline ); } else oldNewline += distance+1; } } // Split into lines and add each one individually oldNewline = 0; { std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory while ( (distance = wrapAround.find(newline, oldNewline)) != wrapAround.npos) { distance -= oldNewline; m_deqMsgHistory.push_front(wrapAround.substr(oldNewline, distance)); oldNewline += distance+1; } m_deqMsgHistory.push_front(wrapAround.substr(oldNewline)); } } const wchar_t* CConsole::GetBuffer() { m_szBuffer[m_iBufferLength] = 0; return( m_szBuffer ); } void CConsole::SetBuffer(const wchar_t* szMessage) { int oldBufferPos = m_iBufferPos; // remember since FlushBuffer will set it to 0 FlushBuffer(); wcsncpy(m_szBuffer, szMessage, CONSOLE_BUFFER_SIZE); m_szBuffer[CONSOLE_BUFFER_SIZE-1] = 0; m_iBufferLength = (int)wcslen(m_szBuffer); m_iBufferPos = std::min(oldBufferPos, m_iBufferLength); } void CConsole::UseHistoryFile(const VfsPath& filename, int max_history_lines) { m_MaxHistoryLines = max_history_lines; m_sHistoryFile = filename; LoadHistory(); } void CConsole::ProcessBuffer(const wchar_t* szLine) { shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); JSContext* cx = pScriptInterface->GetContext(); JSAutoRequest rq(cx); if (szLine == NULL) return; if (wcslen(szLine) <= 0) return; ENSURE(wcslen(szLine) < CONSOLE_BUFFER_SIZE); m_deqBufHistory.push_front(szLine); SaveHistory(); // Do this each line for the moment; if a script causes // a crash it's a useful record. // Process it as JavaScript JS::RootedValue rval(cx); pScriptInterface->Eval(szLine, &rval); if (!rval.isUndefined()) InsertMessage(pScriptInterface->ToString(&rval)); } void CConsole::LoadHistory() { // note: we don't care if this file doesn't exist or can't be read; // just don't load anything in that case. // do this before LoadFile to avoid an error message if file not found. if (!VfsFileExists(m_sHistoryFile)) return; shared_ptr buf; size_t buflen; if (g_VFS->LoadFile(m_sHistoryFile, buf, buflen) < 0) return; CStr bytes ((char*)buf.get(), buflen); CStrW str (bytes.FromUTF8()); size_t pos = 0; while (pos != CStrW::npos) { pos = str.find('\n'); if (pos != CStrW::npos) { if (pos > 0) m_deqBufHistory.push_front(str.Left(str[pos-1] == '\r' ? pos - 1 : pos)); str = str.substr(pos + 1); } else if (str.length() > 0) m_deqBufHistory.push_front(str); } } void CConsole::SaveHistory() { WriteBuffer buffer; const int linesToSkip = (int)m_deqBufHistory.size() - m_MaxHistoryLines; std::deque::reverse_iterator it = m_deqBufHistory.rbegin(); if(linesToSkip > 0) std::advance(it, linesToSkip); for (; it != m_deqBufHistory.rend(); ++it) { CStr8 line = CStrW(*it).ToUTF8(); buffer.Append(line.data(), line.length()); static const char newline = '\n'; buffer.Append(&newline, 1); } g_VFS->CreateFile(m_sHistoryFile, buffer.Data(), buffer.Size()); } static bool isUnprintableChar(SDL_Keysym key) { switch (key.sym) { // We want to allow some, which are handled specially case SDLK_RETURN: case SDLK_TAB: case SDLK_BACKSPACE: case SDLK_DELETE: case SDLK_HOME: case SDLK_END: case SDLK_LEFT: case SDLK_RIGHT: case SDLK_UP: case SDLK_DOWN: case SDLK_PAGEUP: case SDLK_PAGEDOWN: return false; // Ignore the others default: return true; } } InReaction conInputHandler(const SDL_Event_* ev) { if ((int)ev->ev.type == SDL_HOTKEYDOWN) { std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "console.toggle") { g_Console->ToggleVisible(); return IN_HANDLED; } else if (g_Console->IsActive() && hotkey == "copy") { sys_clipboard_set(g_Console->GetBuffer()); return IN_HANDLED; } else if (g_Console->IsActive() && hotkey == "paste") { wchar_t* text = sys_clipboard_get(); if (text) { for (wchar_t* c = text; *c; c++) g_Console->InsertChar(0, *c); sys_clipboard_free(text); } return IN_HANDLED; } } if (!g_Console->IsActive()) return IN_PASS; // In SDL2, we no longer get Unicode wchars via SDL_Keysym // we use text input events instead and they provide UTF-8 chars if (ev->ev.type == SDL_TEXTINPUT && !HotkeyIsPressed("console.toggle")) { // TODO: this could be more efficient with an interface to insert UTF-8 strings directly std::wstring wstr = wstring_from_utf8(ev->ev.text.text); for (size_t i = 0; i < wstr.length(); ++i) g_Console->InsertChar(0, wstr[i]); return IN_HANDLED; } // TODO: text editing events for IME support if (ev->ev.type != SDL_KEYDOWN) return IN_PASS; int sym = ev->ev.key.keysym.sym; // Stop unprintable characters (ctrl+, alt+ and escape), // also prevent ` and/or ~ appearing in console every time it's toggled. if (!isUnprintableChar(ev->ev.key.keysym) && !HotkeyIsPressed("console.toggle")) { g_Console->InsertChar(sym, 0); return IN_HANDLED; } return IN_PASS; } Index: ps/trunk/source/ps/ProfileViewer.cpp =================================================================== --- ps/trunk/source/ps/ProfileViewer.cpp (revision 22604) +++ ps/trunk/source/ps/ProfileViewer.cpp (revision 22605) @@ -1,623 +1,623 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * Implementation of profile display (containing only display routines, * the data model(s) are implemented elsewhere). */ #include "precompiled.h" -#include -#include - #include "ProfileViewer.h" #include "graphics/FontMetrics.h" -#include "gui/GUIutil.h" #include "graphics/ShaderManager.h" #include "graphics/TextRenderer.h" +#include "gui/GUIMatrix.h" +#include "lib/external_libraries/libsdl.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Hotkey.h" #include "ps/Profile.h" -#include "lib/external_libraries/libsdl.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptInterface.h" +#include +#include + extern int g_xres, g_yres; struct CProfileViewerInternals { NONCOPYABLE(CProfileViewerInternals); // because of the ofstream public: CProfileViewerInternals() {} /// Whether the profiling display is currently visible bool profileVisible; /// List of root tables std::vector rootTables; /// Path from a root table (path[0]) to the currently visible table (path[size-1]) std::vector path; /// Helper functions void TableIsDeleted(AbstractProfileTable* table); void NavigateTree(int id); /// File for saved profile output (reset when the game is restarted) std::ofstream outputStream; }; /////////////////////////////////////////////////////////////////////////////////////////////// // AbstractProfileTable implementation AbstractProfileTable::~AbstractProfileTable() { if (CProfileViewer::IsInitialised()) { g_ProfileViewer.m->TableIsDeleted(this); } } /////////////////////////////////////////////////////////////////////////////////////////////// // CProfileViewer implementation // AbstractProfileTable got deleted, make sure we have no dangling pointers void CProfileViewerInternals::TableIsDeleted(AbstractProfileTable* table) { for(int idx = (int)rootTables.size()-1; idx >= 0; --idx) { if (rootTables[idx] == table) rootTables.erase(rootTables.begin() + idx); } for(size_t idx = 0; idx < path.size(); ++idx) { if (path[idx] != table) continue; path.erase(path.begin() + idx, path.end()); if (path.size() == 0) profileVisible = false; } } // Move into child tables or return to parent tables based on the given number void CProfileViewerInternals::NavigateTree(int id) { if (id == 0) { if (path.size() > 1) path.pop_back(); } else { AbstractProfileTable* table = path[path.size() - 1]; size_t numrows = table->GetNumberRows(); for(size_t row = 0; row < numrows; ++row) { AbstractProfileTable* child = table->GetChild(row); if (!child) continue; --id; if (id == 0) { path.push_back(child); break; } } } } // Construction/Destruction CProfileViewer::CProfileViewer() { m = new CProfileViewerInternals; m->profileVisible = false; } CProfileViewer::~CProfileViewer() { delete m; } // Render void CProfileViewer::RenderProfile() { if (!m->profileVisible) return; if (!m->path.size()) { m->profileVisible = false; return; } PROFILE3_GPU("profile viewer"); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); AbstractProfileTable* table = m->path[m->path.size() - 1]; const std::vector& columns = table->GetColumns(); size_t numrows = table->GetNumberRows(); CStrIntern font_name("mono-stroke-10"); CFontMetrics font(font_name); int lineSpacing = font.GetLineSpacing(); // Render background GLint estimate_height; GLint estimate_width; estimate_width = 50; for(size_t i = 0; i < columns.size(); ++i) estimate_width += (GLint)columns[i].width; estimate_height = 3 + (GLint)numrows; if (m->path.size() > 1) estimate_height += 2; estimate_height = lineSpacing*estimate_height; CShaderTechniquePtr solidTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid); solidTech->BeginPass(); CShaderProgramPtr solidShader = solidTech->GetShader(); solidShader->Uniform(str_color, 0.0f, 0.0f, 0.0f, 0.5f); CMatrix3D transform = GetDefaultGuiMatrix(); solidShader->Uniform(str_transform, transform); float backgroundVerts[] = { (float)estimate_width, 0.0f, 0.0f, 0.0f, 0.0f, (float)estimate_height, 0.0f, (float)estimate_height, (float)estimate_width, (float)estimate_height, (float)estimate_width, 0.0f }; solidShader->VertexPointer(2, GL_FLOAT, 0, backgroundVerts); solidShader->AssertPointersBound(); glDrawArrays(GL_TRIANGLES, 0, 6); transform.PostTranslate(22.0f, lineSpacing*3.0f, 0.0f); solidShader->Uniform(str_transform, transform); // Draw row backgrounds for (size_t row = 0; row < numrows; ++row) { if (row % 2) solidShader->Uniform(str_color, 1.0f, 1.0f, 1.0f, 0.1f); else solidShader->Uniform(str_color, 0.0f, 0.0f, 0.0f, 0.1f); float rowVerts[] = { -22.f, 2.f, estimate_width-22.f, 2.f, estimate_width-22.f, 2.f-lineSpacing, estimate_width-22.f, 2.f-lineSpacing, -22.f, 2.f-lineSpacing, -22.f, 2.f }; solidShader->VertexPointer(2, GL_FLOAT, 0, rowVerts); solidShader->AssertPointersBound(); glDrawArrays(GL_TRIANGLES, 0, 6); transform.PostTranslate(0.0f, lineSpacing, 0.0f); solidShader->Uniform(str_transform, transform); } solidTech->EndPass(); // Print table and column titles CShaderTechniquePtr textTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text); textTech->BeginPass(); CTextRenderer textRenderer(textTech->GetShader()); textRenderer.Font(font_name); textRenderer.Color(1.0f, 1.0f, 1.0f); textRenderer.PrintfAt(2.0f, lineSpacing, L"%hs", table->GetTitle().c_str()); textRenderer.Translate(22.0f, lineSpacing*2.0f, 0.0f); float colX = 0.0f; for (size_t col = 0; col < columns.size(); ++col) { CStrW text = columns[col].title.FromUTF8(); int w, h; font.CalculateStringSize(text.c_str(), w, h); float x = colX; if (col > 0) // right-align all but the first column x += columns[col].width - w; textRenderer.Put(x, 0.0f, text.c_str()); colX += columns[col].width; } textRenderer.Translate(0.0f, lineSpacing, 0.0f); // Print rows int currentExpandId = 1; for (size_t row = 0; row < numrows; ++row) { if (table->IsHighlightRow(row)) textRenderer.Color(1.0f, 0.5f, 0.5f); else textRenderer.Color(1.0f, 1.0f, 1.0f); if (table->GetChild(row)) { textRenderer.PrintfAt(-15.0f, 0.0f, L"%d", currentExpandId); currentExpandId++; } float colX = 0.0f; for (size_t col = 0; col < columns.size(); ++col) { CStrW text = table->GetCellText(row, col).FromUTF8(); int w, h; font.CalculateStringSize(text.c_str(), w, h); float x = colX; if (col > 0) // right-align all but the first column x += columns[col].width - w; textRenderer.Put(x, 0.0f, text.c_str()); colX += columns[col].width; } textRenderer.Translate(0.0f, lineSpacing, 0.0f); } textRenderer.Color(1.0f, 1.0f, 1.0f); if (m->path.size() > 1) { textRenderer.Translate(0.0f, lineSpacing, 0.0f); textRenderer.Put(-15.0f, 0.0f, L"0"); textRenderer.Put(0.0f, 0.0f, L"back to parent"); } textRenderer.Render(); textTech->EndPass(); glDisable(GL_BLEND); glEnable(GL_DEPTH_TEST); } // Handle input InReaction CProfileViewer::Input(const SDL_Event_* ev) { switch(ev->ev.type) { case SDL_KEYDOWN: { if (!m->profileVisible) break; int k = ev->ev.key.keysym.sym; if (k >= SDLK_0 && k <= SDLK_9) { m->NavigateTree(k - SDLK_0); return IN_HANDLED; } break; } case SDL_HOTKEYDOWN: std::string hotkey = static_cast(ev->ev.user.data1); if( hotkey == "profile.toggle" ) { if (!m->profileVisible) { if (m->rootTables.size()) { m->profileVisible = true; m->path.push_back(m->rootTables[0]); } } else { size_t i; for(i = 0; i < m->rootTables.size(); ++i) { if (m->rootTables[i] == m->path[0]) break; } i++; m->path.clear(); if (i < m->rootTables.size()) { m->path.push_back(m->rootTables[i]); } else { m->profileVisible = false; } } return( IN_HANDLED ); } else if( hotkey == "profile.save" ) { SaveToFile(); return( IN_HANDLED ); } break; } return( IN_PASS ); } InReaction CProfileViewer::InputThunk(const SDL_Event_* ev) { if (CProfileViewer::IsInitialised()) return g_ProfileViewer.Input(ev); return IN_PASS; } // Add a table to the list of roots void CProfileViewer::AddRootTable(AbstractProfileTable* table, bool front) { if (front) m->rootTables.insert(m->rootTables.begin(), table); else m->rootTables.push_back(table); } namespace { struct WriteTable { std::ofstream& f; WriteTable(std::ofstream& f) : f(f) {} void operator() (AbstractProfileTable* table) { std::vector data; // 2d array of (rows+head)*columns elements const std::vector& columns = table->GetColumns(); // Add column headers to 'data' for (std::vector::const_iterator col_it = columns.begin(); col_it != columns.end(); ++col_it) data.push_back(col_it->title); // Recursively add all profile data to 'data' WriteRows(1, table, data); // Calculate the width of each column ( = the maximum width of // any value in that column) std::vector columnWidths; size_t cols = columns.size(); for (size_t c = 0; c < cols; ++c) { size_t max = 0; for (size_t i = c; i < data.size(); i += cols) max = std::max(max, data[i].length()); columnWidths.push_back(max); } // Output data as a formatted table: f << "\n\n" << table->GetTitle() << "\n"; if (cols == 0) // avoid divide-by-zero return; for (size_t r = 0; r < data.size()/cols; ++r) { for (size_t c = 0; c < cols; ++c) f << (c ? " | " : "\n") << data[r*cols + c].Pad(PS_TRIM_RIGHT, columnWidths[c]); // Add dividers under some rows. (Currently only the first, since // that contains the column headers.) if (r == 0) for (size_t c = 0; c < cols; ++c) f << (c ? "-|-" : "\n") << CStr::Repeat("-", columnWidths[c]); } } void WriteRows(int indent, AbstractProfileTable* table, std::vector& data) { const std::vector& columns = table->GetColumns(); for (size_t r = 0; r < table->GetNumberRows(); ++r) { // Do pretty tree-structure indenting CStr indentation = CStr::Repeat("| ", indent-1); if (r+1 == table->GetNumberRows()) indentation += "'-"; else indentation += "|-"; for (size_t c = 0; c < columns.size(); ++c) if (c == 0) data.push_back(indentation + table->GetCellText(r, c)); else data.push_back(table->GetCellText(r, c)); if (table->GetChild(r)) WriteRows(indent+1, table->GetChild(r), data); } } private: const WriteTable& operator=(const WriteTable&); }; struct DumpTable { const ScriptInterface& m_ScriptInterface; JS::PersistentRooted m_Root; DumpTable(const ScriptInterface& scriptInterface, JS::HandleValue root) : m_ScriptInterface(scriptInterface), m_Root(scriptInterface.GetJSRuntime(), root) { } // std::for_each requires a move constructor and the use of JS::PersistentRooted apparently breaks a requirement for an // automatic move constructor DumpTable(DumpTable && original) : m_ScriptInterface(original.m_ScriptInterface), m_Root(original.m_ScriptInterface.GetJSRuntime(), original.m_Root.get()) { } void operator() (AbstractProfileTable* table) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue t(cx); JS::RootedValue rows(cx, DumpRows(table)); m_ScriptInterface.Eval(L"({})", &t); m_ScriptInterface.SetProperty(t, "cols", DumpCols(table)); m_ScriptInterface.SetProperty(t, "data", rows); m_ScriptInterface.SetProperty(m_Root, table->GetTitle().c_str(), t); } std::vector DumpCols(AbstractProfileTable* table) { std::vector titles; const std::vector& columns = table->GetColumns(); for (size_t c = 0; c < columns.size(); ++c) titles.push_back(columns[c].title); return titles; } JS::Value DumpRows(AbstractProfileTable* table) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue data(cx); m_ScriptInterface.CreateObject(&data); const std::vector& columns = table->GetColumns(); for (size_t r = 0; r < table->GetNumberRows(); ++r) { JS::RootedValue row(cx); m_ScriptInterface.CreateArray(&row); m_ScriptInterface.SetProperty(data, table->GetCellText(r, 0).c_str(), row); if (table->GetChild(r)) { JS::RootedValue childRows(cx, DumpRows(table->GetChild(r))); m_ScriptInterface.SetPropertyInt(row, 0, childRows); } for (size_t c = 1; c < columns.size(); ++c) m_ScriptInterface.SetPropertyInt(row, c, table->GetCellText(r, c)); } return data; } private: const DumpTable& operator=(const DumpTable&); }; bool SortByName(AbstractProfileTable* a, AbstractProfileTable* b) { return (a->GetName() < b->GetName()); } } void CProfileViewer::SaveToFile() { // Open the file, if necessary. If this method is called several times, // the profile results will be appended to the previous ones from the same // run. if (! m->outputStream.is_open()) { // Open the file. (It will be closed when the CProfileViewer // destructor is called.) OsPath path = psLogDir()/"profile.txt"; m->outputStream.open(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); if (m->outputStream.fail()) { LOGERROR("Failed to open profile log file"); return; } else { LOGMESSAGERENDER("Profiler snapshot saved to '%s'", path.string8()); } } time_t t; time(&t); m->outputStream << "================================================================\n\n"; m->outputStream << "PS profiler snapshot - " << asctime(localtime(&t)); std::vector tables = m->rootTables; sort(tables.begin(), tables.end(), SortByName); for_each(tables.begin(), tables.end(), WriteTable(m->outputStream)); m->outputStream << "\n\n================================================================\n"; m->outputStream.flush(); } void CProfileViewer::ShowTable(const CStr& table) { m->path.clear(); if (table.length() > 0) { for (size_t i = 0; i < m->rootTables.size(); ++i) { if (m->rootTables[i]->GetName() == table) { m->path.push_back(m->rootTables[i]); m->profileVisible = true; return; } } } // No matching table found, so don't display anything m->profileVisible = false; } Index: ps/trunk/source/renderer/ShadowMap.cpp =================================================================== --- ps/trunk/source/renderer/ShadowMap.cpp (revision 22604) +++ ps/trunk/source/renderer/ShadowMap.cpp (revision 22605) @@ -1,788 +1,785 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * Shadow mapping related texture and matrix management */ #include "precompiled.h" -#include "gui/GUIutil.h" -#include "lib/bits.h" -#include "lib/ogl.h" -#include "ps/CLogger.h" -#include "ps/ConfigDB.h" -#include "ps/Profile.h" +#include "ShadowMap.h" #include "graphics/LightEnv.h" #include "graphics/ShaderManager.h" - +#include "gui/GUIMatrix.h" +#include "lib/bits.h" +#include "lib/ogl.h" #include "maths/BoundingBoxAligned.h" #include "maths/Brush.h" #include "maths/MathUtil.h" #include "maths/Matrix3D.h" - +#include "ps/CLogger.h" +#include "ps/ConfigDB.h" +#include "ps/Profile.h" #include "renderer/Renderer.h" -#include "renderer/ShadowMap.h" - /////////////////////////////////////////////////////////////////////////////////////////////////// // ShadowMap implementation /** * Struct ShadowMapInternals: Internal data for the ShadowMap implementation */ struct ShadowMapInternals { // bit depth for the depth texture int DepthTextureBits; // the EXT_framebuffer_object framebuffer GLuint Framebuffer; // handle of shadow map GLuint Texture; // width, height of shadow map int Width, Height; // Shadow map quality (-2 - Very Low, -1 - Low, 0 - Medium, 1 - High, 2 - Very High) int QualityLevel; // used width, height of shadow map int EffectiveWidth, EffectiveHeight; // transform light space into projected light space // in projected light space, the shadowbound box occupies the [-1..1] cube // calculated on BeginRender, after the final shadow bounds are known CMatrix3D LightProjection; // Transform world space into light space; calculated on SetupFrame CMatrix3D LightTransform; // Transform world space into texture space of the shadow map; // calculated on BeginRender, after the final shadow bounds are known CMatrix3D TextureMatrix; // transform light space into world space CMatrix3D InvLightTransform; // bounding box of shadowed objects in light space CBoundingBoxAligned ShadowCasterBound; CBoundingBoxAligned ShadowReceiverBound; CBoundingBoxAligned ShadowRenderBound; // Camera transformed into light space CCamera LightspaceCamera; // Some drivers (at least some Intel Mesa ones) appear to handle alpha testing // incorrectly when the FBO has only a depth attachment. // When m_ShadowAlphaFix is true, we use DummyTexture to store a useless // alpha texture which is attached to the FBO as a workaround. GLuint DummyTexture; // Copy of renderer's standard view camera, saved between // BeginRender and EndRender while we replace it with the shadow camera CCamera SavedViewCamera; // Save the caller's FBO so it can be restored GLint SavedViewFBO; // Helper functions void CalcShadowMatrices(); void CreateTexture(); }; /////////////////////////////////////////////////////////////////////////////////////////////////// // Construction/Destruction ShadowMap::ShadowMap() { m = new ShadowMapInternals; m->Framebuffer = 0; m->Texture = 0; m->DummyTexture = 0; m->Width = 0; m->Height = 0; m->QualityLevel = 0; m->EffectiveWidth = 0; m->EffectiveHeight = 0; m->DepthTextureBits = 0; // DepthTextureBits: 24/32 are very much faster than 16, on GeForce 4 and FX; // but they're very much slower on Radeon 9800. // In both cases, the default (no specified depth) is fast, so we just use // that by default and hope it's alright. (Otherwise, we'd probably need to // do some kind of hardware detection to work out what to use.) // Avoid using uninitialised values in AddShadowedBound if SetupFrame wasn't called first m->LightTransform.SetIdentity(); } ShadowMap::~ShadowMap() { if (m->Texture) glDeleteTextures(1, &m->Texture); if (m->DummyTexture) glDeleteTextures(1, &m->DummyTexture); if (m->Framebuffer) pglDeleteFramebuffersEXT(1, &m->Framebuffer); delete m; } /////////////////////////////////////////////////////////////////////////////////////////////////// // Force the texture/buffer/etc to be recreated, particularly when the renderer's // size has changed void ShadowMap::RecreateTexture() { if (m->Texture) glDeleteTextures(1, &m->Texture); if (m->DummyTexture) glDeleteTextures(1, &m->DummyTexture); if (m->Framebuffer) pglDeleteFramebuffersEXT(1, &m->Framebuffer); m->Texture = 0; m->DummyTexture = 0; m->Framebuffer = 0; // (Texture will be constructed in next SetupFrame) } ////////////////////////////////////////////////////////////////////////////// // SetupFrame: camera and light direction for this frame void ShadowMap::SetupFrame(const CCamera& camera, const CVector3D& lightdir) { if (!m->Texture) m->CreateTexture(); CVector3D z = lightdir; CVector3D y; CVector3D x = camera.m_Orientation.GetIn(); CVector3D eyepos = camera.m_Orientation.GetTranslation(); z.Normalize(); x -= z * z.Dot(x); if (x.Length() < 0.001) { // this is invoked if the camera and light directions almost coincide // assumption: light direction has a significant Z component x = CVector3D(1.0, 0.0, 0.0); x -= z * z.Dot(x); } x.Normalize(); y = z.Cross(x); // X axis perpendicular to light direction, flowing along with view direction m->LightTransform._11 = x.X; m->LightTransform._12 = x.Y; m->LightTransform._13 = x.Z; // Y axis perpendicular to light and view direction m->LightTransform._21 = y.X; m->LightTransform._22 = y.Y; m->LightTransform._23 = y.Z; // Z axis is in direction of light m->LightTransform._31 = z.X; m->LightTransform._32 = z.Y; m->LightTransform._33 = z.Z; // eye is at the origin of the coordinate system m->LightTransform._14 = -x.Dot(eyepos); m->LightTransform._24 = -y.Dot(eyepos); m->LightTransform._34 = -z.Dot(eyepos); m->LightTransform._41 = 0.0; m->LightTransform._42 = 0.0; m->LightTransform._43 = 0.0; m->LightTransform._44 = 1.0; m->LightTransform.GetInverse(m->InvLightTransform); m->ShadowCasterBound.SetEmpty(); m->ShadowReceiverBound.SetEmpty(); // m->LightspaceCamera = camera; m->LightspaceCamera.m_Orientation = m->LightTransform * camera.m_Orientation; m->LightspaceCamera.UpdateFrustum(); } ////////////////////////////////////////////////////////////////////////////// // AddShadowedBound: add a world-space bounding box to the bounds of shadowed // objects void ShadowMap::AddShadowCasterBound(const CBoundingBoxAligned& bounds) { CBoundingBoxAligned lightspacebounds; bounds.Transform(m->LightTransform, lightspacebounds); m->ShadowCasterBound += lightspacebounds; } void ShadowMap::AddShadowReceiverBound(const CBoundingBoxAligned& bounds) { CBoundingBoxAligned lightspacebounds; bounds.Transform(m->LightTransform, lightspacebounds); m->ShadowReceiverBound += lightspacebounds; } CFrustum ShadowMap::GetShadowCasterCullFrustum() { // Get the bounds of all objects that can receive shadows CBoundingBoxAligned bound = m->ShadowReceiverBound; // Intersect with the camera frustum, so the shadow map doesn't have to get // stretched to cover the off-screen parts of large models bound.IntersectFrustumConservative(m->LightspaceCamera.GetFrustum()); // ShadowBound might have been empty to begin with, producing an empty result if (bound.IsEmpty()) { // CFrustum can't easily represent nothingness, so approximate it with // a single point which won't match many objects bound += CVector3D(0.0f, 0.0f, 0.0f); return bound.ToFrustum(); } // Extend the bounds a long way towards the light source, to encompass // all objects that might cast visible shadows. // (The exact constant was picked entirely arbitrarily.) bound[0].Z -= 1000.f; CFrustum frustum = bound.ToFrustum(); frustum.Transform(m->InvLightTransform); return frustum; } /////////////////////////////////////////////////////////////////////////////////////////////////// // CalcShadowMatrices: calculate required matrices for shadow map generation - the light's // projection and transformation matrices void ShadowMapInternals::CalcShadowMatrices() { // Start building the shadow map to cover all objects that will receive shadows CBoundingBoxAligned receiverBound = ShadowReceiverBound; // Intersect with the camera frustum, so the shadow map doesn't have to get // stretched to cover the off-screen parts of large models receiverBound.IntersectFrustumConservative(LightspaceCamera.GetFrustum()); // Intersect with the shadow caster bounds, because there's no point // wasting space around the edges of the shadow map that we're not going // to draw into ShadowRenderBound[0].X = std::max(receiverBound[0].X, ShadowCasterBound[0].X); ShadowRenderBound[0].Y = std::max(receiverBound[0].Y, ShadowCasterBound[0].Y); ShadowRenderBound[1].X = std::min(receiverBound[1].X, ShadowCasterBound[1].X); ShadowRenderBound[1].Y = std::min(receiverBound[1].Y, ShadowCasterBound[1].Y); // Set the near and far planes to include just the shadow casters, // so we make full use of the depth texture's range. Add a bit of a // delta so we don't accidentally clip objects that are directly on // the planes. ShadowRenderBound[0].Z = ShadowCasterBound[0].Z - 2.f; ShadowRenderBound[1].Z = ShadowCasterBound[1].Z + 2.f; // ShadowBound might have been empty to begin with, producing an empty result if (ShadowRenderBound.IsEmpty()) { // no-op LightProjection.SetIdentity(); TextureMatrix = LightTransform; return; } // round off the shadow boundaries to sane increments to help reduce swim effect float boundInc = 16.0f; ShadowRenderBound[0].X = floor(ShadowRenderBound[0].X / boundInc) * boundInc; ShadowRenderBound[0].Y = floor(ShadowRenderBound[0].Y / boundInc) * boundInc; ShadowRenderBound[1].X = ceil(ShadowRenderBound[1].X / boundInc) * boundInc; ShadowRenderBound[1].Y = ceil(ShadowRenderBound[1].Y / boundInc) * boundInc; // Setup orthogonal projection (lightspace -> clip space) for shadowmap rendering CVector3D scale = ShadowRenderBound[1] - ShadowRenderBound[0]; CVector3D shift = (ShadowRenderBound[1] + ShadowRenderBound[0]) * -0.5; if (scale.X < 1.0) scale.X = 1.0; if (scale.Y < 1.0) scale.Y = 1.0; if (scale.Z < 1.0) scale.Z = 1.0; scale.X = 2.0 / scale.X; scale.Y = 2.0 / scale.Y; scale.Z = 2.0 / scale.Z; // make sure a given world position falls on a consistent shadowmap texel fractional offset float offsetX = fmod(ShadowRenderBound[0].X - LightTransform._14, 2.0f/(scale.X*EffectiveWidth)); float offsetY = fmod(ShadowRenderBound[0].Y - LightTransform._24, 2.0f/(scale.Y*EffectiveHeight)); LightProjection.SetZero(); LightProjection._11 = scale.X; LightProjection._14 = (shift.X + offsetX) * scale.X; LightProjection._22 = scale.Y; LightProjection._24 = (shift.Y + offsetY) * scale.Y; LightProjection._33 = scale.Z; LightProjection._34 = shift.Z * scale.Z; LightProjection._44 = 1.0; // Calculate texture matrix by creating the clip space to texture coordinate matrix // and then concatenating all matrices that have been calculated so far float texscalex = scale.X * 0.5f * (float)EffectiveWidth / (float)Width; float texscaley = scale.Y * 0.5f * (float)EffectiveHeight / (float)Height; float texscalez = scale.Z * 0.5f; CMatrix3D lightToTex; lightToTex.SetZero(); lightToTex._11 = texscalex; lightToTex._14 = (offsetX - ShadowRenderBound[0].X) * texscalex; lightToTex._22 = texscaley; lightToTex._24 = (offsetY - ShadowRenderBound[0].Y) * texscaley; lightToTex._33 = texscalez; lightToTex._34 = -ShadowRenderBound[0].Z * texscalez; lightToTex._44 = 1.0; TextureMatrix = lightToTex * LightTransform; } ////////////////////////////////////////////////////////////////////////// // Create the shadow map void ShadowMapInternals::CreateTexture() { // Cleanup if (Texture) { glDeleteTextures(1, &Texture); Texture = 0; } if (DummyTexture) { glDeleteTextures(1, &DummyTexture); DummyTexture = 0; } if (Framebuffer) { pglDeleteFramebuffersEXT(1, &Framebuffer); Framebuffer = 0; } // save the caller's FBO glGetIntegerv(GL_FRAMEBUFFER_BINDING_EXT, &SavedViewFBO); pglGenFramebuffersEXT(1, &Framebuffer); if (g_Renderer.m_ShadowMapSize != 0) { // non-default option to override the size Width = Height = g_Renderer.m_ShadowMapSize; } else { CFG_GET_VAL("shadowquality", QualityLevel); // get shadow map size as next power of two up from view width/height int shadow_map_size = (int)round_up_to_pow2((unsigned)std::max(g_Renderer.GetWidth(), g_Renderer.GetHeight())); switch (QualityLevel) { // Very Low case -2: shadow_map_size /= 4; break; // Low case -1: shadow_map_size /= 2; break; // High case 1: shadow_map_size *= 2; break; // Ultra case 2: shadow_map_size *= 4; break; // Medium as is default: break; } Width = Height = shadow_map_size; } // Clamp to the maximum texture size Width = std::min(Width, (int)ogl_max_tex_size); Height = std::min(Height, (int)ogl_max_tex_size); // Since we're using a framebuffer object, the whole texture is available EffectiveWidth = Width; EffectiveHeight = Height; const char* formatname; switch(DepthTextureBits) { case 16: formatname = "DEPTH_COMPONENT16"; break; case 24: formatname = "DEPTH_COMPONENT24"; break; case 32: formatname = "DEPTH_COMPONENT32"; break; default: formatname = "DEPTH_COMPONENT"; break; } LOGMESSAGE("Creating shadow texture (size %dx%d) (format = %s)", Width, Height, formatname); if (g_Renderer.m_Options.m_ShadowAlphaFix) { glGenTextures(1, &DummyTexture); g_Renderer.BindTexture(0, DummyTexture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, Width, Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); } glGenTextures(1, &Texture); g_Renderer.BindTexture(0, Texture); GLenum format; #if CONFIG2_GLES format = GL_DEPTH_COMPONENT; #else switch (DepthTextureBits) { case 16: format = GL_DEPTH_COMPONENT16; break; case 24: format = GL_DEPTH_COMPONENT24; break; case 32: format = GL_DEPTH_COMPONENT32; break; default: format = GL_DEPTH_COMPONENT; break; } #endif glTexImage2D(GL_TEXTURE_2D, 0, format, Width, Height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, NULL); // GLES requires type == UNSIGNED_SHORT or UNSIGNED_INT // set texture parameters glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); #if CONFIG2_GLES // GLES doesn't do depth comparisons, so treat it as a // basic unfiltered depth texture glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); #else // Enable automatic depth comparisons glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); // Use GL_LINEAR to trigger automatic PCF on some devices glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); #endif ogl_WarnIfError(); // bind to framebuffer object glBindTexture(GL_TEXTURE_2D, 0); pglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, Framebuffer); pglFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, Texture, 0); if (g_Renderer.m_Options.m_ShadowAlphaFix) { pglFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, DummyTexture, 0); } else { #if CONFIG2_GLES #warning TODO: figure out whether the glDrawBuffer/glReadBuffer stuff is needed, since it is not supported by GLES #else glDrawBuffer(GL_NONE); #endif } #if !CONFIG2_GLES glReadBuffer(GL_NONE); #endif ogl_WarnIfError(); GLenum status = pglCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); pglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, SavedViewFBO); if (status != GL_FRAMEBUFFER_COMPLETE_EXT) { LOGWARNING("Framebuffer object incomplete: 0x%04X", status); // Disable shadow rendering (but let the user try again if they want) g_Renderer.m_Options.m_Shadows = false; } } /////////////////////////////////////////////////////////////////////////////////////////////////// // Set up to render into shadow map texture void ShadowMap::BeginRender() { // HACK HACK: this depends in non-obvious ways on the behaviour of the caller // save caller's FBO glGetIntegerv(GL_FRAMEBUFFER_BINDING_EXT, &m->SavedViewFBO); // Calc remaining shadow matrices m->CalcShadowMatrices(); { PROFILE("bind framebuffer"); glBindTexture(GL_TEXTURE_2D, 0); pglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m->Framebuffer); } // clear buffers { PROFILE("clear depth texture"); // In case we used m_ShadowAlphaFix, we ought to clear the unused // color buffer too, else Mali 400 drivers get confused. // Might as well clear stencil too for completeness. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); glColorMask(0,0,0,0); } // setup viewport const SViewPort vp = { 0, 0, m->EffectiveWidth, m->EffectiveHeight }; g_Renderer.SetViewport(vp); m->SavedViewCamera = g_Renderer.GetViewCamera(); CCamera c = m->SavedViewCamera; c.SetProjection(m->LightProjection); c.GetOrientation() = m->InvLightTransform; g_Renderer.SetViewCamera(c); #if !CONFIG2_GLES glMatrixMode(GL_PROJECTION); glLoadMatrixf(&m->LightProjection._11); glMatrixMode(GL_MODELVIEW); glLoadMatrixf(&m->LightTransform._11); #endif glEnable(GL_SCISSOR_TEST); glScissor(1,1, m->EffectiveWidth-2, m->EffectiveHeight-2); } /////////////////////////////////////////////////////////////////////////////////////////////////// // Finish rendering into shadow map texture void ShadowMap::EndRender() { glDisable(GL_SCISSOR_TEST); g_Renderer.SetViewCamera(m->SavedViewCamera); { PROFILE("unbind framebuffer"); pglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m->SavedViewFBO); } const SViewPort vp = { 0, 0, g_Renderer.GetWidth(), g_Renderer.GetHeight() }; g_Renderer.SetViewport(vp); glColorMask(1,1,1,1); } /////////////////////////////////////////////////////////////////////////////////////////////////// // Retrieve the texture handle and texture matrix for shadowing GLuint ShadowMap::GetTexture() const { return m->Texture; } const CMatrix3D& ShadowMap::GetTextureMatrix() const { return m->TextureMatrix; } /////////////////////////////////////////////////////////////////////////////////////////////////// // Depth texture bits int ShadowMap::GetDepthTextureBits() const { return m->DepthTextureBits; } void ShadowMap::SetDepthTextureBits(int bits) { if (bits != m->DepthTextureBits) { if (m->Texture) { glDeleteTextures(1, &m->Texture); m->Texture = 0; } m->Width = m->Height = 0; m->DepthTextureBits = bits; } } /////////////////////////////////////////////////////////////////////////////////////////////////// // Depth texture size int ShadowMap::GetWidth() const { return m->Width; } int ShadowMap::GetHeight() const { return m->Height; } ////////////////////////////////////////////////////////////////////////////// void ShadowMap::RenderDebugBounds() { CShaderTechniquePtr shaderTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid); shaderTech->BeginPass(); CShaderProgramPtr shader = shaderTech->GetShader(); glDepthMask(0); glDisable(GL_CULL_FACE); // Render various shadow bounds: // Yellow = bounds of objects in view frustum that receive shadows // Red = culling frustum used to find potential shadow casters // Green = bounds of objects in culling frustum that cast shadows // Blue = frustum used for rendering the shadow map shader->Uniform(str_transform, g_Renderer.GetViewCamera().GetViewProjection() * m->InvLightTransform); shader->Uniform(str_color, 1.0f, 1.0f, 0.0f, 1.0f); m->ShadowReceiverBound.RenderOutline(shader); shader->Uniform(str_color, 0.0f, 1.0f, 0.0f, 1.0f); m->ShadowCasterBound.RenderOutline(shader); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); shader->Uniform(str_color, 0.0f, 0.0f, 1.0f, 0.25f); m->ShadowRenderBound.Render(shader); glDisable(GL_BLEND); shader->Uniform(str_color, 0.0f, 0.0f, 1.0f, 1.0f); m->ShadowRenderBound.RenderOutline(shader); // Render light frustum shader->Uniform(str_transform, g_Renderer.GetViewCamera().GetViewProjection()); CFrustum frustum = GetShadowCasterCullFrustum(); // We don't have a function to create a brush directly from a frustum, so use // the ugly approach of creating a large cube and then intersecting with the frustum CBoundingBoxAligned dummy(CVector3D(-1e4, -1e4, -1e4), CVector3D(1e4, 1e4, 1e4)); CBrush brush(dummy); CBrush frustumBrush; brush.Intersect(frustum, frustumBrush); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); shader->Uniform(str_color, 1.0f, 0.0f, 0.0f, 0.25f); frustumBrush.Render(shader); glDisable(GL_BLEND); shader->Uniform(str_color, 1.0f, 0.0f, 0.0f, 1.0f); frustumBrush.RenderOutline(shader); shaderTech->EndPass(); #if 0 CMatrix3D InvTexTransform; m->TextureMatrix.GetInverse(InvTexTransform); // Render representative texture rectangle glPushMatrix(); glMultMatrixf(&InvTexTransform._11); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glColor4ub(255,0,0,64); glBegin(GL_QUADS); glVertex3f(0.0, 0.0, 0.0); glVertex3f(1.0, 0.0, 0.0); glVertex3f(1.0, 1.0, 0.0); glVertex3f(0.0, 1.0, 0.0); glEnd(); glDisable(GL_BLEND); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glColor3ub(255,0,0); glBegin(GL_QUADS); glVertex3f(0.0, 0.0, 0.0); glVertex3f(1.0, 0.0, 0.0); glVertex3f(1.0, 1.0, 0.0); glVertex3f(0.0, 1.0, 0.0); glEnd(); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glPopMatrix(); #endif glEnable(GL_CULL_FACE); glDepthMask(1); } void ShadowMap::RenderDebugTexture() { glDepthMask(0); glDisable(GL_DEPTH_TEST); #if !CONFIG2_GLES g_Renderer.BindTexture(0, m->Texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE); #endif CShaderTechniquePtr texTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_basic); texTech->BeginPass(); CShaderProgramPtr texShader = texTech->GetShader(); texShader->Uniform(str_transform, GetDefaultGuiMatrix()); texShader->BindTexture(str_tex, m->Texture); float s = 256.f; float boxVerts[] = { 0,0, 0,s, s,0, s,0, 0,s, s,s }; float boxUV[] = { 0,0, 0,1, 1,0, 1,0, 0,1, 1,1 }; texShader->VertexPointer(2, GL_FLOAT, 0, boxVerts); texShader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 0, boxUV); texShader->AssertPointersBound(); glDrawArrays(GL_TRIANGLES, 0, 6); texTech->EndPass(); #if !CONFIG2_GLES g_Renderer.BindTexture(0, m->Texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE); #endif glEnable(GL_DEPTH_TEST); glDepthMask(1); }