Index: ps/trunk/source/graphics/MiniMapTexture.cpp =================================================================== --- ps/trunk/source/graphics/MiniMapTexture.cpp (revision 26183) +++ ps/trunk/source/graphics/MiniMapTexture.cpp (revision 26184) @@ -1,525 +1,526 @@ /* Copyright (C) 2022 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 "MiniMapTexture.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/MiniPatch.h" #include "graphics/ShaderManager.h" #include "graphics/ShaderProgramPtr.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerritoryTexture.h" +#include "graphics/TextureManager.h" #include "lib/bits.h" #include "lib/timer.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/World.h" #include "ps/XML/Xeromyces.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/SceneRenderer.h" #include "renderer/WaterManager.h" #include "scriptinterface/Object.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpMinimap.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/system/ParamNode.h" namespace { // 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; const size_t FINAL_TEXTURE_SIZE = 512; 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); } void DrawTexture(CShaderProgramPtr shader) { const float quadUVs[] = { 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f }; const float quadVertices[] = { -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f }; shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 0, quadUVs); shader->VertexPointer(3, GL_FLOAT, 0, quadVertices); shader->AssertPointersBound(); glDrawArrays(GL_TRIANGLES, 0, 6); } struct MinimapUnitVertex { // This struct is copyable for convenience and because to move is to copy for primitives. 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; } } // anonymous namespace CMiniMapTexture::CMiniMapTexture(CSimulation2& simulation) : m_Simulation(simulation), m_IndexArray(GL_STATIC_DRAW), m_VertexArray(GL_DYNAMIC_DRAW) { // Register Relax NG validator. CXeromyces::AddValidator(g_VFS, "pathfinder", "simulation/data/pathfinder.rng"); m_ShallowPassageHeight = GetShallowPassageHeight(); double blinkDuration = 1.0; // Tests won't have config initialised if (CConfigDB::IsInitialised()) { CFG_GET_VAL("gui.session.minimap.blinkduration", blinkDuration); CFG_GET_VAL("gui.session.minimap.pingduration", m_PingDuration); } m_HalfBlinkDuration = blinkDuration / 2.0; 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(); } CMiniMapTexture::~CMiniMapTexture() { DestroyTextures(); } void CMiniMapTexture::Update(const float UNUSED(deltaRealTime)) { if (m_WaterHeight != g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight) { m_TerrainTextureDirty = true; m_FinalTextureDirty = true; } } void CMiniMapTexture::Render(Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext) { const CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); if (!terrain) return; if (!m_TerrainTexture) CreateTextures(deviceCommandContext, terrain); if (m_TerrainTextureDirty) RebuildTerrainTexture(deviceCommandContext, terrain); RenderFinalTexture(); } void CMiniMapTexture::CreateTextures( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext, const CTerrain* terrain) { DestroyTextures(); m_MapSize = terrain->GetVerticesPerSide(); const size_t textureSize = round_up_to_pow2(static_cast(m_MapSize)); const Renderer::Backend::Sampler::Desc defaultSamplerDesc = Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE); // Create terrain texture m_TerrainTexture = Renderer::Backend::GL::CTexture::Create2D( Renderer::Backend::Format::R8G8B8A8, textureSize, textureSize, defaultSamplerDesc); // Initialise texture with solid black, for the areas we don't // overwrite with uploading later. std::unique_ptr texData = std::make_unique(textureSize * textureSize); for (size_t i = 0; i < textureSize * textureSize; ++i) texData[i] = 0xFF000000; deviceCommandContext->UploadTexture( m_TerrainTexture.get(), Renderer::Backend::Format::R8G8B8A8, texData.get(), textureSize * textureSize * 4); texData.reset(); m_TerrainData = std::make_unique((m_MapSize - 1) * (m_MapSize - 1)); m_FinalTexture = Renderer::Backend::GL::CTexture::Create2D( Renderer::Backend::Format::R8G8B8A8, FINAL_TEXTURE_SIZE, FINAL_TEXTURE_SIZE, defaultSamplerDesc); glGenFramebuffersEXT(1, &m_FinalTextureFBO); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_FinalTextureFBO); glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, m_FinalTexture->GetHandle(), 0); GLenum status = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); if (status != GL_FRAMEBUFFER_COMPLETE_EXT) { LOGWARNING("MiniMapTexture Framebuffer object incomplete (A): 0x%04X", status); } glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); } void CMiniMapTexture::DestroyTextures() { m_TerrainTexture.reset(); m_FinalTexture.reset(); m_TerrainData.reset(); } void CMiniMapTexture::RebuildTerrainTexture( Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext, const CTerrain* terrain) { const u32 x = 0; const u32 y = 0; const u32 width = m_MapSize - 1; const u32 height = m_MapSize - 1; m_WaterHeight = g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight; m_TerrainTextureDirty = false; for (u32 j = 0; j < height; ++j) { u32* dataPtr = m_TerrainData.get() + ((y + j) * width) + x; for (u32 i = 0; i < width; ++i) { const float avgHeight = ( terrain->GetVertexGroundLevel((int)i, (int)j) + terrain->GetVertexGroundLevel((int)i+1, (int)j) + terrain->GetVertexGroundLevel((int)i, (int)j+1) + 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)terrain->GetHeightMap()[(y + j) * m_MapSize + x + i]) >> 8; int val = (hmap / 3) + 170; u32 color = 0xFFFFFFFF; CMiniPatch* mp = 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_TerrainTextureDirty = true; color = tex->GetBaseColor(); } } *dataPtr++ = ScaleColor(color, float(val) / 255.0f); } } } // Upload the texture deviceCommandContext->UploadTextureRegion( m_TerrainTexture.get(), Renderer::Backend::Format::R8G8B8A8, m_TerrainData.get(), width * height * 4, 0, 0, width, height); } void CMiniMapTexture::RenderFinalTexture() { // 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 const double currentTime = timer_Time(); const bool doUpdate = (currentTime - m_LastFinalTextureUpdate > 0.5) || m_FinalTextureDirty; if (doUpdate) m_LastFinalTextureUpdate = currentTime; else return; m_FinalTextureDirty = false; glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_FinalTextureFBO); const SViewPort oldViewPort = g_Renderer.GetViewport(); const SViewPort viewPort = { 0, 0, FINAL_TEXTURE_SIZE, FINAL_TEXTURE_SIZE }; g_Renderer.SetViewport(viewPort); CmpPtr cmpRangeManager(m_Simulation, SYSTEM_ENTITY); ENSURE(cmpRangeManager); CLOSTexture& losTexture = g_Game->GetView()->GetLOSTexture(); const float invTileMapSize = 1.0f / static_cast(TERRAIN_TILE_SIZE * m_MapSize); const float texCoordMax = m_TerrainTexture ? static_cast(m_MapSize - 1) / m_TerrainTexture->GetWidth() : 1.0f; CShaderProgramPtr shader; CShaderTechniquePtr tech; CShaderDefines baseDefines; baseDefines.Add(str_MINIMAP_BASE, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, baseDefines); tech->BeginPass(); shader = tech->GetShader(); if (m_TerrainTexture) shader->BindTexture(str_baseTex, m_TerrainTexture.get()); CMatrix3D baseTransform; baseTransform.SetIdentity(); CMatrix3D baseTextureTransform; baseTextureTransform.SetIdentity(); CMatrix3D terrainTransform; terrainTransform.SetIdentity(); terrainTransform.Scale(texCoordMax, texCoordMax, 1.0f); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, terrainTransform); if (m_TerrainTexture) DrawTexture(shader); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glColorMask(1, 1, 1, 0); // Draw territory boundaries CTerritoryTexture& territoryTexture = g_Game->GetView()->GetTerritoryTexture(); shader->BindTexture(str_baseTex, territoryTexture.GetTexture()); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, territoryTexture.GetMinimapTextureMatrix()); DrawTexture(shader); glDisable(GL_BLEND); glColorMask(0, 0, 0, 1); shader->BindTexture(str_baseTex, losTexture.GetTexture()); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, losTexture.GetMinimapTextureMatrix()); DrawTexture(shader); tech->EndPass(); glColorMask(1, 1, 1, 1); CShaderDefines pointDefines; pointDefines.Add(str_MINIMAP_POINT, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, pointDefines); tech->BeginPass(); shader = tech->GetShader(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_pointSize, 9.0f); CMatrix3D unitMatrix; unitMatrix.SetIdentity(); // Convert world space coordinates into [0, 2]. const float unitScale = invTileMapSize; unitMatrix.Scale(unitScale * 2.0f, unitScale * 2.0f, 1.0f); // Offset the coordinates to [-1, 1]. unitMatrix.Translate(CVector3D(-1.0f, -1.0f, 0.0f)); shader->Uniform(str_transform, unitMatrix); CSimulation2::InterfaceList ents = m_Simulation.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 (currentTime > m_NextBlinkTime) { m_BlinkState = !m_BlinkState; m_NextBlinkTime = currentTime + 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)) { LosVisibility vis = cmpRangeManager->GetLosVisibility(it->first, m_Simulation.GetSimContext().GetCurrentDisplayedPlayer()); if (vis != LosVisibility::HIDDEN) { v.a = 255; v.x = posX.ToFloat(); v.y = posZ.ToFloat(); // Check minimap pinging to indicate something if (m_BlinkState && cmpMinimap->CheckPing(currentTime, 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 (const MinimapUnitVertex& vertex : pingingVertices) { addVertex(vertex, attrColor, attrPos); ++m_EntitiesDrawn; } ENSURE(m_EntitiesDrawn < MAX_ENTITIES_DRAWN); m_VertexArray.Upload(); } m_VertexArray.PrepareForRendering(); if (m_EntitiesDrawn > 0) { glEnable(GL_SCISSOR_TEST); glScissor(1, 1, FINAL_TEXTURE_SIZE - 1, FINAL_TEXTURE_SIZE - 1); #if !CONFIG2_GLES 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(); glDrawElements(GL_POINTS, (GLsizei)(m_EntitiesDrawn), GL_UNSIGNED_SHORT, indexBase); g_Renderer.GetStats().m_DrawCalls++; CVertexBuffer::Unbind(); #if !CONFIG2_GLES glDisable(GL_VERTEX_PROGRAM_POINT_SIZE); #endif glDisable(GL_SCISSOR_TEST); } tech->EndPass(); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); g_Renderer.SetViewport(oldViewPort); } // static float CMiniMapTexture::GetShallowPassageHeight() { float shallowPassageHeight = 0.0f; 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()) shallowPassageHeight = pathingSettings.GetChild("default").GetChild("MaxWaterDepth").ToFloat(); return shallowPassageHeight; } Index: ps/trunk/source/graphics/TerrainTextureEntry.cpp =================================================================== --- ps/trunk/source/graphics/TerrainTextureEntry.cpp (revision 26183) +++ ps/trunk/source/graphics/TerrainTextureEntry.cpp (revision 26184) @@ -1,368 +1,196 @@ /* Copyright (C) 2022 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 "TerrainTextureEntry.h" #include "graphics/MaterialManager.h" #include "graphics/Terrain.h" #include "graphics/TerrainProperties.h" #include "graphics/TerrainTextureManager.h" -#include "graphics/Texture.h" -#include "lib/allocators/shared_ptr.h" -#include "lib/file/io/io.h" +#include "graphics/TextureManager.h" #include "lib/ogl.h" -#include "lib/tex/tex.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include CTerrainTextureEntry::CTerrainTextureEntry(CTerrainPropertiesPtr properties, const VfsPath& path): m_pProperties(properties), m_BaseColor(0), m_BaseColorValid(false) { ENSURE(properties); CXeromyces XeroFile; if (XeroFile.Load(g_VFS, path, "terrain_texture") != PSRETURN_OK) { LOGERROR("Terrain xml not found (%s)", path.string8()); return; } #define EL(x) int el_##x = XeroFile.GetElementID(#x) #define AT(x) int at_##x = XeroFile.GetAttributeID(#x) EL(tag); EL(terrain); EL(texture); EL(textures); EL(material); EL(props); EL(alphamap); AT(file); AT(name); #undef AT #undef EL XMBElement root = XeroFile.GetRoot(); if (root.GetNodeName() != el_terrain) { LOGERROR("Invalid terrain format (unrecognised root element '%s')", XeroFile.GetElementString(root.GetNodeName())); return; } std::vector > samplers; VfsPath alphamap("standard"); m_Tag = utf8_from_wstring(path.Basename().string()); XERO_ITER_EL(root, child) { int child_name = child.GetNodeName(); if (child_name == el_textures) { XERO_ITER_EL(child, textures_element) { ENSURE(textures_element.GetNodeName() == el_texture); CStr name; VfsPath terrainTexturePath; XERO_ITER_ATTR(textures_element, relativePath) { if (relativePath.Name == at_file) terrainTexturePath = VfsPath("art/textures/terrain") / relativePath.Value.FromUTF8(); else if (relativePath.Name == at_name) name = relativePath.Value; } samplers.emplace_back(name, terrainTexturePath); if (name == str_baseTex.string()) m_DiffuseTexturePath = terrainTexturePath; } } else if (child_name == el_material) { VfsPath mat = VfsPath("art/materials") / child.GetText().FromUTF8(); if (CRenderer::IsInitialised()) m_Material = g_Renderer.GetSceneRenderer().GetMaterialManager().LoadMaterial(mat); } else if (child_name == el_alphamap) { alphamap = child.GetText().FromUTF8(); } else if (child_name == el_props) { CTerrainPropertiesPtr ret (new CTerrainProperties(properties)); ret->LoadXml(child, &XeroFile, path); if (ret) m_pProperties = ret; } else if (child_name == el_tag) { m_Tag = child.GetText(); } } for (size_t i = 0; i < samplers.size(); ++i) { CTextureProperties texture(samplers[i].second); texture.SetWrap(GL_REPEAT); // TODO: anisotropy should probably be user-configurable, but we want it to be // at least 2 for terrain else the ground looks very blurry when you tilt the // camera upwards texture.SetMaxAnisotropy(2.0f); if (CRenderer::IsInitialised()) { CTexturePtr texptr = g_Renderer.GetTextureManager().CreateTexture(texture); m_Material.AddSampler(CMaterial::TextureSampler(samplers[i].first, texptr)); } } if (CRenderer::IsInitialised()) - LoadAlphaMaps(alphamap); + m_TerrainAlpha = g_TexMan.LoadAlphaMap(alphamap); float texAngle = 0.f; float texSize = 1.f; if (m_pProperties) { m_Groups = m_pProperties->GetGroups(); texAngle = m_pProperties->GetTextureAngle(); texSize = m_pProperties->GetTextureSize(); } m_TextureMatrix.SetZero(); m_TextureMatrix._11 = cosf(texAngle) / texSize; m_TextureMatrix._13 = -sinf(texAngle) / texSize; m_TextureMatrix._21 = -sinf(texAngle) / texSize; m_TextureMatrix._23 = -cosf(texAngle) / texSize; m_TextureMatrix._44 = 1.f; GroupVector::iterator it=m_Groups.begin(); for (;it!=m_Groups.end();++it) (*it)->AddTerrain(this); } CTerrainTextureEntry::~CTerrainTextureEntry() { for (GroupVector::iterator it=m_Groups.begin();it!=m_Groups.end();++it) (*it)->RemoveTerrain(this); } // BuildBaseColor: calculate the root color of the texture, used for coloring minimap, and store // in m_BaseColor member void CTerrainTextureEntry::BuildBaseColor() { // Use the explicit properties value if possible if (m_pProperties && m_pProperties->HasBaseColor()) { m_BaseColor=m_pProperties->GetBaseColor(); m_BaseColorValid = true; return; } // Use the texture color if available if (GetTexture()->TryLoad()) { m_BaseColor = GetTexture()->GetBaseColor(); m_BaseColorValid = true; } } const float* CTerrainTextureEntry::GetTextureMatrix() const { return &m_TextureMatrix._11; } - -// LoadAlphaMaps: load the 14 default alpha maps, pack them into one composite texture and -// calculate the coordinate of each alphamap within this packed texture -void CTerrainTextureEntry::LoadAlphaMaps(const VfsPath& alphaMapType) -{ - const std::wstring key = L"(alpha map composite" + alphaMapType.string() + L")"; - - CTerrainTextureManager::TerrainAlphaMap::iterator it = g_TexMan.m_TerrainAlphas.find(alphaMapType); - - if (it != g_TexMan.m_TerrainAlphas.end()) - { - m_TerrainAlpha = it; - return; - } - - g_TexMan.m_TerrainAlphas[alphaMapType] = TerrainAlpha(); - it = g_TexMan.m_TerrainAlphas.find(alphaMapType); - - TerrainAlpha &result = it->second; - - // - // load all textures and store Handle in array - // - Tex textures[NUM_ALPHA_MAPS] = {}; - const VfsPath path = VfsPath("art/textures/terrain/alphamaps") / alphaMapType; - - const wchar_t* fnames[NUM_ALPHA_MAPS] = - { - L"blendcircle.png", - L"blendlshape.png", - L"blendedge.png", - L"blendedgecorner.png", - L"blendedgetwocorners.png", - L"blendfourcorners.png", - L"blendtwooppositecorners.png", - L"blendlshapecorner.png", - L"blendtwocorners.png", - L"blendcorner.png", - L"blendtwoedges.png", - L"blendthreecorners.png", - L"blendushape.png", - L"blendbad.png" - }; - size_t base = 0; // texture width/height (see below) - // for convenience, we require all alpha maps to be of the same BPP - // (avoids another ogl_tex_get_size call, and doesn't hurt) - size_t bpp = 0; - for (size_t i = 0; i < NUM_ALPHA_MAPS; ++i) - { - // note: these individual textures can be discarded afterwards; - // we cache the composite. - std::shared_ptr fileData; - size_t fileSize; - if (g_VFS->LoadFile(path / fnames[i], fileData, fileSize) != INFO::OK || - textures[i].decode(fileData, fileSize) != INFO::OK) - { - g_TexMan.m_TerrainAlphas.erase(it); - LOGERROR("Failed to load alphamap: %s", alphaMapType.string8()); - - const VfsPath standard("standard"); - if (path != standard) - LoadAlphaMaps(standard); - return; - } - - // Get its size and make sure they are all equal. - // (the packing algo assumes this). - if (textures[i].m_Width != textures[i].m_Height) - DEBUG_DISPLAY_ERROR(L"Alpha maps are not square"); - // .. first iteration: establish size - if (i == 0) - { - base = textures[i].m_Width; - bpp = textures[i].m_Bpp; - } - // .. not first: make sure texture size matches - else if (base != textures[i].m_Width || bpp != textures[i].m_Bpp) - DEBUG_DISPLAY_ERROR(L"Alpha maps are not identically sized (including pixel depth)"); - } - - // - // copy each alpha map (tile) into one buffer, arrayed horizontally. - // - const size_t tileWidth = 2 + base + 2; // 2 pixel border (avoids bilinear filtering artifacts) - const size_t totalWidth = round_up_to_pow2(tileWidth * NUM_ALPHA_MAPS); - const size_t totalHeight = base; ENSURE(is_pow2(totalHeight)); - std::shared_ptr data; - AllocateAligned(data, totalWidth * totalHeight, maxSectorSize); - // for each tile on row - for (size_t i = 0; i < NUM_ALPHA_MAPS; ++i) - { - // get src of copy - u8* src = textures[i].get_data(); - ENSURE(src); - - const size_t srcStep = bpp / 8; - - // get destination of copy - u8* dst = data.get() + (i * tileWidth); - - // for each row of image - for (size_t j = 0; j < base; ++j) - { - // duplicate first pixel - *dst++ = *src; - *dst++ = *src; - - // copy a row - for (size_t k = 0; k < base; ++k) - { - *dst++ = *src; - src += srcStep; - } - - // duplicate last pixel - *dst++ = *(src - srcStep); - *dst++ = *(src - srcStep); - - // advance write pointer for next row - dst += totalWidth - tileWidth; - } - - result.m_AlphaMapCoords[i].u0 = static_cast(i * tileWidth + 2) / totalWidth; - result.m_AlphaMapCoords[i].u1 = static_cast((i + 1) * tileWidth - 2) / totalWidth; - result.m_AlphaMapCoords[i].v0 = 0.0f; - result.m_AlphaMapCoords[i].v1 = 1.0f; - } - - for (size_t i = 0; i < NUM_ALPHA_MAPS; ++i) - textures[i].free(); - - // Enable the following to save a png of the generated texture - // in the public/ directory, for debugging. -#if 0 - Tex t; - ignore_result(t.wrap(totalWidth, totalHeight, 8, TEX_GREY, data, 0)); - - const VfsPath filename("blendtex.png"); - - DynArray da; - RETURN_STATUS_IF_ERR(tex_encode(&t, filename.Extension(), &da)); - - // write to disk - //Status ret = INFO::OK; - { - std::shared_ptr file = DummySharedPtr(da.base); - const ssize_t bytes_written = g_VFS->CreateFile(filename, file, da.pos); - if(bytes_written > 0) - ENSURE(bytes_written == (ssize_t)da.pos); - //else - // ret = (Status)bytes_written; - } - - ignore_result(da_free(&da)); -#endif - - result.m_CompositeAlphaMap = Renderer::Backend::GL::CTexture::Create2D( - Renderer::Backend::Format::A8, totalWidth, totalHeight, - Renderer::Backend::Sampler::MakeDefaultSampler( - Renderer::Backend::Sampler::Filter::LINEAR, - Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE)); - - // Upload the composite texture. - g_Renderer.BindTexture(0, result.m_CompositeAlphaMap->GetHandle()); - glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, totalWidth, totalHeight, 0, GL_ALPHA, GL_UNSIGNED_BYTE, data.get()); - g_Renderer.BindTexture(0, 0); - - m_TerrainAlpha = it; -} Index: ps/trunk/source/graphics/TerrainTextureEntry.h =================================================================== --- ps/trunk/source/graphics/TerrainTextureEntry.h (revision 26183) +++ ps/trunk/source/graphics/TerrainTextureEntry.h (revision 26184) @@ -1,98 +1,95 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 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_TERRAINTEXTUREENTRY #define INCLUDED_TERRAINTEXTUREENTRY #include "graphics/Material.h" #include "graphics/TerrainTextureManager.h" -#include "graphics/TextureManager.h" +#include "graphics/Texture.h" #include "lib/file/vfs/vfs_path.h" -#include "lib/res/handle.h" #include "maths/Matrix3D.h" #include "ps/CStr.h" #include // CTerrainTextureEntry: class wrapping a terrain texture object; contains various other required // elements - color of minimap, terrain "group" it belongs to, etc class CTerrainTextureEntry { public: using GroupVector = std::vector; // Most of the texture's data is delay-loaded, so after the constructor has // been called, the texture entry is ready to be used. CTerrainTextureEntry(CTerrainPropertiesPtr props, const VfsPath& path); ~CTerrainTextureEntry(); const CStr& GetTag() const { return m_Tag; } const CTerrainProperties& GetProperties() const { return *m_pProperties; } // Get texture handle, load texture if not loaded. const CTexturePtr& GetTexture() const { return m_Material.GetDiffuseTexture(); } const CMaterial& GetMaterial() const { return m_Material; } // Returns a matrix of the form [c 0 -s 0; -s 0 -c 0; 0 0 0 0; 0 0 0 1] // mapping world-space (x,y,z,1) coordinates onto (u,v,0,1) texcoords const float* GetTextureMatrix() const; // Used in Atlas to retrieve a texture for previews. Can't use textures // directly because they're required on CPU side. Another solution is to // retrieve path from diffuse texture from material. const VfsPath& GetDiffuseTexturePath() const { return m_DiffuseTexturePath; } // Get mipmap color in BGRA format u32 GetBaseColor() { if (!m_BaseColorValid) BuildBaseColor(); return m_BaseColor; } CTerrainTextureManager::TerrainAlphaMap::iterator m_TerrainAlpha; private: // Tag = file name stripped of path and extension (grass_dark_1) CStr m_Tag; VfsPath m_DiffuseTexturePath; // The property sheet used by this texture CTerrainPropertiesPtr m_pProperties; CMaterial m_Material; CMatrix3D m_TextureMatrix; // BGRA color of topmost mipmap level, for coloring minimap, or a color // specified by the terrain properties u32 m_BaseColor; // ..Valid is true if the base color has been cached bool m_BaseColorValid; // All terrain type groups we're a member of GroupVector m_Groups; // calculate the root color of the texture, used for coloring minimap void BuildBaseColor(); - - void LoadAlphaMaps(const VfsPath& alphaMapType); }; #endif Index: ps/trunk/source/graphics/TerrainTextureManager.cpp =================================================================== --- ps/trunk/source/graphics/TerrainTextureManager.cpp (revision 26183) +++ ps/trunk/source/graphics/TerrainTextureManager.cpp (revision 26184) @@ -1,146 +1,335 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 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 "TerrainTextureManager.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainProperties.h" +#include "graphics/TextureManager.h" +#include "lib/allocators/shared_ptr.h" +#include "lib/bits.h" +#include "lib/ogl.h" +#include "lib/tex/tex.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" +#include "renderer/Renderer.h" #include #include #include CTerrainTextureManager::CTerrainTextureManager() : m_LastGroupIndex(0) { if (!VfsDirectoryExists(L"art/terrains/")) return; if (!CXeromyces::AddValidator(g_VFS, "terrain", "art/terrains/terrain.rng")) LOGERROR("CTerrainTextureManager: failed to load grammar file 'art/terrains/terrain.rng'"); if (!CXeromyces::AddValidator(g_VFS, "terrain_texture", "art/terrains/terrain_texture.rng")) LOGERROR("CTerrainTextureManager: failed to load grammar file 'art/terrains/terrain_texture.rng'"); } CTerrainTextureManager::~CTerrainTextureManager() { UnloadTerrainTextures(); for (std::pair& ta : m_TerrainAlphas) ta.second.m_CompositeAlphaMap.reset(); } void CTerrainTextureManager::UnloadTerrainTextures() { for (CTerrainTextureEntry* const& te : m_TextureEntries) delete te; m_TextureEntries.clear(); for (const std::pair& tg : m_TerrainGroups) delete tg.second; m_TerrainGroups.clear(); m_LastGroupIndex = 0; } CTerrainTextureEntry* CTerrainTextureManager::FindTexture(const CStr& tag_) const { CStr tag = tag_.BeforeLast("."); // Strip extension for (CTerrainTextureEntry* const& te : m_TextureEntries) if (te->GetTag() == tag) return te; LOGWARNING("CTerrainTextureManager: Couldn't find terrain %s", tag.c_str()); return 0; } CTerrainTextureEntry* CTerrainTextureManager::AddTexture(const CTerrainPropertiesPtr& props, const VfsPath& path) { CTerrainTextureEntry* entry = new CTerrainTextureEntry(props, path); m_TextureEntries.push_back(entry); return entry; } void CTerrainTextureManager::DeleteTexture(CTerrainTextureEntry* entry) { std::vector::iterator it = std::find(m_TextureEntries.begin(), m_TextureEntries.end(), entry); if (it != m_TextureEntries.end()) m_TextureEntries.erase(it); delete entry; } struct AddTextureCallbackData { CTerrainTextureManager* self; CTerrainPropertiesPtr props; }; static Status AddTextureDirCallback(const VfsPath& pathname, const uintptr_t cbData) { AddTextureCallbackData& data = *(AddTextureCallbackData*)cbData; VfsPath path = pathname / L"terrains.xml"; if (!VfsFileExists(path)) LOGMESSAGE("'%s' does not exist. Using previous properties.", path.string8()); else data.props = CTerrainProperties::FromXML(data.props, path); return INFO::OK; } static Status AddTextureCallback(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) { AddTextureCallbackData& data = *(AddTextureCallbackData*)cbData; if (pathname.Basename() != L"terrains") data.self->AddTexture(data.props, pathname); return INFO::OK; } int CTerrainTextureManager::LoadTerrainTextures() { AddTextureCallbackData data = {this, CTerrainPropertiesPtr(new CTerrainProperties(CTerrainPropertiesPtr()))}; vfs::ForEachFile(g_VFS, L"art/terrains/", AddTextureCallback, (uintptr_t)&data, L"*.xml", vfs::DIR_RECURSIVE, AddTextureDirCallback, (uintptr_t)&data); return 0; } CTerrainGroup* CTerrainTextureManager::FindGroup(const CStr& name) { TerrainGroupMap::const_iterator it = m_TerrainGroups.find(name); if (it != m_TerrainGroups.end()) return it->second; else return m_TerrainGroups[name] = new CTerrainGroup(name, ++m_LastGroupIndex); } +// LoadAlphaMaps: load the 14 default alpha maps, pack them into one composite texture and +// calculate the coordinate of each alphamap within this packed texture. +CTerrainTextureManager::TerrainAlphaMap::iterator +CTerrainTextureManager::LoadAlphaMap(const VfsPath& alphaMapType) +{ + const std::wstring key = L"(alpha map composite" + alphaMapType.string() + L")"; + + CTerrainTextureManager::TerrainAlphaMap::iterator it = m_TerrainAlphas.find(alphaMapType); + + if (it != g_TexMan.m_TerrainAlphas.end()) + return it; + + m_TerrainAlphas[alphaMapType] = TerrainAlpha(); + it = m_TerrainAlphas.find(alphaMapType); + + TerrainAlpha& result = it->second; + + // + // load all textures and store Handle in array + // + Tex textures[NUM_ALPHA_MAPS] = {}; + const VfsPath path = VfsPath("art/textures/terrain/alphamaps") / alphaMapType; + + const wchar_t* fnames[NUM_ALPHA_MAPS] = + { + L"blendcircle.png", + L"blendlshape.png", + L"blendedge.png", + L"blendedgecorner.png", + L"blendedgetwocorners.png", + L"blendfourcorners.png", + L"blendtwooppositecorners.png", + L"blendlshapecorner.png", + L"blendtwocorners.png", + L"blendcorner.png", + L"blendtwoedges.png", + L"blendthreecorners.png", + L"blendushape.png", + L"blendbad.png" + }; + size_t base = 0; // texture width/height (see below) + // For convenience, we require all alpha maps to be of the same BPP. + size_t bpp = 0; + for (size_t i = 0; i < NUM_ALPHA_MAPS; ++i) + { + // note: these individual textures can be discarded afterwards; + // we cache the composite. + std::shared_ptr fileData; + size_t fileSize; + if (g_VFS->LoadFile(path / fnames[i], fileData, fileSize) != INFO::OK || + textures[i].decode(fileData, fileSize) != INFO::OK) + { + m_TerrainAlphas.erase(it); + LOGERROR("Failed to load alphamap: %s", alphaMapType.string8()); + + const VfsPath standard("standard"); + if (path != standard) + return LoadAlphaMap(standard); + return m_TerrainAlphas.end(); + } + + // Get its size and make sure they are all equal. + // (the packing algo assumes this). + if (textures[i].m_Width != textures[i].m_Height) + DEBUG_DISPLAY_ERROR(L"Alpha maps are not square"); + // .. first iteration: establish size + if (i == 0) + { + base = textures[i].m_Width; + bpp = textures[i].m_Bpp; + } + // .. not first: make sure texture size matches + else if (base != textures[i].m_Width || bpp != textures[i].m_Bpp) + DEBUG_DISPLAY_ERROR(L"Alpha maps are not identically sized (including pixel depth)"); + } + + // + // copy each alpha map (tile) into one buffer, arrayed horizontally. + // + const size_t tileWidth = 2 + base + 2; // 2 pixel border (avoids bilinear filtering artifacts) + const size_t totalWidth = round_up_to_pow2(tileWidth * NUM_ALPHA_MAPS); + const size_t totalHeight = base; ENSURE(is_pow2(totalHeight)); + std::shared_ptr data; + AllocateAligned(data, totalWidth * totalHeight, maxSectorSize); + // for each tile on row + for (size_t i = 0; i < NUM_ALPHA_MAPS; ++i) + { + // get src of copy + u8* src = textures[i].get_data(); + ENSURE(src); + + const size_t srcStep = bpp / 8; + + // get destination of copy + u8* dst = data.get() + (i * tileWidth); + + // for each row of image + for (size_t j = 0; j < base; ++j) + { + // duplicate first pixel + *dst++ = *src; + *dst++ = *src; + + // copy a row + for (size_t k = 0; k < base; ++k) + { + *dst++ = *src; + src += srcStep; + } + + // duplicate last pixel + *dst++ = *(src - srcStep); + *dst++ = *(src - srcStep); + + // advance write pointer for next row + dst += totalWidth - tileWidth; + } + + result.m_AlphaMapCoords[i].u0 = static_cast(i * tileWidth + 2) / totalWidth; + result.m_AlphaMapCoords[i].u1 = static_cast((i + 1) * tileWidth - 2) / totalWidth; + result.m_AlphaMapCoords[i].v0 = 0.0f; + result.m_AlphaMapCoords[i].v1 = 1.0f; + } + + for (size_t i = 0; i < NUM_ALPHA_MAPS; ++i) + textures[i].free(); + + // Enable the following to save a png of the generated texture + // in the public/ directory, for debugging. +#if 0 + Tex t; + ignore_result(t.wrap(totalWidth, totalHeight, 8, TEX_GREY, data, 0)); + + const VfsPath filename("blendtex.png"); + + DynArray da; + RETURN_STATUS_IF_ERR(tex_encode(&t, filename.Extension(), &da)); + + // write to disk + //Status ret = INFO::OK; + { + std::shared_ptr file = DummySharedPtr(da.base); + const ssize_t bytes_written = g_VFS->CreateFile(filename, file, da.pos); + if (bytes_written > 0) + ENSURE(bytes_written == (ssize_t)da.pos); + //else + // ret = (Status)bytes_written; + } + + ignore_result(da_free(&da)); +#endif + + result.m_CompositeAlphaMap = Renderer::Backend::GL::CTexture::Create2D( + Renderer::Backend::Format::A8, totalWidth, totalHeight, + Renderer::Backend::Sampler::MakeDefaultSampler( + Renderer::Backend::Sampler::Filter::LINEAR, + Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE)); + + result.m_CompositeDataToUpload = std::move(data); + + m_AlphaMapsToUpload.emplace_back(it); + + return it; +} + +void CTerrainTextureManager::UploadResourcesIfNeeded( + Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext) +{ + for (const CTerrainTextureManager::TerrainAlphaMap::iterator& it : m_AlphaMapsToUpload) + { + TerrainAlpha& alphaMap = it->second; + if (!alphaMap.m_CompositeDataToUpload) + continue; + // Upload the composite texture. + deviceCommandContext->UploadTexture(alphaMap.m_CompositeAlphaMap.get(), + Renderer::Backend::Format::A8, alphaMap.m_CompositeDataToUpload.get(), + alphaMap.m_CompositeAlphaMap->GetWidth() * alphaMap.m_CompositeAlphaMap->GetHeight()); + alphaMap.m_CompositeDataToUpload.reset(); + } + + m_AlphaMapsToUpload.clear(); +} + void CTerrainGroup::AddTerrain(CTerrainTextureEntry* pTerrain) { m_Terrains.push_back(pTerrain); } void CTerrainGroup::RemoveTerrain(CTerrainTextureEntry* pTerrain) { std::vector::iterator it = find(m_Terrains.begin(), m_Terrains.end(), pTerrain); if (it != m_Terrains.end()) m_Terrains.erase(it); } Index: ps/trunk/source/graphics/TerrainTextureManager.h =================================================================== --- ps/trunk/source/graphics/TerrainTextureManager.h (revision 26183) +++ ps/trunk/source/graphics/TerrainTextureManager.h (revision 26184) @@ -1,134 +1,143 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 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_TERRAINTEXTUREMANAGER #define INCLUDED_TERRAINTEXTUREMANAGER #include "lib/file/vfs/vfs_path.h" #include "lib/res/handle.h" #include "ps/CStr.h" #include "ps/Singleton.h" +#include "renderer/backend/gl/DeviceCommandContext.h" #include "renderer/backend/gl/Texture.h" #include #include #include // access to sole CTerrainTextureManager object #define g_TexMan CTerrainTextureManager::GetSingleton() #define NUM_ALPHA_MAPS 14 class CTerrainTextureEntry; class CTerrainProperties; typedef std::shared_ptr CTerrainPropertiesPtr; class CTerrainGroup { // name of this terrain group (as specified by the terrain XML) CStr m_Name; // "index".. basically a bogus integer that can be used by ScEd to set texture // priorities size_t m_Index; // list of textures of this type (found from the texture directory) std::vector m_Terrains; public: CTerrainGroup(CStr name, size_t index): m_Name(name), m_Index(index) {} // Add a texture entry to this terrain type void AddTerrain(CTerrainTextureEntry*); // Remove a texture entry void RemoveTerrain(CTerrainTextureEntry*); size_t GetIndex() const { return m_Index; } CStr GetName() const { return m_Name; } const std::vector &GetTerrains() const { return m_Terrains; } }; struct TerrainAlpha { // Composite alpha map (all the alpha maps packed into one texture). std::unique_ptr m_CompositeAlphaMap; + // Data is used to separate file loading and uploading to GPU. + std::shared_ptr m_CompositeDataToUpload; // Coordinates of each (untransformed) alpha map within the packed texture. struct { float u0, u1, v0, v1; } m_AlphaMapCoords[NUM_ALPHA_MAPS]; }; /////////////////////////////////////////////////////////////////////////////////////////// // CTerrainTextureManager : manager class for all terrain texture objects class CTerrainTextureManager : public Singleton { friend class CTerrainTextureEntry; public: - typedef std::map TerrainGroupMap; - typedef std::map TerrainAlphaMap; + using TerrainGroupMap = std::map; + using TerrainAlphaMap = std::map; -private: - // All texture entries created by this class, for easy freeing now that - // textures may be in several STextureType's - std::vector m_TextureEntries; - - TerrainGroupMap m_TerrainGroups; - - TerrainAlphaMap m_TerrainAlphas; - - size_t m_LastGroupIndex; - -public: // constructor, destructor CTerrainTextureManager(); ~CTerrainTextureManager(); // Find all XML's in the directory (with subdirs) and try to load them as // terrain XML's int LoadTerrainTextures(); void UnloadTerrainTextures(); CTerrainTextureEntry* FindTexture(const CStr& tag) const; // Create a texture object for a new terrain texture at path, using the // property sheet props. CTerrainTextureEntry* AddTexture(const CTerrainPropertiesPtr& props, const VfsPath& path); // Remove the texture from all our maps and lists and delete it afterwards. void DeleteTexture(CTerrainTextureEntry* entry); // Find or create a new texture group. All terrain groups are owned by the // texturemanager (TerrainTypeManager) CTerrainGroup* FindGroup(const CStr& name); const TerrainGroupMap& GetGroups() const { return m_TerrainGroups; } -}; + CTerrainTextureManager::TerrainAlphaMap::iterator LoadAlphaMap(const VfsPath& alphaMapType); + + void UploadResourcesIfNeeded(Renderer::Backend::GL::CDeviceCommandContext* deviceCommandContext); + +private: + // All texture entries created by this class, for easy freeing now that + // textures may be in several STextureType's + std::vector m_TextureEntries; + + TerrainGroupMap m_TerrainGroups; + + TerrainAlphaMap m_TerrainAlphas; + + size_t m_LastGroupIndex; + + // A way to separate file loading and uploading to GPU to not stall uploading. + // Once we get a properly threaded loading we might optimize that. + std::vector m_AlphaMapsToUpload; +}; -#endif +#endif // INCLUDED_TERRAINTEXTUREMANAGER Index: ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp =================================================================== --- ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp (revision 26183) +++ ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp (revision 26184) @@ -1,460 +1,461 @@ /* Copyright (C) 2022 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 "CMiniMap.h" #include "graphics/Canvas2D.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/MiniMapTexture.h" #include "graphics/MiniPatch.h" #include "graphics/ShaderManager.h" #include "graphics/ShaderProgramPtr.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" +#include "graphics/TextureManager.h" #include "gui/CGUI.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 "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/SceneRenderer.h" #include "renderer/WaterManager.h" #include "scriptinterface/Object.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpMinimap.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/helpers/Los.h" #include "simulation2/system/ParamNode.h" #include #include #include namespace { // Adds segments pieces lying inside the circle to lines. void CropPointsByCircle(const std::array& points, const CVector3D& center, const float radius, std::vector* lines) { constexpr float EPS = 1e-3f; lines->reserve(points.size() * 2); for (size_t idx = 0; idx < points.size(); ++idx) { const CVector3D& currentPoint = points[idx]; const CVector3D& nextPoint = points[(idx + 1) % points.size()]; const CVector3D direction = (nextPoint - currentPoint).Normalized(); const CVector3D normal(direction.Z, 0.0f, -direction.X); const float offset = normal.Dot(currentPoint) - normal.Dot(center); // We need to have lines only inside the circle. if (std::abs(offset) + EPS >= radius) continue; const CVector3D closestPoint = center + normal * offset; const float halfChordLength = sqrt(radius * radius - offset * offset); const CVector3D intersectionA = closestPoint - direction * halfChordLength; const CVector3D intersectionB = closestPoint + direction * halfChordLength; // We have no intersection if the segment is lying outside of the circle. if (direction.Dot(currentPoint) + EPS > direction.Dot(intersectionB) || direction.Dot(nextPoint) - EPS < direction.Dot(intersectionA)) continue; lines->emplace_back( direction.Dot(currentPoint) > direction.Dot(intersectionA) ? currentPoint : intersectionA); lines->emplace_back( direction.Dot(nextPoint) < direction.Dot(intersectionB) ? nextPoint : intersectionB); } } void DrawTexture(CShaderProgramPtr shader, float angle, float x, float y, float x2, float y2, float mapScale) { // 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) * mapScale; const float c = cos(angle) * mapScale; const float m = 0.5f; 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, 0.0f, x2, y, 0.0f, x2, y2, 0.0f, x2, y2, 0.0f, x, y2, 0.0f, x, y, 0.0f }; shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 0, quadTex); shader->VertexPointer(3, GL_FLOAT, 0, quadVerts); shader->AssertPointersBound(); glDrawArrays(GL_TRIANGLES, 0, 6); } } // anonymous namespace const CStr CMiniMap::EventNameWorldClick = "WorldClick"; CMiniMap::CMiniMap(CGUI& pGUI) : IGUIObject(pGUI), m_MapSize(0), m_MapScale(1.f), m_Mask(this, "mask", false), m_FlareTextureCount(this, "flare_texture_count", 0), m_FlareRenderSize(this, "flare_render_size", 0), m_FlareInterleave(this, "flare_interleave", false), m_FlareAnimationSpeed(this, "flare_animation_speed", 0.0f), m_FlareLifetimeSeconds(this, "flare_lifetime_seconds", 0.0f), m_FlareStartFadeSeconds(this, "flare_start_fade_seconds", 0.0f), m_FlareStopFadeSeconds(this, "flare_stop_fade_seconds", 0.0f) { m_Clicking = false; m_MouseHovering = false; } CMiniMap::~CMiniMap() = default; void CMiniMap::HandleMessage(SGUIMessage& Message) { IGUIObject::HandleMessage(Message); switch (Message.type) { case GUIM_LOAD: RecreateFlareTextures(); break; case GUIM_SETTINGS_UPDATED: if (Message.value == "flare_texture_count") RecreateFlareTextures(); break; case GUIM_MOUSE_PRESS_LEFT: if (m_MouseHovering) { if (!CMiniMap::FireWorldClickEvent(SDL_BUTTON_LEFT, 1)) { SetCameraPositionFromMousePosition(); m_Clicking = true; } } break; case GUIM_MOUSE_RELEASE_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPositionFromMousePosition(); m_Clicking = false; break; case GUIM_MOUSE_DBLCLICK_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPositionFromMousePosition(); 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) SetCameraPositionFromMousePosition(); break; case GUIM_MOUSE_WHEEL_DOWN: case GUIM_MOUSE_WHEEL_UP: Message.Skip(); break; default: break; } } void CMiniMap::RecreateFlareTextures() { // Catch invalid values. if (m_FlareTextureCount > 99) { LOGERROR("Invalid value for flare texture count. Valid range is 0-99."); return; } const CStr textureNumberingFormat = "art/textures/animated/minimap-flare/frame%02u.png"; m_FlareTextures.clear(); m_FlareTextures.reserve(m_FlareTextureCount); for (u32 i = 0; i < m_FlareTextureCount; ++i) { const CTextureProperties textureProps(fmt::sprintf(textureNumberingFormat, i).c_str()); m_FlareTextures.emplace_back(g_Renderer.GetTextureManager().CreateTexture(textureProps)); } } bool CMiniMap::IsMouseOver() const { const CVector2D& mousePos = m_pGUI.GetMousePos(); // Take the magnitude of the difference of the mouse position and minimap center. const float distanceFromCenter = (mousePos - m_CachedActualSize.CenterPoint()).Length(); // If the distance is less then the radius of the minimap (half the width) the mouse is over the minimap. return distanceFromCenter < m_CachedActualSize.GetWidth() / 2.0; } void CMiniMap::GetMouseWorldCoordinates(float& x, float& z) const { // Determine X and Z according to proportion of mouse position and minimap. const CVector2D& mousePos = m_pGUI.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::SetCameraPositionFromMousePosition() { 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() const { CVector3D cameraIn = g_Game->GetView()->GetCamera()->GetOrientation().GetIn(); return -atan2(cameraIn.X, cameraIn.Z); } CVector2D CMiniMap::WorldSpaceToMiniMapSpace(const CVector3D& worldPosition) const { // Coordinates with 0,0 in the middle of the minimap and +-0.5 as max. const float invTileMapSize = 1.0f / static_cast(TERRAIN_TILE_SIZE * m_MapSize); const float relativeX = (worldPosition.X * invTileMapSize - 0.5) / m_MapScale; const float relativeY = (worldPosition.Z * invTileMapSize - 0.5) / m_MapScale; // Rotate coordinates. const float angle = GetAngle(); const float rotatedX = cos(angle) * relativeX + sin(angle) * relativeY; const float rotatedY = -sin(angle) * relativeX + cos(angle) * relativeY; // Calculate coordinates in GUI space. return CVector2D( m_CachedActualSize.left + (0.5f + rotatedX) * m_CachedActualSize.GetWidth(), m_CachedActualSize.bottom - (0.5f + rotatedY) * m_CachedActualSize.GetHeight()); } bool CMiniMap::FireWorldClickEvent(int button, int UNUSED(clicks)) { ScriptRequest rq(g_GUI->GetActiveGUI()->GetScriptInterface()); float x, z; GetMouseWorldCoordinates(x, z); JS::RootedValue coords(rq.cx); Script::CreateObject(rq, &coords, "x", x, "z", z); JS::RootedValue buttonJs(rq.cx); Script::ToJSVal(rq, &buttonJs, button); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(coords)); ignore_result(paramData.append(buttonJs)); return ScriptEventWithReturn(EventNameWorldClick, paramData); } // This sets up and draws the rectangle on the minimap // which represents the view of the camera in the world. void CMiniMap::DrawViewRect(CCanvas2D& canvas) const { // 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 const float sampleHeight = g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight; const CCamera* camera = g_Game->GetView()->GetCamera(); const std::array hitPoints = { camera->GetWorldCoordinates(0, g_Renderer.GetHeight(), sampleHeight), camera->GetWorldCoordinates(g_Renderer.GetWidth(), g_Renderer.GetHeight(), sampleHeight), camera->GetWorldCoordinates(g_Renderer.GetWidth(), 0, sampleHeight), camera->GetWorldCoordinates(0, 0, sampleHeight) }; std::vector worldSpaceLines; // We need to prevent drawing view bounds out of the map. const float halfMapSize = static_cast((m_MapSize - 1) * TERRAIN_TILE_SIZE) * 0.5f; CropPointsByCircle(hitPoints, CVector3D(halfMapSize, 0.0f, halfMapSize), halfMapSize * m_MapScale, &worldSpaceLines); if (worldSpaceLines.empty()) return; for (size_t index = 0; index < worldSpaceLines.size() && index + 1 < worldSpaceLines.size(); index += 2) { const CVector2D from = WorldSpaceToMiniMapSpace(worldSpaceLines[index]); const CVector2D to = WorldSpaceToMiniMapSpace(worldSpaceLines[index + 1]); canvas.DrawLine({from, to}, 2.0f, CColor(1.0f, 0.3f, 0.3f, 1.0f)); } } void CMiniMap::DrawFlare(CCanvas2D& canvas, const MapFlare& flare, double currentTime) const { if (m_FlareTextures.empty()) return; const CVector2D flareCenter = WorldSpaceToMiniMapSpace(CVector3D(flare.pos.X, 0.0f, flare.pos.Y)); const CRect destination( flareCenter.X - m_FlareRenderSize, flareCenter.Y - m_FlareRenderSize, flareCenter.X + m_FlareRenderSize, flareCenter.Y + m_FlareRenderSize); const double deltaTime = currentTime - flare.time; const double remainingTime = m_FlareLifetimeSeconds - deltaTime; const u32 flooredStep = floor(deltaTime * m_FlareAnimationSpeed); const float startFadeAlpha = m_FlareStartFadeSeconds > 0.0f ? deltaTime / m_FlareStartFadeSeconds : 1.0f; const float stopFadeAlpha = m_FlareStopFadeSeconds > 0.0f ? remainingTime / m_FlareStopFadeSeconds : 1.0f; const float alpha = Clamp(std::min( SmoothStep(0.0f, 1.0f, startFadeAlpha), SmoothStep(0.0f, 1.0f, stopFadeAlpha)), 0.0f, 1.0f); DrawFlareFrame(canvas, flooredStep % m_FlareTextures.size(), destination, flare.color, alpha); // Draw a second circle if the first has reached half of the animation. if (m_FlareInterleave && flooredStep >= m_FlareTextures.size() / 2) { DrawFlareFrame(canvas, (flooredStep - m_FlareTextures.size() / 2) % m_FlareTextures.size(), destination, flare.color, alpha); } } void CMiniMap::DrawFlareFrame(CCanvas2D& canvas, const u32 frameIndex, const CRect& destination, const CColor& color, float alpha) const { // TODO: Only draw inside the minimap circle. CTexturePtr texture = m_FlareTextures[frameIndex % m_FlareTextures.size()]; CColor finalColor = color; finalColor.a *= alpha; canvas.DrawTexture(texture, destination, CRect(0, 0, texture->GetWidth(), texture->GetHeight()), finalColor, CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f); } void CMiniMap::Draw(CCanvas2D& canvas) { 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 (!g_Game || !g_Game->IsGameStarted()) return; if (!m_Mask) canvas.DrawRect(m_CachedActualSize, CColor(0.0f, 0.0f, 0.0f, 1.0f)); canvas.Flush(); CSimulation2* sim = g_Game->GetSimulation2(); CmpPtr cmpRangeManager(*sim, SYSTEM_ENTITY); ENSURE(cmpRangeManager); // Set our globals in case they hadn't been set before const CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); m_MapSize = terrain->GetVerticesPerSide(); m_MapScale = (cmpRangeManager->GetLosCircular() ? 1.f : 1.414f); // Draw the main textured quad CMiniMapTexture& miniMapTexture = g_Game->GetView()->GetMiniMapTexture(); if (miniMapTexture.GetTexture()) { CShaderProgramPtr shader; CShaderTechniquePtr tech; CShaderDefines baseDefines; baseDefines.Add(str_MINIMAP_BASE, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, baseDefines); tech->BeginPass(); shader = tech->GetShader(); shader->BindTexture(str_baseTex, miniMapTexture.GetTexture()); const CMatrix3D baseTransform = GetDefaultGuiMatrix(); CMatrix3D baseTextureTransform; baseTextureTransform.SetIdentity(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, baseTextureTransform); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); const float x = m_CachedActualSize.left, y = m_CachedActualSize.bottom; const float x2 = m_CachedActualSize.right, y2 = m_CachedActualSize.top; const float angle = GetAngle(); DrawTexture(shader, angle, x, y, x2, y2, m_MapScale); tech->EndPass(); glDisable(GL_BLEND); } PROFILE_START("minimap flares"); DrawViewRect(canvas); const double currentTime = timer_Time(); while (!m_MapFlares.empty() && m_FlareLifetimeSeconds + m_MapFlares.front().time < currentTime) m_MapFlares.pop_front(); for (const MapFlare& flare : m_MapFlares) DrawFlare(canvas, flare, currentTime); PROFILE_END("minimap flares"); } bool CMiniMap::Flare(const CVector2D& pos, const CStr& colorStr) { CColor color; if (!color.ParseString(colorStr)) { LOGERROR("CMiniMap::Flare: Couldn't parse color string"); return false; } m_MapFlares.push_back({ pos, color, timer_Time() }); return true; } Index: ps/trunk/source/renderer/PatchRData.cpp =================================================================== --- ps/trunk/source/renderer/PatchRData.cpp (revision 26183) +++ ps/trunk/source/renderer/PatchRData.cpp (revision 26184) @@ -1,1436 +1,1437 @@ /* Copyright (C) 2022 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 "renderer/PatchRData.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/LOSTexture.h" #include "graphics/Patch.h" #include "graphics/ShaderManager.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TextRenderer.h" +#include "graphics/TextureManager.h" #include "lib/allocators/DynamicArena.h" #include "lib/allocators/STLAllocators.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/VideoMode.h" #include "ps/World.h" #include "renderer/AlphaMapCalculator.h" #include "renderer/DebugRenderer.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/TerrainRenderer.h" #include "renderer/WaterManager.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/Simulation2.h" #include #include #include const ssize_t BlendOffsets[9][2] = { { 0, -1 }, { -1, -1 }, { -1, 0 }, { -1, 1 }, { 0, 1 }, { 1, 1 }, { 1, 0 }, { 1, -1 }, { 0, 0 } }; CPatchRData::CPatchRData(CPatch* patch, CSimulation2* simulation) : m_Patch(patch), m_VBSides(), m_VBBase(), m_VBBaseIndices(), m_VBBlends(), m_VBBlendIndices(), m_VBWater(), m_VBWaterIndices(), m_VBWaterShore(), m_VBWaterIndicesShore(), m_Simulation(simulation) { ENSURE(patch); Build(); } CPatchRData::~CPatchRData() = default; /** * Represents a blend for a single tile, texture and shape. */ struct STileBlend { CTerrainTextureEntry* m_Texture; int m_Priority; u16 m_TileMask; // bit n set if this blend contains neighbour tile BlendOffsets[n] struct DecreasingPriority { bool operator()(const STileBlend& a, const STileBlend& b) const { if (a.m_Priority > b.m_Priority) return true; if (a.m_Priority < b.m_Priority) return false; if (a.m_Texture && b.m_Texture) return a.m_Texture->GetTag() > b.m_Texture->GetTag(); return false; } }; struct CurrentTile { bool operator()(const STileBlend& a) const { return (a.m_TileMask & (1 << 8)) != 0; } }; }; /** * Represents the ordered collection of blends drawn on a particular tile. */ struct STileBlendStack { u8 i, j; std::vector blends; // back of vector is lowest-priority texture }; /** * Represents a batched collection of blends using the same texture. */ struct SBlendLayer { struct Tile { u8 i, j; u8 shape; }; CTerrainTextureEntry* m_Texture; std::vector m_Tiles; }; void CPatchRData::BuildBlends() { PROFILE3("build blends"); m_BlendSplats.clear(); std::vector blendVertices; std::vector blendIndices; CTerrain* terrain = m_Patch->m_Parent; std::vector blendStacks; blendStacks.reserve(PATCH_SIZE*PATCH_SIZE); std::vector blends; blends.reserve(9); // For each tile in patch .. for (ssize_t j = 0; j < PATCH_SIZE; ++j) { for (ssize_t i = 0; i < PATCH_SIZE; ++i) { ssize_t gx = m_Patch->m_X * PATCH_SIZE + i; ssize_t gz = m_Patch->m_Z * PATCH_SIZE + j; blends.clear(); // Compute a blend for every tile in the 3x3 square around this tile for (size_t n = 0; n < 9; ++n) { ssize_t ox = gx + BlendOffsets[n][1]; ssize_t oz = gz + BlendOffsets[n][0]; CMiniPatch* nmp = terrain->GetTile(ox, oz); if (!nmp) continue; STileBlend blend; blend.m_Texture = nmp->GetTextureEntry(); blend.m_Priority = nmp->GetPriority(); blend.m_TileMask = 1 << n; blends.push_back(blend); } // Sort the blends, highest priority first std::sort(blends.begin(), blends.end(), STileBlend::DecreasingPriority()); STileBlendStack blendStack; blendStack.i = i; blendStack.j = j; // Put the blends into the tile's stack, merging any adjacent blends with the same texture for (size_t k = 0; k < blends.size(); ++k) { if (!blendStack.blends.empty() && blendStack.blends.back().m_Texture == blends[k].m_Texture) blendStack.blends.back().m_TileMask |= blends[k].m_TileMask; else blendStack.blends.push_back(blends[k]); } // Remove blends that are after (i.e. lower priority than) the current tile // (including the current tile), since we don't want to render them on top of // the tile's base texture blendStack.blends.erase( std::find_if(blendStack.blends.begin(), blendStack.blends.end(), STileBlend::CurrentTile()), blendStack.blends.end()); blendStacks.push_back(blendStack); } } // Given the blend stack per tile, we want to batch together as many blends as possible. // Group them into a series of layers (each of which has a single texture): // (This is effectively a topological sort / linearisation of the partial order induced // by the per-tile stacks, preferring to make tiles with equal textures adjacent.) std::vector blendLayers; while (true) { if (!blendLayers.empty()) { // Try to grab as many tiles as possible that match our current layer, // from off the blend stacks of all the tiles CTerrainTextureEntry* tex = blendLayers.back().m_Texture; for (size_t k = 0; k < blendStacks.size(); ++k) { if (!blendStacks[k].blends.empty() && blendStacks[k].blends.back().m_Texture == tex) { SBlendLayer::Tile t = { blendStacks[k].i, blendStacks[k].j, (u8)blendStacks[k].blends.back().m_TileMask }; blendLayers.back().m_Tiles.push_back(t); blendStacks[k].blends.pop_back(); } // (We've already merged adjacent entries of the same texture in each stack, // so we don't need to bother looping to check the next entry in this stack again) } } // We've grabbed as many tiles as possible; now we need to start a new layer. // The new layer's texture could come from the back of any non-empty stack; // choose the longest stack as a heuristic to reduce the number of layers CTerrainTextureEntry* bestTex = NULL; size_t bestStackSize = 0; for (size_t k = 0; k < blendStacks.size(); ++k) { if (blendStacks[k].blends.size() > bestStackSize) { bestStackSize = blendStacks[k].blends.size(); bestTex = blendStacks[k].blends.back().m_Texture; } } // If all our stacks were empty, we're done if (bestStackSize == 0) break; // Otherwise add the new layer, then loop back and start filling it in SBlendLayer layer; layer.m_Texture = bestTex; blendLayers.push_back(layer); } // Now build outgoing splats m_BlendSplats.resize(blendLayers.size()); for (size_t k = 0; k < blendLayers.size(); ++k) { SSplat& splat = m_BlendSplats[k]; splat.m_IndexStart = blendIndices.size(); splat.m_Texture = blendLayers[k].m_Texture; for (size_t t = 0; t < blendLayers[k].m_Tiles.size(); ++t) { SBlendLayer::Tile& tile = blendLayers[k].m_Tiles[t]; AddBlend(blendVertices, blendIndices, tile.i, tile.j, tile.shape, splat.m_Texture); } splat.m_IndexCount = blendIndices.size() - splat.m_IndexStart; } // Release existing vertex buffer chunks m_VBBlends.Reset(); m_VBBlendIndices.Reset(); if (blendVertices.size()) { // Construct vertex buffer m_VBBlends = g_VBMan.AllocateChunk(sizeof(SBlendVertex), blendVertices.size(), GL_STATIC_DRAW, GL_ARRAY_BUFFER, nullptr, CVertexBufferManager::Group::TERRAIN); m_VBBlends->m_Owner->UpdateChunkVertices(m_VBBlends.Get(), &blendVertices[0]); // Update the indices to include the base offset of the vertex data for (size_t k = 0; k < blendIndices.size(); ++k) blendIndices[k] += static_cast(m_VBBlends->m_Index); m_VBBlendIndices = g_VBMan.AllocateChunk(sizeof(u16), blendIndices.size(), GL_STATIC_DRAW, GL_ELEMENT_ARRAY_BUFFER, nullptr, CVertexBufferManager::Group::TERRAIN); m_VBBlendIndices->m_Owner->UpdateChunkVertices(m_VBBlendIndices.Get(), &blendIndices[0]); } } void CPatchRData::AddBlend(std::vector& blendVertices, std::vector& blendIndices, u16 i, u16 j, u8 shape, CTerrainTextureEntry* texture) { CTerrain* terrain = m_Patch->m_Parent; ssize_t gx = m_Patch->m_X * PATCH_SIZE + i; ssize_t gz = m_Patch->m_Z * PATCH_SIZE + j; // uses the current neighbour texture BlendShape8 shape8; for (size_t m = 0; m < 8; ++m) shape8[m] = (shape & (1 << m)) ? 0 : 1; // calculate the required alphamap and the required rotation of the alphamap from blendshape unsigned int alphamapflags; int alphamap = CAlphaMapCalculator::Calculate(shape8, alphamapflags); // now actually render the blend tile (if we need one) if (alphamap == -1) return; float u0 = texture->m_TerrainAlpha->second.m_AlphaMapCoords[alphamap].u0; float u1 = texture->m_TerrainAlpha->second.m_AlphaMapCoords[alphamap].u1; float v0 = texture->m_TerrainAlpha->second.m_AlphaMapCoords[alphamap].v0; float v1 = texture->m_TerrainAlpha->second.m_AlphaMapCoords[alphamap].v1; if (alphamapflags & BLENDMAP_FLIPU) std::swap(u0, u1); if (alphamapflags & BLENDMAP_FLIPV) std::swap(v0, v1); int base = 0; if (alphamapflags & BLENDMAP_ROTATE90) base = 1; else if (alphamapflags & BLENDMAP_ROTATE180) base = 2; else if (alphamapflags & BLENDMAP_ROTATE270) base = 3; SBlendVertex vtx[4]; vtx[(base + 0) % 4].m_AlphaUVs[0] = u0; vtx[(base + 0) % 4].m_AlphaUVs[1] = v0; vtx[(base + 1) % 4].m_AlphaUVs[0] = u1; vtx[(base + 1) % 4].m_AlphaUVs[1] = v0; vtx[(base + 2) % 4].m_AlphaUVs[0] = u1; vtx[(base + 2) % 4].m_AlphaUVs[1] = v1; vtx[(base + 3) % 4].m_AlphaUVs[0] = u0; vtx[(base + 3) % 4].m_AlphaUVs[1] = v1; SBlendVertex dst; CVector3D normal; u16 index = static_cast(blendVertices.size()); terrain->CalcPosition(gx, gz, dst.m_Position); terrain->CalcNormal(gx, gz, normal); dst.m_Normal = normal; dst.m_AlphaUVs[0] = vtx[0].m_AlphaUVs[0]; dst.m_AlphaUVs[1] = vtx[0].m_AlphaUVs[1]; blendVertices.push_back(dst); terrain->CalcPosition(gx + 1, gz, dst.m_Position); terrain->CalcNormal(gx + 1, gz, normal); dst.m_Normal = normal; dst.m_AlphaUVs[0] = vtx[1].m_AlphaUVs[0]; dst.m_AlphaUVs[1] = vtx[1].m_AlphaUVs[1]; blendVertices.push_back(dst); terrain->CalcPosition(gx + 1, gz + 1, dst.m_Position); terrain->CalcNormal(gx + 1, gz + 1, normal); dst.m_Normal = normal; dst.m_AlphaUVs[0] = vtx[2].m_AlphaUVs[0]; dst.m_AlphaUVs[1] = vtx[2].m_AlphaUVs[1]; blendVertices.push_back(dst); terrain->CalcPosition(gx, gz + 1, dst.m_Position); terrain->CalcNormal(gx, gz + 1, normal); dst.m_Normal = normal; dst.m_AlphaUVs[0] = vtx[3].m_AlphaUVs[0]; dst.m_AlphaUVs[1] = vtx[3].m_AlphaUVs[1]; blendVertices.push_back(dst); bool dir = terrain->GetTriangulationDir(gx, gz); if (dir) { blendIndices.push_back(index+0); blendIndices.push_back(index+1); blendIndices.push_back(index+3); blendIndices.push_back(index+1); blendIndices.push_back(index+2); blendIndices.push_back(index+3); } else { blendIndices.push_back(index+0); blendIndices.push_back(index+1); blendIndices.push_back(index+2); blendIndices.push_back(index+2); blendIndices.push_back(index+3); blendIndices.push_back(index+0); } } void CPatchRData::BuildIndices() { PROFILE3("build indices"); CTerrain* terrain = m_Patch->m_Parent; ssize_t px = m_Patch->m_X * PATCH_SIZE; ssize_t pz = m_Patch->m_Z * PATCH_SIZE; // must have allocated some vertices before trying to build corresponding indices ENSURE(m_VBBase); // number of vertices in each direction in each patch ssize_t vsize=PATCH_SIZE+1; // PATCH_SIZE must be 2^8-2 or less to not overflow u16 indices buffer. Thankfully this is always true. ENSURE(vsize*vsize < 65536); std::vector indices; indices.reserve(PATCH_SIZE * PATCH_SIZE * 4); // release existing splats m_Splats.clear(); // build grid of textures on this patch std::vector textures; CTerrainTextureEntry* texgrid[PATCH_SIZE][PATCH_SIZE]; for (ssize_t j=0;jm_MiniPatches[j][i].GetTextureEntry(); texgrid[j][i]=tex; if (std::find(textures.begin(),textures.end(),tex)==textures.end()) { textures.push_back(tex); } } } // now build base splats from interior textures m_Splats.resize(textures.size()); // build indices for base splats size_t base=m_VBBase->m_Index; for (size_t k = 0; k < m_Splats.size(); ++k) { CTerrainTextureEntry* tex = textures[k]; SSplat& splat=m_Splats[k]; splat.m_Texture=tex; splat.m_IndexStart=indices.size(); for (ssize_t j = 0; j < PATCH_SIZE; j++) { for (ssize_t i = 0; i < PATCH_SIZE; i++) { if (texgrid[j][i] == tex) { bool dir = terrain->GetTriangulationDir(px+i, pz+j); if (dir) { indices.push_back(u16(((j+0)*vsize+(i+0))+base)); indices.push_back(u16(((j+0)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+0))+base)); indices.push_back(u16(((j+0)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+0))+base)); } else { indices.push_back(u16(((j+0)*vsize+(i+0))+base)); indices.push_back(u16(((j+0)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+1))+base)); indices.push_back(u16(((j+1)*vsize+(i+0))+base)); indices.push_back(u16(((j+0)*vsize+(i+0))+base)); } } } } splat.m_IndexCount=indices.size()-splat.m_IndexStart; } // Release existing vertex buffer chunk m_VBBaseIndices.Reset(); ENSURE(indices.size()); // Construct vertex buffer m_VBBaseIndices = g_VBMan.AllocateChunk(sizeof(u16), indices.size(), GL_STATIC_DRAW, GL_ELEMENT_ARRAY_BUFFER, nullptr, CVertexBufferManager::Group::TERRAIN); m_VBBaseIndices->m_Owner->UpdateChunkVertices(m_VBBaseIndices.Get(), &indices[0]); } void CPatchRData::BuildVertices() { PROFILE3("build vertices"); // create both vertices and lighting colors // number of vertices in each direction in each patch ssize_t vsize = PATCH_SIZE + 1; std::vector vertices; vertices.resize(vsize * vsize); // get index of this patch ssize_t px = m_Patch->m_X; ssize_t pz = m_Patch->m_Z; CTerrain* terrain = m_Patch->m_Parent; // build vertices for (ssize_t j = 0; j < vsize; ++j) { for (ssize_t i = 0; i < vsize; ++i) { ssize_t ix = px * PATCH_SIZE + i; ssize_t iz = pz * PATCH_SIZE + j; ssize_t v = j * vsize + i; // calculate vertex data terrain->CalcPosition(ix, iz, vertices[v].m_Position); CVector3D normal; terrain->CalcNormal(ix, iz, normal); vertices[v].m_Normal = normal; } } // upload to vertex buffer if (!m_VBBase) m_VBBase = g_VBMan.AllocateChunk(sizeof(SBaseVertex), vsize * vsize, GL_STATIC_DRAW, GL_ARRAY_BUFFER, nullptr, CVertexBufferManager::Group::TERRAIN); m_VBBase->m_Owner->UpdateChunkVertices(m_VBBase.Get(), &vertices[0]); } void CPatchRData::BuildSide(std::vector& vertices, CPatchSideFlags side) { ssize_t vsize = PATCH_SIZE + 1; CTerrain* terrain = m_Patch->m_Parent; CmpPtr cmpWaterManager(*m_Simulation, SYSTEM_ENTITY); for (ssize_t k = 0; k < vsize; k++) { ssize_t gx = m_Patch->m_X * PATCH_SIZE; ssize_t gz = m_Patch->m_Z * PATCH_SIZE; switch (side) { case CPATCH_SIDE_NEGX: gz += k; break; case CPATCH_SIDE_POSX: gx += PATCH_SIZE; gz += PATCH_SIZE-k; break; case CPATCH_SIDE_NEGZ: gx += PATCH_SIZE-k; break; case CPATCH_SIDE_POSZ: gz += PATCH_SIZE; gx += k; break; } CVector3D pos; terrain->CalcPosition(gx, gz, pos); // Clamp the height to the water level float waterHeight = 0.f; if (cmpWaterManager) waterHeight = cmpWaterManager->GetExactWaterLevel(pos.X, pos.Z); pos.Y = std::max(pos.Y, waterHeight); SSideVertex v0, v1; v0.m_Position = pos; v1.m_Position = pos; v1.m_Position.Y = 0; // If this is the start of this tristrip, but we've already got a partial // tristrip, add a couple of degenerate triangles to join the strips properly if (k == 0 && !vertices.empty()) { vertices.push_back(vertices.back()); vertices.push_back(v1); } // Now add the new triangles vertices.push_back(v1); vertices.push_back(v0); } } void CPatchRData::BuildSides() { PROFILE3("build sides"); std::vector sideVertices; int sideFlags = m_Patch->GetSideFlags(); // If no sides are enabled, we don't need to do anything if (!sideFlags) return; // For each side, generate a tristrip by adding a vertex at ground/water // level and a vertex underneath at height 0. if (sideFlags & CPATCH_SIDE_NEGX) BuildSide(sideVertices, CPATCH_SIDE_NEGX); if (sideFlags & CPATCH_SIDE_POSX) BuildSide(sideVertices, CPATCH_SIDE_POSX); if (sideFlags & CPATCH_SIDE_NEGZ) BuildSide(sideVertices, CPATCH_SIDE_NEGZ); if (sideFlags & CPATCH_SIDE_POSZ) BuildSide(sideVertices, CPATCH_SIDE_POSZ); if (sideVertices.empty()) return; if (!m_VBSides) m_VBSides = g_VBMan.AllocateChunk(sizeof(SSideVertex), sideVertices.size(), GL_STATIC_DRAW, GL_ARRAY_BUFFER, nullptr, CVertexBufferManager::Group::DEFAULT); m_VBSides->m_Owner->UpdateChunkVertices(m_VBSides.Get(), &sideVertices[0]); } void CPatchRData::Build() { BuildVertices(); BuildSides(); BuildIndices(); BuildBlends(); BuildWater(); } void CPatchRData::Update(CSimulation2* simulation) { m_Simulation = simulation; if (m_UpdateFlags!=0) { // TODO,RC 11/04/04 - need to only rebuild necessary bits of renderdata rather // than everything; it's complicated slightly because the blends are dependent // on both vertex and index data BuildVertices(); BuildSides(); BuildIndices(); BuildBlends(); BuildWater(); m_UpdateFlags=0; } } // Types used for glMultiDrawElements batching: // To minimise the cost of memory allocations, everything used for computing // batches uses a arena allocator. (All allocations are short-lived so we can // just throw away the whole arena at the end of each frame.) using Arena = Allocators::DynamicArena<1 * MiB>; // std::map types with appropriate arena allocators and default comparison operator template using PooledBatchMap = std::map, ProxyAllocator, Arena>>; // Equivalent to "m[k]", when it returns a arena-allocated std::map (since we can't // use the default constructor in that case) template typename M::mapped_type& PooledMapGet(M& m, const typename M::key_type& k, Arena& arena) { return m.insert(std::make_pair(k, typename M::mapped_type(typename M::mapped_type::key_compare(), typename M::mapped_type::allocator_type(arena)) )).first->second; } // Equivalent to "m[k]", when it returns a std::pair of arena-allocated std::vectors template typename M::mapped_type& PooledPairGet(M& m, const typename M::key_type& k, Arena& arena) { return m.insert(std::make_pair(k, std::make_pair( typename M::mapped_type::first_type(typename M::mapped_type::first_type::allocator_type(arena)), typename M::mapped_type::second_type(typename M::mapped_type::second_type::allocator_type(arena)) ))).first->second; } // Each multidraw batch has a list of index counts, and a list of pointers-to-first-indexes using BatchElements = std::pair>, std::vector>>; // Group batches by index buffer using IndexBufferBatches = PooledBatchMap; // Group batches by vertex buffer using VertexBufferBatches = PooledBatchMap; // Group batches by texture using TextureBatches = PooledBatchMap; // Group batches by shaders. using ShaderTechniqueBatches = PooledBatchMap; void CPatchRData::RenderBases( const std::vector& patches, const CShaderDefines& context, ShadowMap* shadow) { PROFILE3("render terrain bases"); Arena arena; ShaderTechniqueBatches batches(ShaderTechniqueBatches::key_compare(), (ShaderTechniqueBatches::allocator_type(arena))); PROFILE_START("compute batches"); // Collect all the patches' base splats into their appropriate batches for (size_t i = 0; i < patches.size(); ++i) { CPatchRData* patch = patches[i]; for (size_t j = 0; j < patch->m_Splats.size(); ++j) { SSplat& splat = patch->m_Splats[j]; const CMaterial& material = splat.m_Texture->GetMaterial(); if (material.GetShaderEffect().empty()) { LOGERROR("Terrain renderer failed to load shader effect.\n"); continue; } CShaderDefines defines = context; defines.SetMany(material.GetShaderDefines(0)); CShaderTechniquePtr techBase = g_Renderer.GetShaderManager().LoadEffect( material.GetShaderEffect(), defines); BatchElements& batch = PooledPairGet( PooledMapGet( PooledMapGet( PooledMapGet(batches, techBase, arena), splat.m_Texture, arena ), patch->m_VBBase->m_Owner, arena ), patch->m_VBBaseIndices->m_Owner, arena ); batch.first.push_back(splat.m_IndexCount); u8* indexBase = nullptr; batch.second.push_back(indexBase + sizeof(u16)*(patch->m_VBBaseIndices->m_Index + splat.m_IndexStart)); } } PROFILE_END("compute batches"); // Render each batch for (ShaderTechniqueBatches::iterator itTech = batches.begin(); itTech != batches.end(); ++itTech) { const CShaderTechniquePtr& techBase = itTech->first; const int numPasses = techBase->GetNumPasses(); for (int pass = 0; pass < numPasses; ++pass) { techBase->BeginPass(pass); const CShaderProgramPtr& shader = techBase->GetShader(pass); TerrainRenderer::PrepareShader(shader, shadow); TextureBatches& textureBatches = itTech->second; for (TextureBatches::iterator itt = textureBatches.begin(); itt != textureBatches.end(); ++itt) { if (itt->first->GetMaterial().GetSamplers().size() != 0) { const CMaterial::SamplersVector& samplers = itt->first->GetMaterial().GetSamplers(); for(const CMaterial::TextureSampler& samp : samplers) shader->BindTexture(samp.Name, samp.Sampler); itt->first->GetMaterial().GetStaticUniforms().BindUniforms(shader); float c = itt->first->GetTextureMatrix()[0]; float ms = itt->first->GetTextureMatrix()[8]; shader->Uniform(str_textureTransform, c, ms, -ms, 0.f); } else { shader->BindTexture(str_baseTex, g_Renderer.GetTextureManager().GetErrorTexture()); } for (VertexBufferBatches::iterator itv = itt->second.begin(); itv != itt->second.end(); ++itv) { GLsizei stride = sizeof(SBaseVertex); SBaseVertex *base = (SBaseVertex *)itv->first->Bind(); shader->VertexPointer(3, GL_FLOAT, stride, &base->m_Position[0]); shader->NormalPointer(GL_FLOAT, stride, &base->m_Normal[0]); shader->TexCoordPointer(GL_TEXTURE0, 3, GL_FLOAT, stride, &base->m_Position[0]); shader->AssertPointersBound(); for (IndexBufferBatches::iterator it = itv->second.begin(); it != itv->second.end(); ++it) { it->first->Bind(); BatchElements& batch = it->second; // Don't use glMultiDrawElements here since it doesn't have a significant // performance impact and it suffers from various driver bugs (e.g. it breaks // in Mesa 7.10 swrast with index VBOs) for (size_t i = 0; i < batch.first.size(); ++i) glDrawElements(GL_TRIANGLES, batch.first[i], GL_UNSIGNED_SHORT, batch.second[i]); g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_TerrainTris += std::accumulate(batch.first.begin(), batch.first.end(), 0) / 3; } } } techBase->EndPass(); } } CVertexBuffer::Unbind(); } /** * Helper structure for RenderBlends. */ struct SBlendBatch { SBlendBatch(Arena& arena) : m_Batches(VertexBufferBatches::key_compare(), VertexBufferBatches::allocator_type(arena)) { } CTerrainTextureEntry* m_Texture; CShaderTechniquePtr m_ShaderTech; VertexBufferBatches m_Batches; }; /** * Helper structure for RenderBlends. */ struct SBlendStackItem { SBlendStackItem(CVertexBuffer::VBChunk* v, CVertexBuffer::VBChunk* i, const std::vector& s, Arena& arena) : vertices(v), indices(i), splats(s.begin(), s.end(), SplatStack::allocator_type(arena)) { } using SplatStack = std::vector>; CVertexBuffer::VBChunk* vertices; CVertexBuffer::VBChunk* indices; SplatStack splats; }; void CPatchRData::RenderBlends( const std::vector& patches, const CShaderDefines& context, ShadowMap* shadow) { PROFILE3("render terrain blends"); Arena arena; using BatchesStack = std::vector>; BatchesStack batches((BatchesStack::allocator_type(arena))); CShaderDefines contextBlend = context; contextBlend.Add(str_BLEND, str_1); PROFILE_START("compute batches"); // Reserve an arbitrary size that's probably big enough in most cases, // to avoid heavy reallocations batches.reserve(256); using BlendStacks = std::vector>; BlendStacks blendStacks((BlendStacks::allocator_type(arena))); blendStacks.reserve(patches.size()); // Extract all the blend splats from each patch for (size_t i = 0; i < patches.size(); ++i) { CPatchRData* patch = patches[i]; if (!patch->m_BlendSplats.empty()) { blendStacks.push_back(SBlendStackItem(patch->m_VBBlends.Get(), patch->m_VBBlendIndices.Get(), patch->m_BlendSplats, arena)); // Reverse the splats so the first to be rendered is at the back of the list std::reverse(blendStacks.back().splats.begin(), blendStacks.back().splats.end()); } } // Rearrange the collection of splats to be grouped by texture, preserving // order of splats within each patch: // (This is exactly the same algorithm used in CPatchRData::BuildBlends, // but applied to patch-sized splats rather than to tile-sized splats; // see that function for comments on the algorithm.) while (true) { if (!batches.empty()) { CTerrainTextureEntry* tex = batches.back().m_Texture; for (size_t k = 0; k < blendStacks.size(); ++k) { SBlendStackItem::SplatStack& splats = blendStacks[k].splats; if (!splats.empty() && splats.back().m_Texture == tex) { CVertexBuffer::VBChunk* vertices = blendStacks[k].vertices; CVertexBuffer::VBChunk* indices = blendStacks[k].indices; BatchElements& batch = PooledPairGet(PooledMapGet(batches.back().m_Batches, vertices->m_Owner, arena), indices->m_Owner, arena); batch.first.push_back(splats.back().m_IndexCount); u8* indexBase = nullptr; batch.second.push_back(indexBase + sizeof(u16)*(indices->m_Index + splats.back().m_IndexStart)); splats.pop_back(); } } } CTerrainTextureEntry* bestTex = NULL; size_t bestStackSize = 0; for (size_t k = 0; k < blendStacks.size(); ++k) { SBlendStackItem::SplatStack& splats = blendStacks[k].splats; if (splats.size() > bestStackSize) { bestStackSize = splats.size(); bestTex = splats.back().m_Texture; } } if (bestStackSize == 0) break; SBlendBatch layer(arena); layer.m_Texture = bestTex; if (!bestTex->GetMaterial().GetSamplers().empty()) { CShaderDefines defines = contextBlend; defines.SetMany(bestTex->GetMaterial().GetShaderDefines(0)); layer.m_ShaderTech = g_Renderer.GetShaderManager().LoadEffect( bestTex->GetMaterial().GetShaderEffect(), defines); } batches.push_back(layer); } PROFILE_END("compute batches"); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); CVertexBuffer* lastVB = nullptr; CShaderProgramPtr previousShader; for (BatchesStack::iterator itTechBegin = batches.begin(), itTechEnd = batches.begin(); itTechBegin != batches.end(); itTechBegin = itTechEnd) { while (itTechEnd != batches.end() && itTechEnd->m_ShaderTech == itTechBegin->m_ShaderTech) ++itTechEnd; const CShaderTechniquePtr& techBase = itTechBegin->m_ShaderTech; const int numPasses = techBase->GetNumPasses(); for (int pass = 0; pass < numPasses; ++pass) { techBase->BeginPass(pass); const CShaderProgramPtr& shader = techBase->GetShader(pass); TerrainRenderer::PrepareShader(shader, shadow); Renderer::Backend::GL::CTexture* lastBlendTex = nullptr; for (BatchesStack::iterator itt = itTechBegin; itt != itTechEnd; ++itt) { if (itt->m_Texture->GetMaterial().GetSamplers().empty()) continue; if (itt->m_Texture) { const CMaterial::SamplersVector& samplers = itt->m_Texture->GetMaterial().GetSamplers(); for (const CMaterial::TextureSampler& samp : samplers) shader->BindTexture(samp.Name, samp.Sampler); Renderer::Backend::GL::CTexture* currentBlendTex = itt->m_Texture->m_TerrainAlpha->second.m_CompositeAlphaMap.get(); if (currentBlendTex != lastBlendTex) { shader->BindTexture(str_blendTex, currentBlendTex); lastBlendTex = currentBlendTex; } itt->m_Texture->GetMaterial().GetStaticUniforms().BindUniforms(shader); float c = itt->m_Texture->GetTextureMatrix()[0]; float ms = itt->m_Texture->GetTextureMatrix()[8]; shader->Uniform(str_textureTransform, c, ms, -ms, 0.f); } else { shader->BindTexture(str_baseTex, g_Renderer.GetTextureManager().GetErrorTexture()); } for (VertexBufferBatches::iterator itv = itt->m_Batches.begin(); itv != itt->m_Batches.end(); ++itv) { // Rebind the VB only if it changed since the last batch if (itv->first != lastVB || shader != previousShader) { lastVB = itv->first; previousShader = shader; GLsizei stride = sizeof(SBlendVertex); SBlendVertex *base = (SBlendVertex *)itv->first->Bind(); shader->VertexPointer(3, GL_FLOAT, stride, &base->m_Position[0]); shader->NormalPointer(GL_FLOAT, stride, &base->m_Normal[0]); shader->TexCoordPointer(GL_TEXTURE0, 3, GL_FLOAT, stride, &base->m_Position[0]); shader->TexCoordPointer(GL_TEXTURE1, 2, GL_FLOAT, stride, &base->m_AlphaUVs[0]); } shader->AssertPointersBound(); for (IndexBufferBatches::iterator it = itv->second.begin(); it != itv->second.end(); ++it) { it->first->Bind(); BatchElements& batch = it->second; for (size_t i = 0; i < batch.first.size(); ++i) glDrawElements(GL_TRIANGLES, batch.first[i], GL_UNSIGNED_SHORT, batch.second[i]); g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_BlendSplats++; g_Renderer.m_Stats.m_TerrainTris += std::accumulate(batch.first.begin(), batch.first.end(), 0) / 3; } } } techBase->EndPass(); } } glDisable(GL_BLEND); CVertexBuffer::Unbind(); } void CPatchRData::RenderStreams(const std::vector& patches, const CShaderProgramPtr& shader, int streamflags) { PROFILE3("render terrain streams"); // Each batch has a list of index counts, and a list of pointers-to-first-indexes using StreamBatchElements = std::pair, std::vector > ; // Group batches by index buffer using StreamIndexBufferBatches = std::map ; // Group batches by vertex buffer using StreamVertexBufferBatches = std::map ; StreamVertexBufferBatches batches; PROFILE_START("compute batches"); // Collect all the patches into their appropriate batches for (const CPatchRData* patch : patches) { StreamBatchElements& batch = batches[patch->m_VBBase->m_Owner][patch->m_VBBaseIndices->m_Owner]; batch.first.push_back(patch->m_VBBaseIndices->m_Count); u8* indexBase = nullptr; batch.second.push_back(indexBase + sizeof(u16)*(patch->m_VBBaseIndices->m_Index)); } PROFILE_END("compute batches"); ENSURE(!(streamflags & ~(STREAM_POS|STREAM_POSTOUV0|STREAM_POSTOUV1))); // Render each batch for (const std::pair& streamBatch : batches) { GLsizei stride = sizeof(SBaseVertex); SBaseVertex *base = (SBaseVertex *)streamBatch.first->Bind(); shader->VertexPointer(3, GL_FLOAT, stride, &base->m_Position); if (streamflags & STREAM_POSTOUV0) shader->TexCoordPointer(GL_TEXTURE0, 3, GL_FLOAT, stride, &base->m_Position); if (streamflags & STREAM_POSTOUV1) shader->TexCoordPointer(GL_TEXTURE1, 3, GL_FLOAT, stride, &base->m_Position); shader->AssertPointersBound(); for (const std::pair& batchIndexBuffer : streamBatch.second) { batchIndexBuffer.first->Bind(); const StreamBatchElements& batch = batchIndexBuffer.second; for (size_t i = 0; i < batch.first.size(); ++i) glDrawElements(GL_TRIANGLES, batch.first[i], GL_UNSIGNED_SHORT, batch.second[i]); g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_TerrainTris += std::accumulate(batch.first.begin(), batch.first.end(), 0) / 3; } } CVertexBuffer::Unbind(); } void CPatchRData::RenderOutline() { CTerrain* terrain = m_Patch->m_Parent; ssize_t gx = m_Patch->m_X * PATCH_SIZE; ssize_t gz = m_Patch->m_Z * PATCH_SIZE; CVector3D pos; std::vector line; for (ssize_t i = 0, j = 0; i <= PATCH_SIZE; ++i) { terrain->CalcPosition(gx + i, gz + j, pos); line.push_back(pos); } for (ssize_t i = PATCH_SIZE, j = 1; j <= PATCH_SIZE; ++j) { terrain->CalcPosition(gx + i, gz + j, pos); line.push_back(pos); } for (ssize_t i = PATCH_SIZE-1, j = PATCH_SIZE; i >= 0; --i) { terrain->CalcPosition(gx + i, gz + j, pos); line.push_back(pos); } for (ssize_t i = 0, j = PATCH_SIZE-1; j >= 0; --j) { terrain->CalcPosition(gx + i, gz + j, pos); line.push_back(pos); } g_Renderer.GetDebugRenderer().DrawLine(line, CColor(0.0f, 0.0f, 1.0f, 1.0f), 0.1f); } void CPatchRData::RenderSides(const std::vector& patches, const CShaderProgramPtr& shader) { PROFILE3("render terrain sides"); glDisable(GL_CULL_FACE); CVertexBuffer* lastVB = nullptr; for (CPatchRData* patch : patches) { ENSURE(patch->m_UpdateFlags == 0); if (!patch->m_VBSides) continue; if (lastVB != patch->m_VBSides->m_Owner) { lastVB = patch->m_VBSides->m_Owner; SSideVertex *base = (SSideVertex*)patch->m_VBSides->m_Owner->Bind(); // setup data pointers GLsizei stride = sizeof(SSideVertex); shader->VertexPointer(3, GL_FLOAT, stride, &base->m_Position); } shader->AssertPointersBound(); glDrawArrays(GL_TRIANGLE_STRIP, patch->m_VBSides->m_Index, (GLsizei)patch->m_VBSides->m_Count); // bump stats g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_TerrainTris += patch->m_VBSides->m_Count - 2; } CVertexBuffer::Unbind(); glEnable(GL_CULL_FACE); } void CPatchRData::RenderPriorities(CTextRenderer& textRenderer) { CTerrain* terrain = m_Patch->m_Parent; const CCamera& camera = *(g_Game->GetView()->GetCamera()); for (ssize_t j = 0; j < PATCH_SIZE; ++j) { for (ssize_t i = 0; i < PATCH_SIZE; ++i) { ssize_t gx = m_Patch->m_X * PATCH_SIZE + i; ssize_t gz = m_Patch->m_Z * PATCH_SIZE + j; CVector3D pos; terrain->CalcPosition(gx, gz, pos); // Move a bit towards the center of the tile pos.X += TERRAIN_TILE_SIZE/4.f; pos.Z += TERRAIN_TILE_SIZE/4.f; float x, y; camera.GetScreenCoordinates(pos, x, y); textRenderer.PrintfAt(x, y, L"%d", m_Patch->m_MiniPatches[j][i].Priority); } } } // // Water build and rendering // // Build vertex buffer for water vertices over our patch void CPatchRData::BuildWater() { PROFILE3("build water"); // Number of vertices in each direction in each patch ENSURE(PATCH_SIZE % water_cell_size == 0); m_VBWater.Reset(); m_VBWaterIndices.Reset(); m_VBWaterShore.Reset(); m_VBWaterIndicesShore.Reset(); m_WaterBounds.SetEmpty(); // We need to use this to access the water manager or we may not have the // actual values but some compiled-in defaults CmpPtr cmpWaterManager(*m_Simulation, SYSTEM_ENTITY); if (!cmpWaterManager) return; // Build data for water std::vector water_vertex_data; std::vector water_indices; u16 water_index_map[PATCH_SIZE+1][PATCH_SIZE+1]; memset(water_index_map, 0xFF, sizeof(water_index_map)); // Build data for shore std::vector water_vertex_data_shore; std::vector water_indices_shore; u16 water_shore_index_map[PATCH_SIZE+1][PATCH_SIZE+1]; memset(water_shore_index_map, 0xFF, sizeof(water_shore_index_map)); const WaterManager& waterManager = g_Renderer.GetSceneRenderer().GetWaterManager(); CPatch* patch = m_Patch; CTerrain* terrain = patch->m_Parent; ssize_t mapSize = terrain->GetVerticesPerSide(); // Top-left coordinates of our patch. ssize_t px = m_Patch->m_X * PATCH_SIZE; ssize_t pz = m_Patch->m_Z * PATCH_SIZE; // To whoever implements different water heights, this is a TODO: water height) float waterHeight = cmpWaterManager->GetExactWaterLevel(0.0f,0.0f); // The 4 points making a water tile. int moves[4][2] = { {0, 0}, {water_cell_size, 0}, {0, water_cell_size}, {water_cell_size, water_cell_size} }; // Where to look for when checking for water for shore tiles. int check[10][2] = { {0, 0}, {water_cell_size, 0}, {water_cell_size*2, 0}, {0, water_cell_size}, {0, water_cell_size*2}, {water_cell_size, water_cell_size}, {water_cell_size*2, water_cell_size*2}, {-water_cell_size, 0}, {0, -water_cell_size}, {-water_cell_size, -water_cell_size} }; // build vertices, uv, and shader varying for (ssize_t z = 0; z < PATCH_SIZE; z += water_cell_size) { for (ssize_t x = 0; x < PATCH_SIZE; x += water_cell_size) { // Check that this tile is close to water bool nearWater = false; for (size_t test = 0; test < 10; ++test) if (terrain->GetVertexGroundLevel(x + px + check[test][0], z + pz + check[test][1]) < waterHeight) nearWater = true; if (!nearWater) continue; // This is actually lying and I should call CcmpTerrain /*if (!terrain->IsOnMap(x+x1, z+z1) && !terrain->IsOnMap(x+x1, z+z1 + water_cell_size) && !terrain->IsOnMap(x+x1 + water_cell_size, z+z1) && !terrain->IsOnMap(x+x1 + water_cell_size, z+z1 + water_cell_size)) continue;*/ for (int i = 0; i < 4; ++i) { if (water_index_map[z+moves[i][1]][x+moves[i][0]] != 0xFFFF) continue; ssize_t xx = x + px + moves[i][0]; ssize_t zz = z + pz + moves[i][1]; SWaterVertex vertex; terrain->CalcPosition(xx,zz, vertex.m_Position); float depth = waterHeight - vertex.m_Position.Y; vertex.m_Position.Y = waterHeight; m_WaterBounds += vertex.m_Position; vertex.m_WaterData = CVector2D(waterManager.m_WindStrength[xx + zz*mapSize], depth); water_index_map[z+moves[i][1]][x+moves[i][0]] = static_cast(water_vertex_data.size()); water_vertex_data.push_back(vertex); } water_indices.push_back(water_index_map[z + moves[2][1]][x + moves[2][0]]); water_indices.push_back(water_index_map[z + moves[0][1]][x + moves[0][0]]); water_indices.push_back(water_index_map[z + moves[1][1]][x + moves[1][0]]); water_indices.push_back(water_index_map[z + moves[1][1]][x + moves[1][0]]); water_indices.push_back(water_index_map[z + moves[3][1]][x + moves[3][0]]); water_indices.push_back(water_index_map[z + moves[2][1]][x + moves[2][0]]); // Check id this tile is partly over land. // If so add a square over the terrain. This is necessary to render waves that go on shore. if (terrain->GetVertexGroundLevel(x+px, z+pz) < waterHeight && terrain->GetVertexGroundLevel(x+px + water_cell_size, z+pz) < waterHeight && terrain->GetVertexGroundLevel(x+px, z+pz+water_cell_size) < waterHeight && terrain->GetVertexGroundLevel(x+px + water_cell_size, z+pz+water_cell_size) < waterHeight) continue; for (int i = 0; i < 4; ++i) { if (water_shore_index_map[z+moves[i][1]][x+moves[i][0]] != 0xFFFF) continue; ssize_t xx = x + px + moves[i][0]; ssize_t zz = z + pz + moves[i][1]; SWaterVertex vertex; terrain->CalcPosition(xx,zz, vertex.m_Position); vertex.m_Position.Y += 0.02f; m_WaterBounds += vertex.m_Position; vertex.m_WaterData = CVector2D(0.0f, -5.0f); water_shore_index_map[z+moves[i][1]][x+moves[i][0]] = static_cast(water_vertex_data_shore.size()); water_vertex_data_shore.push_back(vertex); } if (terrain->GetTriangulationDir(x + px, z + pz)) { water_indices_shore.push_back(water_shore_index_map[z + moves[2][1]][x + moves[2][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[0][1]][x + moves[0][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[1][1]][x + moves[1][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[1][1]][x + moves[1][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[3][1]][x + moves[3][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[2][1]][x + moves[2][0]]); } else { water_indices_shore.push_back(water_shore_index_map[z + moves[3][1]][x + moves[3][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[2][1]][x + moves[2][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[0][1]][x + moves[0][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[3][1]][x + moves[3][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[0][1]][x + moves[0][0]]); water_indices_shore.push_back(water_shore_index_map[z + moves[1][1]][x + moves[1][0]]); } } } // No vertex buffers if no data generated if (!water_indices.empty()) { m_VBWater = g_VBMan.AllocateChunk(sizeof(SWaterVertex), water_vertex_data.size(), GL_STATIC_DRAW, GL_ARRAY_BUFFER, nullptr, CVertexBufferManager::Group::WATER); m_VBWater->m_Owner->UpdateChunkVertices(m_VBWater.Get(), &water_vertex_data[0]); m_VBWaterIndices = g_VBMan.AllocateChunk(sizeof(GLushort), water_indices.size(), GL_STATIC_DRAW, GL_ELEMENT_ARRAY_BUFFER, nullptr, CVertexBufferManager::Group::WATER); m_VBWaterIndices->m_Owner->UpdateChunkVertices(m_VBWaterIndices.Get(), &water_indices[0]); } if (!water_indices_shore.empty()) { m_VBWaterShore = g_VBMan.AllocateChunk(sizeof(SWaterVertex), water_vertex_data_shore.size(), GL_STATIC_DRAW, GL_ARRAY_BUFFER, nullptr, CVertexBufferManager::Group::WATER); m_VBWaterShore->m_Owner->UpdateChunkVertices(m_VBWaterShore.Get(), &water_vertex_data_shore[0]); // Construct indices buffer m_VBWaterIndicesShore = g_VBMan.AllocateChunk(sizeof(GLushort), water_indices_shore.size(), GL_STATIC_DRAW, GL_ELEMENT_ARRAY_BUFFER, nullptr, CVertexBufferManager::Group::WATER); m_VBWaterIndicesShore->m_Owner->UpdateChunkVertices(m_VBWaterIndicesShore.Get(), &water_indices_shore[0]); } } void CPatchRData::RenderWater(CShaderProgramPtr& shader, bool onlyShore, bool fixedPipeline) { ASSERT(m_UpdateFlags==0); if (!m_VBWater && !m_VBWaterShore) return; #if !CONFIG2_GLES if (g_Renderer.GetSceneRenderer().GetWaterRenderMode() == WIREFRAME) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); #endif if (m_VBWater && !onlyShore) { SWaterVertex *base=(SWaterVertex *)m_VBWater->m_Owner->Bind(); // setup data pointers GLsizei stride = sizeof(SWaterVertex); shader->VertexPointer(3, GL_FLOAT, stride, &base[m_VBWater->m_Index].m_Position); if (!fixedPipeline) shader->VertexAttribPointer(str_a_waterInfo, 2, GL_FLOAT, false, stride, &base[m_VBWater->m_Index].m_WaterData); shader->AssertPointersBound(); u8* indexBase = m_VBWaterIndices->m_Owner->Bind(); glDrawElements(GL_TRIANGLES, (GLsizei) m_VBWaterIndices->m_Count, GL_UNSIGNED_SHORT, indexBase + sizeof(u16)*(m_VBWaterIndices->m_Index)); g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_WaterTris += m_VBWaterIndices->m_Count / 3; } if (m_VBWaterShore && g_VideoMode.GetBackend() != CVideoMode::Backend::GL_ARB && g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterEffects && g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterFancyEffects) { SWaterVertex *base=(SWaterVertex *)m_VBWaterShore->m_Owner->Bind(); GLsizei stride = sizeof(SWaterVertex); shader->VertexPointer(3, GL_FLOAT, stride, &base[m_VBWaterShore->m_Index].m_Position); if (!fixedPipeline) shader->VertexAttribPointer(str_a_waterInfo, 2, GL_FLOAT, false, stride, &base[m_VBWaterShore->m_Index].m_WaterData); shader->AssertPointersBound(); u8* indexBase = m_VBWaterIndicesShore->m_Owner->Bind(); glDrawElements(GL_TRIANGLES, (GLsizei) m_VBWaterIndicesShore->m_Count, GL_UNSIGNED_SHORT, indexBase + sizeof(u16)*(m_VBWaterIndicesShore->m_Index)); g_Renderer.m_Stats.m_DrawCalls++; g_Renderer.m_Stats.m_WaterTris += m_VBWaterIndicesShore->m_Count / 3; } CVertexBuffer::Unbind(); #if !CONFIG2_GLES if (g_Renderer.GetSceneRenderer().GetWaterRenderMode() == WIREFRAME) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); #endif } Index: ps/trunk/source/renderer/Renderer.cpp =================================================================== --- ps/trunk/source/renderer/Renderer.cpp (revision 26183) +++ ps/trunk/source/renderer/Renderer.cpp (revision 26184) @@ -1,860 +1,862 @@ /* Copyright (C) 2022 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 "Renderer.h" #include "graphics/Canvas2D.h" #include "graphics/CinemaManager.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/ModelDef.h" #include "graphics/TerrainTextureManager.h" #include "i18n/L10n.h" #include "lib/allocators/shared_ptr.h" #include "lib/ogl.h" #include "lib/tex/tex.h" #include "gui/GUIManager.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/GameSetup.h" #include "ps/Globals.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Filesystem.h" #include "ps/World.h" #include "ps/ProfileViewer.h" #include "graphics/Camera.h" #include "graphics/FontManager.h" #include "graphics/ShaderManager.h" #include "graphics/Terrain.h" #include "graphics/Texture.h" #include "graphics/TextureManager.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "renderer/backend/gl/Device.h" #include "renderer/DebugRenderer.h" #include "renderer/PostprocManager.h" #include "renderer/RenderingOptions.h" #include "renderer/RenderModifiers.h" #include "renderer/SceneRenderer.h" #include "renderer/TimeManager.h" #include "renderer/VertexBufferManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include "tools/atlas/GameInterface/View.h" #include namespace { size_t g_NextScreenShotNumber = 0; /////////////////////////////////////////////////////////////////////////////////// // CRendererStatsTable - Profile display of rendering stats /** * Class CRendererStatsTable: Implementation of AbstractProfileTable to * display the renderer stats in-game. * * Accesses CRenderer::m_Stats by keeping the reference passed to the * constructor. */ class CRendererStatsTable : public AbstractProfileTable { NONCOPYABLE(CRendererStatsTable); public: CRendererStatsTable(const CRenderer::Stats& st); // Implementation of AbstractProfileTable interface CStr GetName(); CStr GetTitle(); size_t GetNumberRows(); const std::vector& GetColumns(); CStr GetCellText(size_t row, size_t col); AbstractProfileTable* GetChild(size_t row); private: /// Reference to the renderer singleton's stats const CRenderer::Stats& Stats; /// Column descriptions std::vector columnDescriptions; enum { Row_DrawCalls = 0, Row_TerrainTris, Row_WaterTris, Row_ModelTris, Row_OverlayTris, Row_BlendSplats, Row_Particles, Row_VBReserved, Row_VBAllocated, Row_TextureMemory, Row_ShadersLoaded, // Must be last to count number of rows NumberRows }; }; // Construction CRendererStatsTable::CRendererStatsTable(const CRenderer::Stats& st) : Stats(st) { columnDescriptions.push_back(ProfileColumn("Name", 230)); columnDescriptions.push_back(ProfileColumn("Value", 100)); } // Implementation of AbstractProfileTable interface CStr CRendererStatsTable::GetName() { return "renderer"; } CStr CRendererStatsTable::GetTitle() { return "Renderer statistics"; } size_t CRendererStatsTable::GetNumberRows() { return NumberRows; } const std::vector& CRendererStatsTable::GetColumns() { return columnDescriptions; } CStr CRendererStatsTable::GetCellText(size_t row, size_t col) { char buf[256]; switch(row) { case Row_DrawCalls: if (col == 0) return "# draw calls"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_DrawCalls); return buf; case Row_TerrainTris: if (col == 0) return "# terrain tris"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_TerrainTris); return buf; case Row_WaterTris: if (col == 0) return "# water tris"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_WaterTris); return buf; case Row_ModelTris: if (col == 0) return "# model tris"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_ModelTris); return buf; case Row_OverlayTris: if (col == 0) return "# overlay tris"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_OverlayTris); return buf; case Row_BlendSplats: if (col == 0) return "# blend splats"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_BlendSplats); return buf; case Row_Particles: if (col == 0) return "# particles"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)Stats.m_Particles); return buf; case Row_VBReserved: if (col == 0) return "VB reserved"; sprintf_s(buf, sizeof(buf), "%lu kB", (unsigned long)g_VBMan.GetBytesReserved() / 1024); return buf; case Row_VBAllocated: if (col == 0) return "VB allocated"; sprintf_s(buf, sizeof(buf), "%lu kB", (unsigned long)g_VBMan.GetBytesAllocated() / 1024); return buf; case Row_TextureMemory: if (col == 0) return "textures uploaded"; sprintf_s(buf, sizeof(buf), "%lu kB", (unsigned long)g_Renderer.GetTextureManager().GetBytesUploaded() / 1024); return buf; case Row_ShadersLoaded: if (col == 0) return "shader effects loaded"; sprintf_s(buf, sizeof(buf), "%lu", (unsigned long)g_Renderer.GetShaderManager().GetNumEffectsLoaded()); return buf; default: return "???"; } } AbstractProfileTable* CRendererStatsTable::GetChild(size_t UNUSED(row)) { return 0; } } // anonymous namespace /////////////////////////////////////////////////////////////////////////////////// // CRenderer implementation /** * Struct CRendererInternals: Truly hide data that is supposed to be hidden * in this structure so it won't even appear in header files. */ class CRenderer::Internals { NONCOPYABLE(Internals); public: /// true if CRenderer::Open has been called bool IsOpen; /// true if shaders need to be reloaded bool ShadersDirty; /// Table to display renderer stats in-game via profile system CRendererStatsTable profileTable; /// Shader manager CShaderManager shaderManager; /// Texture manager CTextureManager textureManager; /// Time manager CTimeManager timeManager; /// Postprocessing effect manager CPostprocManager postprocManager; CSceneRenderer sceneRenderer; CDebugRenderer debugRenderer; CFontManager fontManager; std::unique_ptr deviceCommandContext; Internals() : IsOpen(false), ShadersDirty(true), profileTable(g_Renderer.m_Stats), textureManager(g_VFS, false, false) { } }; CRenderer::CRenderer() { TIMER(L"InitRenderer"); m = std::make_unique(); g_ProfileViewer.AddRootTable(&m->profileTable); m_Width = 0; m_Height = 0; m_Stats.Reset(); // Create terrain related stuff. new CTerrainTextureManager; Open(g_xres, g_yres); // Setup lighting environment. Since the Renderer accesses the // lighting environment through a pointer, this has to be done before // the first Frame. GetSceneRenderer().SetLightEnv(&g_LightEnv); // I haven't seen the camera affecting GUI rendering and such, but the // viewport has to be updated according to the video mode SViewPort vp; vp.m_X = 0; vp.m_Y = 0; vp.m_Width = g_xres; vp.m_Height = g_yres; SetViewport(vp); ModelDefActivateFastImpl(); ColorActivateFastImpl(); ModelRenderer::Init(); } CRenderer::~CRenderer() { // We no longer UnloadWaterTextures here - // that is the responsibility of the module that asked for // them to be loaded (i.e. CGameView). m.reset(); } void CRenderer::EnumCaps() { // assume support for nothing m_Caps.m_ARBProgram = false; m_Caps.m_ARBProgramShadow = false; m_Caps.m_VertexShader = false; m_Caps.m_FragmentShader = false; m_Caps.m_Shadows = false; m_Caps.m_PrettyWater = false; // now start querying extensions if (0 == ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL)) { m_Caps.m_ARBProgram = true; if (ogl_HaveExtension("GL_ARB_fragment_program_shadow")) m_Caps.m_ARBProgramShadow = true; } // GLSL shaders are in core since GL2.0. if (ogl_HaveVersion(2, 0)) m_Caps.m_VertexShader = m_Caps.m_FragmentShader = true; #if CONFIG2_GLES m_Caps.m_Shadows = true; #else if (0 == ogl_HaveExtensions(0, "GL_ARB_shadow", "GL_ARB_depth_texture", NULL)) { if (ogl_max_tex_units >= 4) m_Caps.m_Shadows = true; } #endif #if CONFIG2_GLES m_Caps.m_PrettyWater = true; #else if (m_Caps.m_VertexShader && m_Caps.m_FragmentShader) m_Caps.m_PrettyWater = true; #endif } void CRenderer::ReloadShaders() { ENSURE(m->IsOpen); m->sceneRenderer.ReloadShaders(); m->ShadersDirty = false; } bool CRenderer::Open(int width, int height) { m->IsOpen = true; // Must query card capabilities before creating renderers that depend // on card capabilities. EnumCaps(); // Dimensions m_Width = width; m_Height = height; // set packing parameters glPixelStorei(GL_PACK_ALIGNMENT,1); glPixelStorei(GL_UNPACK_ALIGNMENT,1); // setup default state glDepthFunc(GL_LEQUAL); glEnable(GL_DEPTH_TEST); glCullFace(GL_BACK); glFrontFace(GL_CCW); glEnable(GL_CULL_FACE); // Validate the currently selected render path SetRenderPath(g_RenderingOptions.GetRenderPath()); m->deviceCommandContext = Renderer::Backend::GL::CDeviceCommandContext::Create(); if (g_RenderingOptions.GetPostProc()) m->postprocManager.Initialize(); m->sceneRenderer.Initialize(); return true; } void CRenderer::Resize(int width, int height) { m_Width = width; m_Height = height; m->postprocManager.Resize(); m->sceneRenderer.Resize(width, height); } void CRenderer::SetRenderPath(RenderPath rp) { if (!m->IsOpen) { // Delay until Open() is called. return; } // Renderer has been opened, so validate the selected renderpath if (rp == RenderPath::DEFAULT) { if (m_Caps.m_ARBProgram || (m_Caps.m_VertexShader && m_Caps.m_FragmentShader && g_VideoMode.GetBackend() != CVideoMode::Backend::GL_ARB)) rp = RenderPath::SHADER; else rp = RenderPath::FIXED; } if (rp == RenderPath::SHADER) { if (!(m_Caps.m_ARBProgram || (m_Caps.m_VertexShader && m_Caps.m_FragmentShader && g_VideoMode.GetBackend() != CVideoMode::Backend::GL_ARB))) { LOGWARNING("Falling back to fixed function\n"); rp = RenderPath::FIXED; } } // TODO: remove this once capabilities have been properly extracted and the above checks have been moved elsewhere. g_RenderingOptions.m_RenderPath = rp; MakeShadersDirty(); // We might need to regenerate some render data after changing path if (g_Game) g_Game->GetWorld()->GetTerrain()->MakeDirty(RENDERDATA_UPDATE_COLOR); } bool CRenderer::ShouldRender() const { return !g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen()); } void CRenderer::RenderFrame(bool needsPresent) { // Do not render if not focused while in fullscreen or minimised, // as that triggers a difficult-to-reproduce crash on some graphic cards. if (!ShouldRender()) return; if (m_ShouldPreloadResourcesBeforeNextFrame) { m_ShouldPreloadResourcesBeforeNextFrame = false; // We don't meed to render logger for the preload. RenderFrameImpl(true, false); } if (m_ScreenShotType == ScreenShotType::BIG) { RenderBigScreenShot(); } else { if (m_ScreenShotType == ScreenShotType::DEFAULT) RenderScreenShot(); else RenderFrameImpl(true, true); if (needsPresent) g_VideoMode.GetBackendDevice()->Present(); } } void CRenderer::RenderFrameImpl(bool renderGUI, bool renderLogger) { PROFILE3("render"); g_Profiler2.RecordGPUFrameStart(); ogl_WarnIfError(); + g_TexMan.UploadResourcesIfNeeded(m->deviceCommandContext.get()); + // prepare before starting the renderer frame if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->BeginFrame(); if (g_Game) m->sceneRenderer.SetSimulation(g_Game->GetSimulation2()); // start new frame BeginFrame(); ogl_WarnIfError(); if (g_Game && g_Game->IsGameStarted()) { g_Game->GetView()->Render(); ogl_WarnIfError(); } m->sceneRenderer.RenderTextOverlays(); // If we're in Atlas game view, render special tools if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawCinemaPathTool(); ogl_WarnIfError(); } if (g_Game && g_Game->IsGameStarted()) { g_Game->GetView()->GetCinema()->Render(); ogl_WarnIfError(); } glDisable(GL_DEPTH_TEST); if (renderGUI) { OGL_SCOPED_DEBUG_GROUP("Draw GUI"); // All GUI elements are drawn in Z order to render semi-transparent // objects correctly. g_GUI->Draw(); ogl_WarnIfError(); } // If we're in Atlas game view, render special overlays (e.g. editor bandbox). if (g_AtlasGameLoop && g_AtlasGameLoop->view) { CCanvas2D canvas; g_AtlasGameLoop->view->DrawOverlays(canvas); ogl_WarnIfError(); } g_Console->Render(); ogl_WarnIfError(); if (renderLogger) { g_Logger->Render(); ogl_WarnIfError(); } // Profile information g_ProfileViewer.RenderProfile(); ogl_WarnIfError(); glEnable(GL_DEPTH_TEST); EndFrame(); const Stats& stats = GetStats(); PROFILE2_ATTR("draw calls: %zu", stats.m_DrawCalls); PROFILE2_ATTR("terrain tris: %zu", stats.m_TerrainTris); PROFILE2_ATTR("water tris: %zu", stats.m_WaterTris); PROFILE2_ATTR("model tris: %zu", stats.m_ModelTris); PROFILE2_ATTR("overlay tris: %zu", stats.m_OverlayTris); PROFILE2_ATTR("blend splats: %zu", stats.m_BlendSplats); PROFILE2_ATTR("particles: %zu", stats.m_Particles); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameEnd(); ogl_WarnIfError(); } void CRenderer::RenderScreenShot() { m_ScreenShotType = ScreenShotType::NONE; // get next available numbered filename // note: %04d -> always 4 digits, so sorting by filename works correctly. const VfsPath filenameFormat(L"screenshots/screenshot%04d.png"); VfsPath filename; vfs::NextNumberedFilename(g_VFS, filenameFormat, g_NextScreenShotNumber, filename); const size_t w = (size_t)g_xres, h = (size_t)g_yres; const size_t bpp = 24; GLenum fmt = GL_RGB; int flags = TEX_BOTTOM_UP; // Hide log messages and re-render RenderFrameImpl(true, false); const size_t img_size = w * h * bpp / 8; const size_t hdr_size = tex_hdr_size(filename); std::shared_ptr buf; AllocateAligned(buf, hdr_size + img_size, maxSectorSize); GLvoid* img = buf.get() + hdr_size; Tex t; if (t.wrap(w, h, bpp, flags, buf, hdr_size) < 0) return; glReadPixels(0, 0, (GLsizei)w, (GLsizei)h, fmt, GL_UNSIGNED_BYTE, img); if (tex_write(&t, filename) == INFO::OK) { OsPath realPath; g_VFS->GetRealPath(filename, realPath); LOGMESSAGERENDER(g_L10n.Translate("Screenshot written to '%s'"), realPath.string8()); debug_printf( CStr(g_L10n.Translate("Screenshot written to '%s'") + "\n").c_str(), realPath.string8().c_str()); } else LOGERROR("Error writing screenshot to '%s'", filename.string8()); } void CRenderer::RenderBigScreenShot() { m_ScreenShotType = ScreenShotType::NONE; // If the game hasn't started yet then use WriteScreenshot to generate the image. if (!g_Game) return RenderScreenShot(); int tiles = 4, tileWidth = 256, tileHeight = 256; CFG_GET_VAL("screenshot.tiles", tiles); CFG_GET_VAL("screenshot.tilewidth", tileWidth); CFG_GET_VAL("screenshot.tileheight", tileHeight); if (tiles <= 0 || tileWidth <= 0 || tileHeight <= 0 || tileWidth * tiles % 4 != 0 || tileHeight * tiles % 4 != 0) { LOGWARNING("Invalid big screenshot size: tiles=%d tileWidth=%d tileHeight=%d", tiles, tileWidth, tileHeight); return; } // get next available numbered filename // note: %04d -> always 4 digits, so sorting by filename works correctly. const VfsPath filenameFormat(L"screenshots/screenshot%04d.bmp"); VfsPath filename; vfs::NextNumberedFilename(g_VFS, filenameFormat, g_NextScreenShotNumber, filename); // Slightly ugly and inflexible: Always draw 640*480 tiles onto the screen, and // hope the screen is actually large enough for that. ENSURE(g_xres >= tileWidth && g_yres >= tileHeight); const int img_w = tileWidth * tiles, img_h = tileHeight * tiles; const int bpp = 24; // we want writing BMP to be as fast as possible, // so read data from OpenGL in BMP format to obviate conversion. #if CONFIG2_GLES // GLES doesn't support BGR const GLenum fmt = GL_RGB; const int flags = TEX_BOTTOM_UP; #else const GLenum fmt = GL_BGR; const int flags = TEX_BOTTOM_UP | TEX_BGR; #endif const size_t img_size = img_w * img_h * bpp / 8; const size_t tile_size = tileWidth * tileHeight * bpp / 8; const size_t hdr_size = tex_hdr_size(filename); void* tile_data = malloc(tile_size); if (!tile_data) { WARN_IF_ERR(ERR::NO_MEM); return; } std::shared_ptr img_buf; AllocateAligned(img_buf, hdr_size + img_size, maxSectorSize); Tex t; GLvoid* img = img_buf.get() + hdr_size; if (t.wrap(img_w, img_h, bpp, flags, img_buf, hdr_size) < 0) { free(tile_data); return; } ogl_WarnIfError(); CCamera oldCamera = *g_Game->GetView()->GetCamera(); // Resize various things so that the sizes and aspect ratios are correct { g_Renderer.Resize(tileWidth, tileHeight); SViewPort vp = { 0, 0, tileWidth, tileHeight }; g_Game->GetView()->SetViewport(vp); } #if !CONFIG2_GLES // Temporarily move everything onto the front buffer, so the user can // see the exciting progress as it renders (and can tell when it's finished). // (It doesn't just use SwapBuffers, because it doesn't know whether to // call the SDL version or the Atlas version.) GLint oldReadBuffer, oldDrawBuffer; glGetIntegerv(GL_READ_BUFFER, &oldReadBuffer); glGetIntegerv(GL_DRAW_BUFFER, &oldDrawBuffer); glDrawBuffer(GL_FRONT); glReadBuffer(GL_FRONT); #endif // Render each tile CMatrix3D projection; projection.SetIdentity(); const float aspectRatio = 1.0f * tileWidth / tileHeight; for (int tile_y = 0; tile_y < tiles; ++tile_y) { for (int tile_x = 0; tile_x < tiles; ++tile_x) { // Adjust the camera to render the appropriate region if (oldCamera.GetProjectionType() == CCamera::ProjectionType::PERSPECTIVE) { projection.SetPerspectiveTile(oldCamera.GetFOV(), aspectRatio, oldCamera.GetNearPlane(), oldCamera.GetFarPlane(), tiles, tile_x, tile_y); } g_Game->GetView()->GetCamera()->SetProjection(projection); RenderFrameImpl(false, false); // Copy the tile pixels into the main image glReadPixels(0, 0, tileWidth, tileHeight, fmt, GL_UNSIGNED_BYTE, tile_data); for (int y = 0; y < tileHeight; ++y) { void* dest = static_cast(img) + ((tile_y * tileHeight + y) * img_w + (tile_x * tileWidth)) * bpp / 8; void* src = static_cast(tile_data) + y * tileWidth * bpp / 8; memcpy(dest, src, tileWidth * bpp / 8); } } } #if !CONFIG2_GLES // Restore the buffer settings glDrawBuffer(oldDrawBuffer); glReadBuffer(oldReadBuffer); #endif // Restore the viewport settings { g_Renderer.Resize(g_xres, g_yres); SViewPort vp = { 0, 0, g_xres, g_yres }; g_Game->GetView()->SetViewport(vp); g_Game->GetView()->GetCamera()->SetProjectionFromCamera(oldCamera); } if (tex_write(&t, filename) == INFO::OK) { OsPath realPath; g_VFS->GetRealPath(filename, realPath); LOGMESSAGERENDER(g_L10n.Translate("Screenshot written to '%s'"), realPath.string8()); debug_printf( CStr(g_L10n.Translate("Screenshot written to '%s'") + "\n").c_str(), realPath.string8().c_str()); } else LOGERROR("Error writing screenshot to '%s'", filename.string8()); free(tile_data); } void CRenderer::BeginFrame() { PROFILE("begin frame"); // Zero out all the per-frame stats. m_Stats.Reset(); if (m->ShadersDirty) ReloadShaders(); m->sceneRenderer.BeginFrame(); } void CRenderer::EndFrame() { PROFILE3("end frame"); m->sceneRenderer.EndFrame(); BindTexture(0, 0); } void CRenderer::SetViewport(const SViewPort &vp) { m_Viewport = vp; glViewport((GLint)vp.m_X,(GLint)vp.m_Y,(GLsizei)vp.m_Width,(GLsizei)vp.m_Height); } SViewPort CRenderer::GetViewport() { return m_Viewport; } void CRenderer::BindTexture(int unit, GLuint tex) { glActiveTextureARB(GL_TEXTURE0+unit); glBindTexture(GL_TEXTURE_2D, tex); } void CRenderer::MakeShadersDirty() { m->ShadersDirty = true; m->sceneRenderer.MakeShadersDirty(); } CTextureManager& CRenderer::GetTextureManager() { return m->textureManager; } CShaderManager& CRenderer::GetShaderManager() { return m->shaderManager; } CTimeManager& CRenderer::GetTimeManager() { return m->timeManager; } CPostprocManager& CRenderer::GetPostprocManager() { return m->postprocManager; } CSceneRenderer& CRenderer::GetSceneRenderer() { return m->sceneRenderer; } CDebugRenderer& CRenderer::GetDebugRenderer() { return m->debugRenderer; } CFontManager& CRenderer::GetFontManager() { return m->fontManager; } void CRenderer::PreloadResourcesBeforeNextFrame() { m_ShouldPreloadResourcesBeforeNextFrame = true; } void CRenderer::MakeScreenShotOnNextFrame(ScreenShotType screenShotType) { m_ScreenShotType = screenShotType; } Renderer::Backend::GL::CDeviceCommandContext* CRenderer::GetDeviceCommandContext() { return m->deviceCommandContext.get(); } Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp (revision 26183) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/TerrainHandlers.cpp (revision 26184) @@ -1,577 +1,578 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 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 "MessageHandler.h" #include "../CommandProc.h" #include "graphics/Patch.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/Terrain.h" +#include "graphics/TextureManager.h" #include "ps/Game.h" #include "ps/World.h" #include "lib/tex/tex.h" #include "ps/Filesystem.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/helpers/Grid.h" #include "../Brushes.h" #include "../DeltaArray.h" #include "../View.h" #include namespace AtlasMessage { namespace { sTerrainTexturePreview MakeEmptyTerrainTexturePreview() { sTerrainTexturePreview preview{}; preview.name = std::wstring(); preview.loaded = false; preview.imageHeight = 0; preview.imageWidth = 0; preview.imageData = {}; return preview; } bool CompareTerrain(const sTerrainTexturePreview& a, const sTerrainTexturePreview& b) { return (wcscmp(a.name.c_str(), b.name.c_str()) < 0); } sTerrainTexturePreview GetPreview(CTerrainTextureEntry* tex, size_t width, size_t height) { sTerrainTexturePreview preview; preview.name = tex->GetTag().FromUTF8(); const size_t previewBPP = 3; std::vector buffer(width * height * previewBPP); // It's not good to shrink the entire texture to fit the small preview // window, since it's the fine details in the texture that are // interesting; so just go down one mipmap level, then crop a chunk // out of the middle. std::shared_ptr fileData; size_t fileSize; Tex texture; const bool canUsePreview = !tex->GetDiffuseTexturePath().empty() && g_VFS->LoadFile(tex->GetDiffuseTexturePath(), fileData, fileSize) == INFO::OK && texture.decode(fileData, fileSize) == INFO::OK && // Check that we can fit the texture into the preview size before any transform. texture.m_Width >= width && texture.m_Height >= height && // Transform to a single format that we can process. texture.transform_to((texture.m_Flags) & ~(TEX_DXT | TEX_MIPMAPS | TEX_GREY | TEX_BGR)) == INFO::OK && (texture.m_Bpp == 24 || texture.m_Bpp == 32); if (canUsePreview) { size_t level = 0; while ((texture.m_Width >> (level + 1)) >= width && (texture.m_Height >> (level + 1)) >= height) ++level; // Extract the middle section (as a representative preview), // and copy into buffer. u8* data = texture.get_data(); const size_t dataShiftX = ((texture.m_Width - width) / 2) >> level; const size_t dataShiftY = ((texture.m_Height - height) / 2) >> level; for (size_t y = 0; y < height; ++y) for (size_t x = 0; x < width; ++x) { const size_t bufferOffset = (y * width + x) * previewBPP; const size_t dataOffset = (((y << level) + dataShiftY) * texture.m_Width + (x << level) + dataShiftX) * texture.m_Bpp / 8; buffer[bufferOffset + 0] = data[dataOffset + 0]; buffer[bufferOffset + 1] = data[dataOffset + 1]; buffer[bufferOffset + 2] = data[dataOffset + 2]; } preview.loaded = true; } else { // Too small to preview. Just use a flat color instead. const u32 baseColor = tex->GetBaseColor(); for (size_t i = 0; i < width * height; ++i) { buffer[i * previewBPP + 0] = (baseColor >> 16) & 0xff; buffer[i * previewBPP + 1] = (baseColor >> 8) & 0xff; buffer[i * previewBPP + 2] = (baseColor >> 0) & 0xff; } preview.loaded = tex->GetTexture()->IsLoaded(); } preview.imageWidth = width; preview.imageHeight = height; preview.imageData = buffer; return preview; } } // anonymous namespace QUERYHANDLER(GetTerrainGroups) { const CTerrainTextureManager::TerrainGroupMap &groups = g_TexMan.GetGroups(); std::vector groupNames; for (CTerrainTextureManager::TerrainGroupMap::const_iterator it = groups.begin(); it != groups.end(); ++it) groupNames.push_back(it->first.FromUTF8()); msg->groupNames = groupNames; } QUERYHANDLER(GetTerrainGroupTextures) { std::vector names; CTerrainGroup* group = g_TexMan.FindGroup(CStrW(*msg->groupName).ToUTF8()); if (group) { for (std::vector::const_iterator it = group->GetTerrains().begin(); it != group->GetTerrains().end(); ++it) names.emplace_back((*it)->GetTag().FromUTF8()); } std::sort(names.begin(), names.end()); msg->names = names; } QUERYHANDLER(GetTerrainGroupPreviews) { std::vector previews; CTerrainGroup* group = g_TexMan.FindGroup(CStrW(*msg->groupName).ToUTF8()); for (std::vector::const_iterator it = group->GetTerrains().begin(); it != group->GetTerrains().end(); ++it) { previews.push_back(GetPreview(*it, msg->imageWidth, msg->imageHeight)); } // Sort the list alphabetically by name std::sort(previews.begin(), previews.end(), CompareTerrain); msg->previews = previews; } QUERYHANDLER(GetTerrainPassabilityClasses) { CmpPtr cmpPathfinder(*AtlasView::GetView_Game()->GetSimulation2(), SYSTEM_ENTITY); if (cmpPathfinder) { std::map nonPathfindingClasses, pathfindingClasses; cmpPathfinder->GetPassabilityClasses(nonPathfindingClasses, pathfindingClasses); std::vector classNames; for (std::map::iterator it = nonPathfindingClasses.begin(); it != nonPathfindingClasses.end(); ++it) classNames.push_back(CStr(it->first).FromUTF8()); msg->classNames = classNames; } } QUERYHANDLER(GetTerrainTexture) { ssize_t x, y; g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(); g_CurrentBrush.GetCentre(x, y); CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); CMiniPatch* tile = terrain->GetTile(x, y); if (tile) { CTerrainTextureEntry* tex = tile->GetTextureEntry(); msg->texture = tex->GetTag().FromUTF8(); } else { msg->texture = std::wstring(); } } QUERYHANDLER(GetTerrainTexturePreview) { CTerrainTextureEntry* tex = g_TexMan.FindTexture(CStrW(*msg->name).ToUTF8()); if (tex) msg->preview = GetPreview(tex, msg->imageWidth, msg->imageHeight); else msg->preview = MakeEmptyTerrainTexturePreview(); } ////////////////////////////////////////////////////////////////////////// namespace { struct TerrainTile { TerrainTile(CTerrainTextureEntry* t, ssize_t p) : tex(t), priority(p) {} CTerrainTextureEntry* tex; ssize_t priority; }; class TerrainArray : public DeltaArray2D { public: void Init() { m_Terrain = g_Game->GetWorld()->GetTerrain(); m_VertsPerSide = g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide(); } void UpdatePriority(ssize_t x, ssize_t y, CTerrainTextureEntry* tex, ssize_t priorityScale, ssize_t& priority) { CMiniPatch* tile = m_Terrain->GetTile(x, y); if (!tile) return; // tile was out-of-bounds // If this tile matches the current texture, we just want to match its // priority; otherwise we want to exceed its priority if (tile->GetTextureEntry() == tex) priority = std::max(priority, tile->GetPriority()*priorityScale); else priority = std::max(priority, tile->GetPriority()*priorityScale + 1); } CTerrainTextureEntry* GetTexEntry(ssize_t x, ssize_t y) { if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1)) return NULL; return get(x, y).tex; } ssize_t GetPriority(ssize_t x, ssize_t y) { if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1)) return 0; return get(x, y).priority; } void PaintTile(ssize_t x, ssize_t y, CTerrainTextureEntry* tex, ssize_t priority) { // Ignore out-of-bounds tiles if (size_t(x) >= size_t(m_VertsPerSide-1) || size_t(y) >= size_t(m_VertsPerSide-1)) return; set(x,y, TerrainTile(tex, priority)); } ssize_t GetTilesPerSide() { return m_VertsPerSide-1; } protected: TerrainTile getOld(ssize_t x, ssize_t y) { CMiniPatch* mp = m_Terrain->GetTile(x, y); ENSURE(mp); return TerrainTile(mp->Tex, mp->Priority); } void setNew(ssize_t x, ssize_t y, const TerrainTile& val) { CMiniPatch* mp = m_Terrain->GetTile(x, y); ENSURE(mp); mp->Tex = val.tex; mp->Priority = val.priority; } CTerrain* m_Terrain; ssize_t m_VertsPerSide; }; } BEGIN_COMMAND(PaintTerrain) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cPaintTerrain() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES); } void Do() { g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(); ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8()); if (! texentry) { debug_warn(L"Can't find texentry"); // TODO: nicer error handling return; } // Priority system: If the new tile should have a high priority, // set it to one plus the maximum priority of all surrounding tiles // that aren't included in the brush (so that it's definitely the highest). // Similar for low priority. ssize_t priorityScale = (msg->priority == ePaintTerrainPriority::HIGH ? +1 : -1); ssize_t priority = 0; for (ssize_t dy = -1; dy < g_CurrentBrush.m_H+1; ++dy) { for (ssize_t dx = -1; dx < g_CurrentBrush.m_W+1; ++dx) { if (!(g_CurrentBrush.Get(dx, dy) > 0.5f)) // ignore tiles that will be painted over m_TerrainDelta.UpdatePriority(x0+dx, y0+dy, texentry, priorityScale, priority); } } for (ssize_t dy = 0; dy < g_CurrentBrush.m_H; ++dy) { for (ssize_t dx = 0; dx < g_CurrentBrush.m_W; ++dx) { if (g_CurrentBrush.Get(dx, dy) > 0.5f) // TODO: proper solid brushes m_TerrainDelta.PaintTile(x0+dx, y0+dy, texentry, priority*priorityScale); } } m_i0 = x0 - 1; m_j0 = y0 - 1; m_i1 = x0 + g_CurrentBrush.m_W + 1; m_j1 = y0 + g_CurrentBrush.m_H + 1; MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } void MergeIntoPrevious(cPaintTerrain* prev) { prev->m_TerrainDelta.OverlayWith(m_TerrainDelta); prev->m_i0 = std::min(prev->m_i0, m_i0); prev->m_j0 = std::min(prev->m_j0, m_j0); prev->m_i1 = std::max(prev->m_i1, m_i1); prev->m_j1 = std::max(prev->m_j1, m_j1); } }; END_COMMAND(PaintTerrain) ////////////////////////////////////////////////////////////////////////// BEGIN_COMMAND(ReplaceTerrain) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cReplaceTerrain() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES); CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(); ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); m_i0 = m_i1 = x0; m_j0 = m_j1 = y0; CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8()); if (! texentry) { debug_warn(L"Can't find texentry"); // TODO: nicer error handling return; } CTerrainTextureEntry* replacedTex = m_TerrainDelta.GetTexEntry(x0, y0); // Don't bother if we're not making a change if (texentry == replacedTex) { return; } ssize_t tiles = m_TerrainDelta.GetTilesPerSide(); for (ssize_t j = 0; j < tiles; ++j) { for (ssize_t i = 0; i < tiles; ++i) { if (m_TerrainDelta.GetTexEntry(i, j) == replacedTex) { m_i0 = std::min(m_i0, i-1); m_j0 = std::min(m_j0, j-1); m_i1 = std::max(m_i1, i+2); m_j1 = std::max(m_j1, j+2); m_TerrainDelta.PaintTile(i, j, texentry, m_TerrainDelta.GetPriority(i, j)); } } } MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } }; END_COMMAND(ReplaceTerrain) ////////////////////////////////////////////////////////////////////////// BEGIN_COMMAND(FillTerrain) { TerrainArray m_TerrainDelta; ssize_t m_i0, m_j0, m_i1, m_j1; // dirtied tiles (inclusive lower bound, exclusive upper) cFillTerrain() { m_TerrainDelta.Init(); } void MakeDirty() { g_Game->GetWorld()->GetTerrain()->MakeDirty(m_i0, m_j0, m_i1, m_j1, RENDERDATA_UPDATE_INDICES); CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->MakeDirty(m_i0, m_j0, m_i1, m_j1); } void Do() { g_CurrentBrush.m_Centre = msg->pos->GetWorldSpace(); ssize_t x0, y0; g_CurrentBrush.GetBottomLeft(x0, y0); m_i0 = m_i1 = x0; m_j0 = m_j1 = y0; CTerrainTextureEntry* texentry = g_TexMan.FindTexture(CStrW(*msg->texture).ToUTF8()); if (! texentry) { debug_warn(L"Can't find texentry"); // TODO: nicer error handling return; } CTerrainTextureEntry* replacedTex = m_TerrainDelta.GetTexEntry(x0, y0); // Don't bother if we're not making a change if (texentry == replacedTex) { return; } ssize_t tiles = m_TerrainDelta.GetTilesPerSide(); // Simple 4-way flood fill algorithm using queue and a grid to keep track of visited tiles, // almost as fast as loop for filling whole map, much faster for small patches SparseGrid visited(tiles, tiles); std::queue > queue; // Initial tile queue.push(std::make_pair((u16)x0, (u16)y0)); visited.set(x0, y0, true); while(!queue.empty()) { // Check front of queue std::pair t = queue.front(); queue.pop(); u16 i = t.first; u16 j = t.second; if (m_TerrainDelta.GetTexEntry(i, j) == replacedTex) { // Found a tile to replace: adjust bounds and paint it m_i0 = std::min(m_i0, (ssize_t)i-1); m_j0 = std::min(m_j0, (ssize_t)j-1); m_i1 = std::max(m_i1, (ssize_t)i+2); m_j1 = std::max(m_j1, (ssize_t)j+2); m_TerrainDelta.PaintTile(i, j, texentry, m_TerrainDelta.GetPriority(i, j)); // Visit 4 adjacent tiles (could visit 8 if we want to count diagonal adjacency) if (i > 0 && !visited.get(i-1, j)) { visited.set(i-1, j, true); queue.push(std::make_pair(i-1, j)); } if (i < (tiles-1) && !visited.get(i+1, j)) { visited.set(i+1, j, true); queue.push(std::make_pair(i+1, j)); } if (j > 0 && !visited.get(i, j-1)) { visited.set(i, j-1, true); queue.push(std::make_pair(i, j-1)); } if (j < (tiles-1) && !visited.get(i, j+1)) { visited.set(i, j+1, true); queue.push(std::make_pair(i, j+1)); } } } MakeDirty(); } void Undo() { m_TerrainDelta.Undo(); MakeDirty(); } void Redo() { m_TerrainDelta.Redo(); MakeDirty(); } }; END_COMMAND(FillTerrain) } // namespace AtlasMessage