/* Copyright (C) 2024 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/code_generation.h"
#include "lib/hash.h"
#include "lib/timer.h"
#include "maths/MathUtil.h"
#include "maths/Vector2D.h"
#include "ps/ConfigDB.h"
#include "ps/CStrInternStatic.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/Profile.h"
#include "ps/VideoMode.h"
#include "ps/World.h"
#include "ps/XML/Xeromyces.h"
#include "renderer/backend/IDevice.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"
#include
#include
#include
namespace
{
// Set max drawn entities to 64K / 4 for now, which is more than enough.
// 4 is the number of vertices per entity.
// TODO: we should be cleverer about drawing them to reduce clutter,
// f.e. use instancing.
constexpr size_t MAX_ENTITIES_DRAWN = 65536 / 4;
constexpr size_t MAX_ICON_COUNT = 256;
constexpr size_t MAX_UNIQUE_ICON_COUNT = 64;
constexpr size_t ICON_COMBINING_GRID_SIZE = 10;
constexpr 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(
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
Renderer::Backend::IVertexInputLayout* quadVertexInputLayout)
{
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,
1.0f, -1.0f,
1.0f, 1.0f,
1.0f, 1.0f,
-1.0f, 1.0f,
-1.0f, -1.0f,
};
deviceCommandContext->SetVertexInputLayout(quadVertexInputLayout);
deviceCommandContext->SetVertexBufferData(
0, quadVertices, std::size(quadVertices) * sizeof(quadVertices[0]));
deviceCommandContext->SetVertexBufferData(
1, quadUVs, std::size(quadUVs) * sizeof(quadUVs[0]));
deviceCommandContext->Draw(0, 6);
}
struct MinimapUnitVertex
{
// This struct is copyable for convenience and because to move is to copy for primitives.
u8 r, g, b, a;
CVector2D position;
};
// Adds a vertex to the passed VertexArray
inline void AddEntity(const MinimapUnitVertex& v,
VertexArrayIterator& attrColor,
VertexArrayIterator& attrPos,
const float entityRadius,
const bool useInstancing)
{
if (useInstancing)
{
(*attrColor)[0] = v.r;
(*attrColor)[1] = v.g;
(*attrColor)[2] = v.b;
(*attrColor)[3] = v.a;
++attrColor;
(*attrPos)[0] = v.position.X;
(*attrPos)[1] = v.position.Y;
++attrPos;
return;
}
const CVector2D offsets[4] =
{
{-entityRadius, 0.0f},
{0.0f, -entityRadius},
{entityRadius, 0.0f},
{0.0f, entityRadius}
};
for (const CVector2D& offset : offsets)
{
(*attrColor)[0] = v.r;
(*attrColor)[1] = v.g;
(*attrColor)[2] = v.b;
(*attrColor)[3] = v.a;
++attrColor;
(*attrPos)[0] = v.position.X + offset.X;
(*attrPos)[1] = v.position.Y + offset.Y;
++attrPos;
}
}
} // anonymous namespace
size_t CMiniMapTexture::CellIconKeyHash::operator()(
const CellIconKey& key) const
{
size_t seed = 0;
hash_combine(seed, key.path);
hash_combine(seed, key.r);
hash_combine(seed, key.g);
hash_combine(seed, key.b);
return seed;
}
bool CMiniMapTexture::CellIconKeyEqual::operator()(
const CellIconKey& lhs, const CellIconKey& rhs) const
{
return
lhs.path == rhs.path &&
lhs.r == rhs.r &&
lhs.g == rhs.g &&
lhs.b == rhs.b;
}
CMiniMapTexture::CMiniMapTexture(Renderer::Backend::IDevice* device, CSimulation2& simulation)
: m_Simulation(simulation), m_IndexArray(Renderer::Backend::IBuffer::Usage::TRANSFER_DST),
m_VertexArray(Renderer::Backend::IBuffer::Type::VERTEX,
Renderer::Backend::IBuffer::Usage::DYNAMIC | Renderer::Backend::IBuffer::Usage::TRANSFER_DST),
m_InstanceVertexArray(Renderer::Backend::IBuffer::Type::VERTEX, Renderer::Backend::IBuffer::Usage::TRANSFER_DST)
{
// 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.format = Renderer::Backend::Format::R32G32_SFLOAT;
m_VertexArray.AddAttribute(&m_AttributePos);
m_AttributeColor.format = Renderer::Backend::Format::R8G8B8A8_UNORM;
m_VertexArray.AddAttribute(&m_AttributeColor);
m_VertexArray.SetNumberOfVertices(MAX_ENTITIES_DRAWN * 4);
m_VertexArray.Layout();
m_IndexArray.SetNumberOfVertices(MAX_ENTITIES_DRAWN * 6);
m_IndexArray.Layout();
VertexArrayIterator index = m_IndexArray.GetIterator();
for (size_t i = 0; i < m_IndexArray.GetNumberOfVertices(); ++i)
*index++ = 0;
m_IndexArray.Upload();
VertexArrayIterator attrPos = m_AttributePos.GetIterator();
VertexArrayIterator attrColor = m_AttributeColor.GetIterator();
for (size_t i = 0; i < m_VertexArray.GetNumberOfVertices(); ++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();
const std::array attributes{{
{Renderer::Backend::VertexAttributeStream::POSITION,
Renderer::Backend::Format::R32G32_SFLOAT, 0, sizeof(float) * 2,
Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0},
{Renderer::Backend::VertexAttributeStream::UV0,
Renderer::Backend::Format::R32G32_SFLOAT, 0, sizeof(float) * 2,
Renderer::Backend::VertexAttributeRate::PER_VERTEX, 1}
}};
m_QuadVertexInputLayout = g_Renderer.GetVertexInputLayout(attributes);
m_Flipped = device->GetBackend() == Renderer::Backend::Backend::VULKAN;
const uint32_t stride = m_VertexArray.GetStride();
if (device->GetCapabilities().instancing)
{
m_UseInstancing = true;
const size_t numberOfCircleSegments = 8;
m_InstanceAttributePosition.format = Renderer::Backend::Format::R32G32_SFLOAT;
m_InstanceVertexArray.AddAttribute(&m_InstanceAttributePosition);
m_InstanceVertexArray.SetNumberOfVertices(numberOfCircleSegments * 3);
m_InstanceVertexArray.Layout();
VertexArrayIterator attributePosition =
m_InstanceAttributePosition.GetIterator();
for (size_t segment = 0; segment < numberOfCircleSegments; ++segment)
{
const float currentAngle = static_cast(segment) / numberOfCircleSegments * 2.0f * M_PI;
const float nextAngle = static_cast(segment + 1) / numberOfCircleSegments * 2.0f * M_PI;
(*attributePosition)[0] = 0.0f;
(*attributePosition)[1] = 0.0f;
++attributePosition;
(*attributePosition)[0] = std::cos(currentAngle);
(*attributePosition)[1] = std::sin(currentAngle);
++attributePosition;
(*attributePosition)[0] = std::cos(nextAngle);
(*attributePosition)[1] = std::sin(nextAngle);
++attributePosition;
}
m_InstanceVertexArray.Upload();
m_InstanceVertexArray.FreeBackingStore();
const std::array attributes{{
{Renderer::Backend::VertexAttributeStream::POSITION,
m_InstanceAttributePosition.format, m_InstanceAttributePosition.offset,
m_InstanceVertexArray.GetStride(),
Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0},
{Renderer::Backend::VertexAttributeStream::UV1,
m_AttributePos.format, m_AttributePos.offset, stride,
Renderer::Backend::VertexAttributeRate::PER_INSTANCE, 1},
{Renderer::Backend::VertexAttributeStream::COLOR,
m_AttributeColor.format, m_AttributeColor.offset, stride,
Renderer::Backend::VertexAttributeRate::PER_INSTANCE, 1},
}};
m_EntitiesVertexInputLayout = g_Renderer.GetVertexInputLayout(attributes);
}
else
{
const std::array entitiesAttributes{{
{Renderer::Backend::VertexAttributeStream::POSITION,
m_AttributePos.format, m_AttributePos.offset, stride,
Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0},
{Renderer::Backend::VertexAttributeStream::COLOR,
m_AttributeColor.format, m_AttributeColor.offset, stride,
Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}
}};
m_EntitiesVertexInputLayout = g_Renderer.GetVertexInputLayout(entitiesAttributes);
}
CShaderDefines baseDefines;
baseDefines.Add(str_MINIMAP_BASE, str_1);
m_TerritoryTechnique = g_Renderer.GetShaderManager().LoadEffect(
str_minimap, baseDefines,
[](Renderer::Backend::SGraphicsPipelineStateDesc& pipelineStateDesc)
{
pipelineStateDesc.blendState.enabled = true;
pipelineStateDesc.blendState.srcColorBlendFactor = pipelineStateDesc.blendState.srcAlphaBlendFactor =
Renderer::Backend::BlendFactor::SRC_ALPHA;
pipelineStateDesc.blendState.dstColorBlendFactor = pipelineStateDesc.blendState.dstAlphaBlendFactor =
Renderer::Backend::BlendFactor::ONE_MINUS_SRC_ALPHA;
pipelineStateDesc.blendState.colorBlendOp = pipelineStateDesc.blendState.alphaBlendOp =
Renderer::Backend::BlendOp::ADD;
pipelineStateDesc.blendState.colorWriteMask =
Renderer::Backend::ColorWriteMask::RED |
Renderer::Backend::ColorWriteMask::GREEN |
Renderer::Backend::ColorWriteMask::BLUE;
});
}
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::IDeviceCommandContext* deviceCommandContext,
CLOSTexture& losTexture, CTerritoryTexture& territoryTexture)
{
const CTerrain& terrain = g_Game->GetWorld()->GetTerrain();
if (!m_TerrainTexture)
CreateTextures(deviceCommandContext, terrain);
if (m_TerrainTextureDirty)
RebuildTerrainTexture(deviceCommandContext, terrain);
RenderFinalTexture(deviceCommandContext, losTexture, territoryTexture);
}
void CMiniMapTexture::CreateTextures(
Renderer::Backend::IDeviceCommandContext* 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);
Renderer::Backend::IDevice* backendDevice = deviceCommandContext->GetDevice();
// Create terrain texture
m_TerrainTexture = backendDevice->CreateTexture2D("MiniMapTerrainTexture",
Renderer::Backend::ITexture::Usage::TRANSFER_DST |
Renderer::Backend::ITexture::Usage::SAMPLED,
Renderer::Backend::Format::R8G8B8A8_UNORM, 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_UNORM,
texData.get(), textureSize * textureSize * 4);
texData.reset();
m_TerrainData = std::make_unique((m_MapSize - 1) * (m_MapSize - 1));
m_FinalTexture = g_Renderer.GetTextureManager().WrapBackendTexture(
backendDevice->CreateTexture2D("MiniMapFinalTexture",
Renderer::Backend::ITexture::Usage::SAMPLED |
Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT,
Renderer::Backend::Format::R8G8B8A8_UNORM,
FINAL_TEXTURE_SIZE, FINAL_TEXTURE_SIZE, defaultSamplerDesc));
Renderer::Backend::SColorAttachment colorAttachment{};
colorAttachment.texture = m_FinalTexture->GetBackendTexture();
colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::DONT_CARE;
colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE;
colorAttachment.clearColor = CColor{0.0f, 0.0f, 0.0f, 0.0f};
m_FinalTextureFramebuffer = backendDevice->CreateFramebuffer(
"MiniMapFinalFramebuffer", &colorAttachment, nullptr);
ENSURE(m_FinalTextureFramebuffer);
}
void CMiniMapTexture::DestroyTextures()
{
m_TerrainTexture.reset();
m_FinalTexture.reset();
m_TerrainData.reset();
}
void CMiniMapTexture::RebuildTerrainTexture(
Renderer::Backend::IDeviceCommandContext* 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(static_cast(i), static_cast(j))
+ terrain.GetVertexGroundLevel(static_cast(i+1), static_cast(j))
+ terrain.GetVertexGroundLevel(static_cast(i), static_cast(j+1))
+ terrain.GetVertexGroundLevel(static_cast(i+1), static_cast(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
{
const int hmap =
static_cast(terrain.GetHeightMap()[(y + j) * m_MapSize + x + i]) >> 8;
int val = (hmap / 3) + 170;
u32 color = 0xFFFFFFFF;
CMiniPatch* const 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_UNORM,
m_TerrainData.get(), width * height * 4, 0, 0, width, height);
}
void CMiniMapTexture::RenderFinalTexture(
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
CLOSTexture& losTexture, CTerritoryTexture& territoryTexture)
{
// 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)
return;
m_LastFinalTextureUpdate = currentTime;
m_FinalTextureDirty = false;
// We might scale entities properly in the vertex shader but it requires
// additional space in the vertex buffer. So we assume that we don't need
// to change an entity size so often.
// Radius with instancing is lower because an entity has a more round shape.
const float entityRadius = static_cast(m_MapSize) / 128.0f * (m_UseInstancing ? 5.0 : 6.0f);
UpdateAndUploadEntities(deviceCommandContext, entityRadius, currentTime);
PROFILE3("Render minimap texture");
GPU_SCOPED_LABEL(deviceCommandContext, "Render minimap texture");
deviceCommandContext->BeginFramebufferPass(m_FinalTextureFramebuffer.get());
Renderer::Backend::IDeviceCommandContext::Rect viewportRect{};
viewportRect.width = FINAL_TEXTURE_SIZE;
viewportRect.height = FINAL_TEXTURE_SIZE;
deviceCommandContext->SetViewports(1, &viewportRect);
const float texCoordMax = m_TerrainTexture ? static_cast(m_MapSize - 1) / m_TerrainTexture->GetWidth() : 1.0f;
Renderer::Backend::IShaderProgram* shader = nullptr;
CShaderTechniquePtr tech;
CShaderDefines baseDefines;
baseDefines.Add(str_MINIMAP_BASE, str_1);
tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, baseDefines);
deviceCommandContext->SetGraphicsPipelineState(
tech->GetGraphicsPipelineState());
deviceCommandContext->BeginPass();
shader = tech->GetShader();
if (m_TerrainTexture)
{
deviceCommandContext->SetTexture(
shader->GetBindingSlot(str_baseTex), m_TerrainTexture.get());
}
CMatrix3D baseTransform;
baseTransform.SetIdentity();
CMatrix3D baseTextureTransform;
baseTextureTransform.SetIdentity();
CMatrix3D terrainTransform;
terrainTransform.SetIdentity();
terrainTransform.Scale(texCoordMax, texCoordMax, 1.0f);
deviceCommandContext->SetUniform(
shader->GetBindingSlot(str_transform),
baseTransform._11, baseTransform._21, baseTransform._12, baseTransform._22);
deviceCommandContext->SetUniform(
shader->GetBindingSlot(str_textureTransform),
terrainTransform._11, terrainTransform._21, terrainTransform._12, terrainTransform._22);
deviceCommandContext->SetUniform(
shader->GetBindingSlot(str_translation),
baseTransform._14, baseTransform._24, terrainTransform._14, terrainTransform._24);
if (m_TerrainTexture)
DrawTexture(deviceCommandContext, m_QuadVertexInputLayout);
deviceCommandContext->EndPass();
deviceCommandContext->SetGraphicsPipelineState(
m_TerritoryTechnique->GetGraphicsPipelineState());
shader = m_TerritoryTechnique->GetShader();
deviceCommandContext->BeginPass();
// Draw territory boundaries
deviceCommandContext->SetTexture(
shader->GetBindingSlot(str_baseTex), territoryTexture.GetTexture());
deviceCommandContext->SetUniform(
shader->GetBindingSlot(str_transform),
baseTransform._11, baseTransform._21, baseTransform._12, baseTransform._22);
const CMatrix3D& territoryTransform = territoryTexture.GetMinimapTextureMatrix();
deviceCommandContext->SetUniform(
shader->GetBindingSlot(str_textureTransform),
territoryTransform._11, territoryTransform._21, territoryTransform._12, territoryTransform._22);
deviceCommandContext->SetUniform(
shader->GetBindingSlot(str_translation),
baseTransform._14, baseTransform._24, territoryTransform._14, territoryTransform._24);
DrawTexture(deviceCommandContext, m_QuadVertexInputLayout);
deviceCommandContext->EndPass();
tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap_los, CShaderDefines());
deviceCommandContext->SetGraphicsPipelineState(
tech->GetGraphicsPipelineState());
deviceCommandContext->BeginPass();
shader = tech->GetShader();
deviceCommandContext->SetTexture(
shader->GetBindingSlot(str_baseTex), losTexture.GetTexture());
deviceCommandContext->SetUniform(
shader->GetBindingSlot(str_transform),
baseTransform._11, baseTransform._21, baseTransform._12, baseTransform._22);
const CMatrix3D& losTransform = losTexture.GetMinimapTextureMatrix();
deviceCommandContext->SetUniform(
shader->GetBindingSlot(str_textureTransform),
losTransform._11, losTransform._21, losTransform._12, losTransform._22);
deviceCommandContext->SetUniform(
shader->GetBindingSlot(str_translation),
baseTransform._14, baseTransform._24, losTransform._14, losTransform._24);
DrawTexture(deviceCommandContext, m_QuadVertexInputLayout);
deviceCommandContext->EndPass();
if (m_EntitiesDrawn > 0)
DrawEntities(deviceCommandContext, entityRadius);
deviceCommandContext->EndFramebufferPass();
}
void CMiniMapTexture::UpdateAndUploadEntities(
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
const float entityRadius, const double& currentTime)
{
const float invTileMapSize = 1.0f / static_cast(TERRAIN_TILE_SIZE * m_MapSize);
m_Icons.clear();
m_IconsCache.clear();
CSimulation2::InterfaceList ents = m_Simulation.GetEntitiesWithInterface(IID_Minimap);
VertexArrayIterator attrPos = m_AttributePos.GetIterator();
VertexArrayIterator attrColor = m_AttributeColor.GetIterator();
m_EntitiesDrawn = 0;
MinimapUnitVertex v;
std::vector pingingVertices;
pingingVertices.reserve(MAX_ENTITIES_DRAWN / 2);
CmpPtr cmpRangeManager(m_Simulation, SYSTEM_ENTITY);
ENSURE(cmpRangeManager);
if (currentTime > m_NextBlinkTime)
{
m_BlinkState = !m_BlinkState;
m_NextBlinkTime = currentTime + m_HalfBlinkDuration;
}
bool iconsEnabled = false;
CFG_GET_VAL("gui.session.minimap.icons.enabled", iconsEnabled);
float iconsOpacity = 1.0f;
CFG_GET_VAL("gui.session.minimap.icons.opacity", iconsOpacity);
float iconsSizeScale = 1.0f;
CFG_GET_VAL("gui.session.minimap.icons.sizescale", iconsSizeScale);
bool iconsCountOverflow = false;
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.position.X = posX.ToFloat();
v.position.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 if (m_EntitiesDrawn < MAX_ENTITIES_DRAWN)
{
AddEntity(v, attrColor, attrPos, entityRadius, m_UseInstancing);
++m_EntitiesDrawn;
}
if (!iconsEnabled || !cmpMinimap->HasIcon())
continue;
const CellIconKey key{
cmpMinimap->GetIconPath(), v.r, v.g, v.b};
const u16 gridX = Clamp(
(v.position.X * invTileMapSize) * ICON_COMBINING_GRID_SIZE, 0, ICON_COMBINING_GRID_SIZE - 1);
const u16 gridY = Clamp(
(v.position.Y * invTileMapSize) * ICON_COMBINING_GRID_SIZE, 0, ICON_COMBINING_GRID_SIZE - 1);
CellIcon icon{
gridX, gridY, cmpMinimap->GetIconSize() * iconsSizeScale * 0.5f, v.position};
if (m_IconsCache.find(key) == m_IconsCache.end() && m_IconsCache.size() >= MAX_UNIQUE_ICON_COUNT)
{
iconsCountOverflow = true;
}
else
{
m_IconsCache[key].emplace_back(std::move(icon));
}
}
}
}
// We need to combine too close icons with the same path, we use a grid for
// that. But to save some allocations and space we store only the current
// row.
struct Cell
{
u32 count;
float maxHalfSize;
CVector2D averagePosition;
};
std::array gridRow;
for (auto& [key, icons] : m_IconsCache)
{
CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(
CTextureProperties(key.path));
const CColor color(key.r / 255.0f, key.g / 255.0f, key.b / 255.0f, iconsOpacity);
std::sort(icons.begin(), icons.end(),
[](const CellIcon& lhs, const CellIcon& rhs) -> bool
{
if (lhs.gridY != rhs.gridY)
return lhs.gridY < rhs.gridY;
return lhs.gridX < rhs.gridX;
});
for (auto beginIt = icons.begin(); beginIt != icons.end();)
{
auto endIt = std::next(beginIt);
while (endIt != icons.end() && beginIt->gridY == endIt->gridY)
++endIt;
gridRow.fill({0, 0.0f, {}});
for (; beginIt != endIt; ++beginIt)
{
Cell& cell = gridRow[beginIt->gridX];
const float previousPositionWeight = static_cast(cell.count) / (cell.count + 1);
cell.averagePosition = cell.averagePosition * previousPositionWeight + beginIt->worldPosition / static_cast(cell.count + 1);
cell.maxHalfSize = std::max(cell.maxHalfSize, beginIt->halfSize);
++cell.count;
}
for (const Cell& cell : gridRow)
{
if (cell.count == 0)
continue;
if (m_Icons.size() < MAX_ICON_COUNT)
{
m_Icons.emplace_back(Icon{
texture, color, cell.averagePosition, cell.maxHalfSize});
}
else
iconsCountOverflow = true;
}
}
}
if (iconsCountOverflow)
LOGWARNING("Too many minimap icons to draw.");
// Add the pinged vertices at the end, so they are drawn on top
for (const MinimapUnitVertex& vertex : pingingVertices)
{
AddEntity(vertex, attrColor, attrPos, entityRadius, m_UseInstancing);
++m_EntitiesDrawn;
if (m_EntitiesDrawn == MAX_ENTITIES_DRAWN)
break;
}
if (m_EntitiesDrawn == MAX_ENTITIES_DRAWN)
ONCE(LOGERROR("Too many entities, some of them will be hidden on the minimap."));
if (!m_UseInstancing)
{
VertexArrayIterator index = m_IndexArray.GetIterator();
for (size_t entityIndex = 0; entityIndex < m_EntitiesDrawn; ++entityIndex)
{
index[entityIndex * 6 + 0] = static_cast(entityIndex * 4 + 0);
index[entityIndex * 6 + 1] = static_cast(entityIndex * 4 + 1);
index[entityIndex * 6 + 2] = static_cast(entityIndex * 4 + 2);
index[entityIndex * 6 + 3] = static_cast(entityIndex * 4 + 0);
index[entityIndex * 6 + 4] = static_cast(entityIndex * 4 + 2);
index[entityIndex * 6 + 5] = static_cast(entityIndex * 4 + 3);
}
m_IndexArray.Upload();
}
m_VertexArray.Upload();
m_VertexArray.PrepareForRendering();
m_VertexArray.UploadIfNeeded(deviceCommandContext);
if (!m_UseInstancing)
m_IndexArray.UploadIfNeeded(deviceCommandContext);
}
void CMiniMapTexture::DrawEntities(
Renderer::Backend::IDeviceCommandContext* deviceCommandContext,
const float entityRadius)
{
const float invTileMapSize = 1.0f / static_cast(TERRAIN_TILE_SIZE * m_MapSize);
CShaderDefines pointDefines;
pointDefines.Add(str_MINIMAP_POINT, str_1);
if (m_UseInstancing)
pointDefines.Add(str_USE_GPU_INSTANCING, str_1);
CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, pointDefines);
deviceCommandContext->SetGraphicsPipelineState(
tech->GetGraphicsPipelineState());
deviceCommandContext->BeginPass();
Renderer::Backend::IShaderProgram* shader = tech->GetShader();
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));
deviceCommandContext->SetUniform(
shader->GetBindingSlot(str_transform),
unitMatrix._11, unitMatrix._21, unitMatrix._12, unitMatrix._22);
deviceCommandContext->SetUniform(
shader->GetBindingSlot(str_translation),
unitMatrix._14, unitMatrix._24, 0.0f, 0.0f);
Renderer::Backend::IDeviceCommandContext::Rect scissorRect;
scissorRect.x = scissorRect.y = 1;
scissorRect.width = scissorRect.height = FINAL_TEXTURE_SIZE - 2;
deviceCommandContext->SetScissors(1, &scissorRect);
const uint32_t stride = m_VertexArray.GetStride();
const uint32_t firstVertexOffset = m_VertexArray.GetOffset() * stride;
deviceCommandContext->SetVertexInputLayout(m_EntitiesVertexInputLayout);
if (m_UseInstancing)
{
deviceCommandContext->SetVertexBuffer(
0, m_InstanceVertexArray.GetBuffer(), m_InstanceVertexArray.GetOffset());
deviceCommandContext->SetVertexBuffer(
1, m_VertexArray.GetBuffer(), firstVertexOffset);
deviceCommandContext->SetUniform(shader->GetBindingSlot(str_width), entityRadius);
deviceCommandContext->DrawInstanced(0, m_InstanceVertexArray.GetNumberOfVertices(), 0, m_EntitiesDrawn);
}
else
{
deviceCommandContext->SetVertexBuffer(
0, m_VertexArray.GetBuffer(), firstVertexOffset);
deviceCommandContext->SetIndexBuffer(m_IndexArray.GetBuffer());
deviceCommandContext->DrawIndexed(m_IndexArray.GetOffset(), m_EntitiesDrawn * 6, 0);
}
g_Renderer.GetStats().m_DrawCalls++;
deviceCommandContext->SetScissors(0, nullptr);
deviceCommandContext->EndPass();
}
// 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;
}
|