Index: ps/trunk/source/graphics/LOSTexture.cpp =================================================================== --- ps/trunk/source/graphics/LOSTexture.cpp (revision 27040) +++ ps/trunk/source/graphics/LOSTexture.cpp (revision 27041) @@ -1,472 +1,472 @@ /* 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 "LOSTexture.h" #include "graphics/ShaderManager.h" #include "lib/bits.h" #include "lib/config2.h" #include "lib/timer.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/Profile.h" #include "renderer/backend/IDevice.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/TimeManager.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/helpers/Los.h" /* The LOS bitmap is computed with one value per LOS vertex, based on CCmpRangeManager's visibility information. The bitmap is then blurred using an NxN filter (in particular a 7-tap Binomial filter as an efficient integral approximation of a Gaussian). To implement the blur efficiently without using extra memory for a second copy of the bitmap, we generate the bitmap with (N-1)/2 pixels of padding on each side, then the blur shifts the image back into the corner. The blurred bitmap is then uploaded into a GL texture for use by the renderer. */ // Blur with a NxN filter, where N = g_BlurSize must be an odd number. // Keep it in relation to the number of impassable tiles in MAP_EDGE_TILES. static const size_t g_BlurSize = 7; // Alignment (in bytes) of the pixel data passed into texture uploading. // This must be a multiple of GL_UNPACK_ALIGNMENT, which ought to be 1 (since // that's what we set it to) but in some weird cases appears to have a different // value. (See Trac #2594). Multiples of 4 are possibly good for performance anyway. static const size_t g_SubTextureAlignment = 4; CLOSTexture::CLOSTexture(CSimulation2& simulation) : m_Simulation(simulation) { if (CRenderer::IsInitialised() && g_RenderingOptions.GetSmoothLOS()) CreateShader(); } CLOSTexture::~CLOSTexture() { m_SmoothFramebuffers[0].reset(); m_SmoothFramebuffers[1].reset(); if (m_Texture) DeleteTexture(); } // Create the LOS texture engine. Should be ran only once. bool CLOSTexture::CreateShader() { m_SmoothTech = g_Renderer.GetShaderManager().LoadEffect(str_los_interp); m_ShaderInitialized = m_SmoothTech && m_SmoothTech->GetShader(); if (!m_ShaderInitialized) { LOGERROR("Failed to load SmoothLOS shader, disabling."); g_RenderingOptions.SetSmoothLOS(false); return false; } return true; } void CLOSTexture::DeleteTexture() { m_Texture.reset(); m_SmoothTextures[0].reset(); m_SmoothTextures[1].reset(); } void CLOSTexture::MakeDirty() { m_Dirty = true; } Renderer::Backend::ITexture* CLOSTexture::GetTextureSmooth() { if (CRenderer::IsInitialised() && !g_RenderingOptions.GetSmoothLOS()) return GetTexture(); else return m_SmoothTextures[m_WhichTexture].get(); } void CLOSTexture::InterpolateLOS(Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { const bool skipSmoothLOS = CRenderer::IsInitialised() && !g_RenderingOptions.GetSmoothLOS(); if (!skipSmoothLOS && !m_ShaderInitialized) { if (!CreateShader()) return; // RecomputeTexture will not cause the ConstructTexture to run. // Force the textures to be created. DeleteTexture(); ConstructTexture(deviceCommandContext); m_Dirty = true; } if (m_Dirty) { RecomputeTexture(deviceCommandContext); // We need to subtract the frame time because without it we have the // same output images for the current and previous frames. m_LastTextureRecomputeTime = timer_Time() - g_Renderer.GetTimeManager().GetFrameDelta(); m_WhichTexture = 1u - m_WhichTexture; m_Dirty = false; } if (skipSmoothLOS) return; GPU_SCOPED_LABEL(deviceCommandContext, "Render LOS texture"); deviceCommandContext->SetFramebuffer(m_SmoothFramebuffers[m_WhichTexture].get()); deviceCommandContext->SetGraphicsPipelineState( m_SmoothTech->GetGraphicsPipelineStateDesc()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = m_SmoothTech->GetShader(); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_losTex1), m_Texture.get()); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_losTex2), m_SmoothTextures[1u - m_WhichTexture].get()); const float delta = Clamp( (timer_Time() - m_LastTextureRecomputeTime) * 2.0f, 0.0f, 1.0f); deviceCommandContext->SetUniform(shader->GetBindingSlot(str_delta), delta); const SViewPort oldVp = g_Renderer.GetViewport(); const SViewPort vp = { 0, 0, static_cast(m_Texture->GetWidth()), static_cast(m_Texture->GetHeight()) }; g_Renderer.SetViewport(vp); float quadVerts[] = { 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f }; float quadTex[] = { 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f }; deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32_SFLOAT, 0, 0, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32_SFLOAT, 0, 0, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 1); deviceCommandContext->SetVertexBufferData( 0, quadVerts, std::size(quadVerts) * sizeof(quadVerts[0])); deviceCommandContext->SetVertexBufferData( 1, quadTex, std::size(quadTex) * sizeof(quadTex[0])); deviceCommandContext->Draw(0, 6); g_Renderer.SetViewport(oldVp); deviceCommandContext->EndPass(); deviceCommandContext->SetFramebuffer( deviceCommandContext->GetDevice()->GetCurrentBackbuffer()); } Renderer::Backend::ITexture* CLOSTexture::GetTexture() { ENSURE(!m_Dirty); return m_Texture.get(); } const CMatrix3D& CLOSTexture::GetTextureMatrix() { ENSURE(!m_Dirty); return m_TextureMatrix; } const CMatrix3D& CLOSTexture::GetMinimapTextureMatrix() { ENSURE(!m_Dirty); return m_MinimapTextureMatrix; } void CLOSTexture::ConstructTexture(Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { CmpPtr cmpRangeManager(m_Simulation, SYSTEM_ENTITY); if (!cmpRangeManager) return; m_MapSize = cmpRangeManager->GetVerticesPerSide(); const size_t textureSize = round_up_to_pow2(round_up((size_t)m_MapSize + g_BlurSize - 1, g_SubTextureAlignment)); Renderer::Backend::IDevice* backendDevice = deviceCommandContext->GetDevice(); const Renderer::Backend::Sampler::Desc defaultSamplerDesc = Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE); if (backendDevice->IsFramebufferFormatSupported(Renderer::Backend::Format::R8_UNORM)) { m_TextureFormat = Renderer::Backend::Format::R8_UNORM; m_TextureFormatStride = 1; } else { m_TextureFormat = Renderer::Backend::Format::R8G8B8A8_UNORM; m_TextureFormatStride = 4; } m_Texture = backendDevice->CreateTexture2D("LOSTexture", m_TextureFormat, textureSize, textureSize, defaultSamplerDesc); // Initialise texture with SoD color, for the areas we don't // overwrite with uploading later. const size_t textureDataSize = textureSize * textureSize * m_TextureFormatStride; std::unique_ptr texData = std::make_unique(textureDataSize); memset(texData.get(), 0x00, textureDataSize); if (CRenderer::IsInitialised() && g_RenderingOptions.GetSmoothLOS()) { m_SmoothTextures[0] = backendDevice->CreateTexture2D("LOSSmoothTexture0", m_TextureFormat, textureSize, textureSize, defaultSamplerDesc); m_SmoothTextures[1] = backendDevice->CreateTexture2D("LOSSmoothTexture1", m_TextureFormat, textureSize, textureSize, defaultSamplerDesc); m_SmoothFramebuffers[0] = backendDevice->CreateFramebuffer( "LOSSmoothFramebuffer0", m_SmoothTextures[0].get(), nullptr); m_SmoothFramebuffers[1] = backendDevice->CreateFramebuffer( "LOSSmoothFramebuffer1", m_SmoothTextures[1].get(), nullptr); if (!m_SmoothFramebuffers[0] || !m_SmoothFramebuffers[1]) { LOGERROR("Failed to create LOS framebuffers"); g_RenderingOptions.SetSmoothLOS(false); } deviceCommandContext->UploadTexture( m_SmoothTextures[0].get(), m_TextureFormat, texData.get(), textureDataSize); deviceCommandContext->UploadTexture( m_SmoothTextures[1].get(), m_TextureFormat, texData.get(), textureDataSize); } deviceCommandContext->UploadTexture( m_Texture.get(), m_TextureFormat, texData.get(), textureDataSize); texData.reset(); { // Texture matrix: We want to map // world pos (0, y, 0) (i.e. first vertex) // onto texcoord (0.5/texsize, 0.5/texsize) (i.e. middle of first texel); // world pos ((mapsize-1)*cellsize, y, (mapsize-1)*cellsize) (i.e. last vertex) // onto texcoord ((mapsize-0.5) / texsize, (mapsize-0.5) / texsize) (i.e. middle of last texel) float s = (m_MapSize-1) / static_cast(textureSize * (m_MapSize-1) * LOS_TILE_SIZE); float t = 0.5f / textureSize; m_TextureMatrix.SetZero(); m_TextureMatrix._11 = s; m_TextureMatrix._23 = s; m_TextureMatrix._14 = t; m_TextureMatrix._24 = t; m_TextureMatrix._44 = 1; } { // Minimap matrix: We want to map UV (0,0)-(1,1) onto (0,0)-(mapsize/texsize, mapsize/texsize) float s = m_MapSize / (float)textureSize; m_MinimapTextureMatrix.SetZero(); m_MinimapTextureMatrix._11 = s; m_MinimapTextureMatrix._22 = s; m_MinimapTextureMatrix._44 = 1; } } void CLOSTexture::RecomputeTexture(Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { // If the map was resized, delete and regenerate the texture if (m_Texture) { CmpPtr cmpRangeManager(m_Simulation, SYSTEM_ENTITY); if (!cmpRangeManager || m_MapSize != cmpRangeManager->GetVerticesPerSide()) DeleteTexture(); } bool recreated = false; if (!m_Texture) { ConstructTexture(deviceCommandContext); recreated = true; } PROFILE("recompute LOS texture"); CmpPtr cmpRangeManager(m_Simulation, SYSTEM_ENTITY); if (!cmpRangeManager) return; size_t pitch; const size_t dataSize = GetBitmapSize(m_MapSize, m_MapSize, &pitch); ENSURE(pitch * m_MapSize <= dataSize); std::unique_ptr losData = std::make_unique( dataSize * m_TextureFormatStride); - CLosQuerier los(cmpRangeManager->GetLosQuerier(g_Game->GetSimulation2()->GetSimContext().GetCurrentDisplayedPlayer())); + CLosQuerier los(cmpRangeManager->GetLosQuerier(m_Simulation.GetSimContext().GetCurrentDisplayedPlayer())); GenerateBitmap(los, &losData[0], m_MapSize, m_MapSize, pitch); // GenerateBitmap writes data tightly packed and we need to offset it to fit // into the texture format properly. const size_t textureDataPitch = pitch * m_TextureFormatStride; if (m_TextureFormatStride > 1) { // We skip the last byte because it will be first in our order and we // don't need to move it. for (size_t index = 0; index + 1 < dataSize; ++index) { const size_t oldAddress = dataSize - 1 - index; const size_t newAddress = oldAddress * m_TextureFormatStride; losData[newAddress] = losData[oldAddress]; losData[oldAddress] = 0; } } if (CRenderer::IsInitialised() && g_RenderingOptions.GetSmoothLOS() && recreated) { deviceCommandContext->UploadTextureRegion( m_SmoothTextures[0].get(), m_TextureFormat, losData.get(), textureDataPitch * m_MapSize, 0, 0, pitch, m_MapSize); deviceCommandContext->UploadTextureRegion( m_SmoothTextures[1].get(), m_TextureFormat, losData.get(), textureDataPitch * m_MapSize, 0, 0, pitch, m_MapSize); } deviceCommandContext->UploadTextureRegion( m_Texture.get(), m_TextureFormat, losData.get(), textureDataPitch * m_MapSize, 0, 0, pitch, m_MapSize); } size_t CLOSTexture::GetBitmapSize(size_t w, size_t h, size_t* pitch) { *pitch = round_up(w + g_BlurSize - 1, g_SubTextureAlignment); return *pitch * (h + g_BlurSize - 1); } void CLOSTexture::GenerateBitmap(const CLosQuerier& los, u8* losData, size_t w, size_t h, size_t pitch) { u8 *dataPtr = losData; // Initialise the top padding for (size_t j = 0; j < g_BlurSize/2; ++j) for (size_t i = 0; i < pitch; ++i) *dataPtr++ = 0; for (size_t j = 0; j < h; ++j) { // Initialise the left padding for (size_t i = 0; i < g_BlurSize/2; ++i) *dataPtr++ = 0; // Fill in the visibility data for (size_t i = 0; i < w; ++i) { if (los.IsVisible_UncheckedRange(i, j)) *dataPtr++ = 255; else if (los.IsExplored_UncheckedRange(i, j)) *dataPtr++ = 127; else *dataPtr++ = 0; } // Initialise the right padding for (size_t i = 0; i < pitch - w - g_BlurSize/2; ++i) *dataPtr++ = 0; } // Initialise the bottom padding for (size_t j = 0; j < g_BlurSize/2; ++j) for (size_t i = 0; i < pitch; ++i) *dataPtr++ = 0; // Horizontal blur: for (size_t j = g_BlurSize/2; j < h + g_BlurSize/2; ++j) { for (size_t i = 0; i < w; ++i) { u8* d = &losData[i+j*pitch]; *d = ( 1*d[0] + 6*d[1] + 15*d[2] + 20*d[3] + 15*d[4] + 6*d[5] + 1*d[6] ) / 64; } } // Vertical blur: for (size_t j = 0; j < h; ++j) { for (size_t i = 0; i < w; ++i) { u8* d = &losData[i+j*pitch]; *d = ( 1*d[0*pitch] + 6*d[1*pitch] + 15*d[2*pitch] + 20*d[3*pitch] + 15*d[4*pitch] + 6*d[5*pitch] + 1*d[6*pitch] ) / 64; } } } Index: ps/trunk/source/graphics/MiniMapTexture.cpp =================================================================== --- ps/trunk/source/graphics/MiniMapTexture.cpp (revision 27040) +++ ps/trunk/source/graphics/MiniMapTexture.cpp (revision 27041) @@ -1,813 +1,814 @@ /* 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/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) { 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 }; deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, 0, 0, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32_SFLOAT, 0, 0, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 1); 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(CSimulation2& simulation) : m_Simulation(simulation), m_IndexArray(false), m_VertexArray(Renderer::Backend::IBuffer::Type::VERTEX, true), m_InstanceVertexArray(Renderer::Backend::IBuffer::Type::VERTEX, false) { // 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(); if (g_VideoMode.GetBackendDevice()->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(); } } 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) { const CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); if (!terrain) return; if (!m_TerrainTexture) CreateTextures(deviceCommandContext, terrain); if (m_TerrainTextureDirty) RebuildTerrainTexture(deviceCommandContext, terrain); RenderFinalTexture(deviceCommandContext); } 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::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 = backendDevice->CreateTexture2D("MiniMapFinalTexture", Renderer::Backend::Format::R8G8B8A8_UNORM, FINAL_TEXTURE_SIZE, FINAL_TEXTURE_SIZE, defaultSamplerDesc); m_FinalTextureFramebuffer = backendDevice->CreateFramebuffer("MiniMapFinalFramebuffer", m_FinalTexture.get(), 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((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_UNORM, m_TerrainData.get(), width * height * 4, 0, 0, width, height); } void CMiniMapTexture::RenderFinalTexture( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { // 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; PROFILE3("Render minimap texture"); GPU_SCOPED_LABEL(deviceCommandContext, "Render minimap texture"); deviceCommandContext->SetFramebuffer(m_FinalTextureFramebuffer.get()); 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(); + CLOSTexture& losTexture = g_Renderer.GetSceneRenderer().GetScene().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; Renderer::Backend::IShaderProgram* shader = nullptr; CShaderTechniquePtr tech; CShaderDefines baseDefines; baseDefines.Add(str_MINIMAP_BASE, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, baseDefines); Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc = tech->GetGraphicsPipelineStateDesc(); deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); 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.AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_textureTransform), terrainTransform.AsFloatArray()); if (m_TerrainTexture) DrawTexture(deviceCommandContext); deviceCommandContext->EndPass(); 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; deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); deviceCommandContext->BeginPass(); // Draw territory boundaries - CTerritoryTexture& territoryTexture = g_Game->GetView()->GetTerritoryTexture(); + CTerritoryTexture& territoryTexture = + g_Renderer.GetSceneRenderer().GetScene().GetTerritoryTexture(); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_baseTex), territoryTexture.GetTexture()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), baseTransform.AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_textureTransform), territoryTexture.GetMinimapTextureMatrix().AsFloatArray()); DrawTexture(deviceCommandContext); deviceCommandContext->EndPass(); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap_los, CShaderDefines()); deviceCommandContext->SetGraphicsPipelineState( tech->GetGraphicsPipelineStateDesc()); deviceCommandContext->BeginPass(); shader = tech->GetShader(); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_baseTex), losTexture.GetTexture()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), baseTransform.AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_textureTransform), losTexture.GetMinimapTextureMatrix().AsFloatArray()); DrawTexture(deviceCommandContext); deviceCommandContext->EndPass(); // 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); if (doUpdate) { 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); 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 { 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; } ENSURE(m_EntitiesDrawn < MAX_ENTITIES_DRAWN); 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(); if (m_EntitiesDrawn > 0) { CShaderDefines pointDefines; pointDefines.Add(str_MINIMAP_POINT, str_1); if (m_UseInstancing) pointDefines.Add(str_USE_GPU_INSTANCING, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, pointDefines); deviceCommandContext->SetGraphicsPipelineState( tech->GetGraphicsPipelineStateDesc()); deviceCommandContext->BeginPass(); shader = tech->GetShader(); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), baseTransform.AsFloatArray()); 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.AsFloatArray()); Renderer::Backend::IDeviceCommandContext::Rect scissorRect; scissorRect.x = scissorRect.y = 1; scissorRect.width = scissorRect.height = FINAL_TEXTURE_SIZE - 2; deviceCommandContext->SetScissors(1, &scissorRect); m_VertexArray.UploadIfNeeded(deviceCommandContext); const uint32_t stride = m_VertexArray.GetStride(); const uint32_t firstVertexOffset = m_VertexArray.GetOffset() * stride; if (m_UseInstancing) { deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, m_AttributePos.format, m_InstanceVertexArray.GetOffset() + m_InstanceAttributePosition.offset, m_InstanceVertexArray.GetStride(), Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV1, m_AttributePos.format, firstVertexOffset + m_AttributePos.offset, stride, Renderer::Backend::VertexAttributeRate::PER_INSTANCE, 1); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::COLOR, m_AttributeColor.format, firstVertexOffset + m_AttributeColor.offset, stride, Renderer::Backend::VertexAttributeRate::PER_INSTANCE, 1); deviceCommandContext->SetVertexBuffer(0, m_InstanceVertexArray.GetBuffer()); deviceCommandContext->SetVertexBuffer(1, m_VertexArray.GetBuffer()); deviceCommandContext->SetUniform(shader->GetBindingSlot(str_width), entityRadius); deviceCommandContext->DrawInstanced(0, m_InstanceVertexArray.GetNumberOfVertices(), 0, m_EntitiesDrawn); } else { m_IndexArray.UploadIfNeeded(deviceCommandContext); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, m_AttributePos.format, firstVertexOffset + m_AttributePos.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::COLOR, m_AttributeColor.format, firstVertexOffset + m_AttributeColor.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexBuffer(0, m_VertexArray.GetBuffer()); 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(); } deviceCommandContext->SetFramebuffer( deviceCommandContext->GetDevice()->GetCurrentBackbuffer()); 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/renderer/TerrainRenderer.cpp =================================================================== --- ps/trunk/source/renderer/TerrainRenderer.cpp (revision 27040) +++ ps/trunk/source/renderer/TerrainRenderer.cpp (revision 27041) @@ -1,728 +1,728 @@ /* 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/TerrainRenderer.h" #include "graphics/Camera.h" #include "graphics/Canvas2D.h" #include "graphics/Decal.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/LOSTexture.h" #include "graphics/Patch.h" #include "graphics/Model.h" #include "graphics/ShaderManager.h" #include "graphics/TerritoryTexture.h" #include "graphics/TextRenderer.h" #include "graphics/TextureManager.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Profile.h" #include "ps/World.h" #include "renderer/backend/IDevice.h" #include "renderer/DecalRData.h" #include "renderer/PatchRData.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/SceneRenderer.h" #include "renderer/ShadowMap.h" #include "renderer/SkyManager.h" #include "renderer/VertexArray.h" #include "renderer/WaterManager.h" /** * TerrainRenderer keeps track of which phase it is in, to detect * when Submit, PrepareForRendering etc. are called in the wrong order. */ enum Phase { Phase_Submit, Phase_Render }; /** * Struct TerrainRendererInternals: Internal variables used by the TerrainRenderer class. */ struct TerrainRendererInternals { /// Which phase (submitting or rendering patches) are we in right now? Phase phase; /// Patches that were submitted for this frame std::vector visiblePatches[CSceneRenderer::CULL_MAX]; /// Decals that were submitted for this frame std::vector visibleDecals[CSceneRenderer::CULL_MAX]; /// Fancy water shader CShaderTechniquePtr fancyWaterTech; CSimulation2* simulation; }; /////////////////////////////////////////////////////////////////// // Construction/Destruction TerrainRenderer::TerrainRenderer() { m = new TerrainRendererInternals(); m->phase = Phase_Submit; } TerrainRenderer::~TerrainRenderer() { delete m; } void TerrainRenderer::SetSimulation(CSimulation2* simulation) { m->simulation = simulation; } /////////////////////////////////////////////////////////////////// // Submit a patch for rendering void TerrainRenderer::Submit(int cullGroup, CPatch* patch) { ENSURE(m->phase == Phase_Submit); CPatchRData* data = (CPatchRData*)patch->GetRenderData(); if (data == 0) { // no renderdata for patch, create it now data = new CPatchRData(patch, m->simulation); patch->SetRenderData(data); } data->Update(m->simulation); m->visiblePatches[cullGroup].push_back(data); } /////////////////////////////////////////////////////////////////// // Submit a decal for rendering void TerrainRenderer::Submit(int cullGroup, CModelDecal* decal) { ENSURE(m->phase == Phase_Submit); CDecalRData* data = (CDecalRData*)decal->GetRenderData(); if (data == 0) { // no renderdata for decal, create it now data = new CDecalRData(decal, m->simulation); decal->SetRenderData(data); } data->Update(m->simulation); m->visibleDecals[cullGroup].push_back(data); } /////////////////////////////////////////////////////////////////// // Prepare for rendering void TerrainRenderer::PrepareForRendering() { ENSURE(m->phase == Phase_Submit); m->phase = Phase_Render; } /////////////////////////////////////////////////////////////////// // Clear submissions lists void TerrainRenderer::EndFrame() { ENSURE(m->phase == Phase_Render || m->phase == Phase_Submit); for (int i = 0; i < CSceneRenderer::CULL_MAX; ++i) { m->visiblePatches[i].clear(); m->visibleDecals[i].clear(); } m->phase = Phase_Submit; } void TerrainRenderer::RenderTerrainOverlayTexture( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, int cullGroup, CMatrix3D& textureMatrix, Renderer::Backend::ITexture* texture) { ENSURE(m->phase == Phase_Render); std::vector& visiblePatches = m->visiblePatches[cullGroup]; CShaderTechniquePtr debugOverlayTech = g_Renderer.GetShaderManager().LoadEffect(str_debug_overlay); deviceCommandContext->SetGraphicsPipelineState( debugOverlayTech->GetGraphicsPipelineStateDesc()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* debugOverlayShader = debugOverlayTech->GetShader(); deviceCommandContext->SetTexture( debugOverlayShader->GetBindingSlot(str_baseTex), texture); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( debugOverlayShader->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetUniform( debugOverlayShader->GetBindingSlot(str_textureTransform), textureMatrix.AsFloatArray()); CPatchRData::RenderStreams(deviceCommandContext, visiblePatches, true); // To make the overlay visible over water, render an additional map-sized // water-height patch. CBoundingBoxAligned waterBounds; for (CPatchRData* data : visiblePatches) waterBounds += data->GetWaterBounds(); if (!waterBounds.IsEmpty()) { // Add a delta to avoid z-fighting. const float height = g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight + 0.05f; const float waterPos[] = { waterBounds[0].X, height, waterBounds[0].Z, waterBounds[1].X, height, waterBounds[0].Z, waterBounds[1].X, height, waterBounds[1].Z, waterBounds[0].X, height, waterBounds[0].Z, waterBounds[1].X, height, waterBounds[1].Z, waterBounds[0].X, height, waterBounds[1].Z }; deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, 0, 0, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32B32_SFLOAT, 0, 0, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexBufferData( 0, waterPos, std::size(waterPos) * sizeof(waterPos[0])); deviceCommandContext->Draw(0, 6); } deviceCommandContext->EndPass(); } /////////////////////////////////////////////////////////////////// /** * Set up all the uniforms for a shader pass. */ void TerrainRenderer::PrepareShader( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IShaderProgram* shader, ShadowMap* shadow) { CSceneRenderer& sceneRenderer = g_Renderer.GetSceneRenderer(); const CMatrix3D transform = sceneRenderer.GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_cameraPos), sceneRenderer.GetViewCamera().GetOrientation().GetTranslation().AsFloatArray()); const CLightEnv& lightEnv = sceneRenderer.GetLightEnv(); if (shadow) shadow->BindTo(deviceCommandContext, shader); CLOSTexture& los = sceneRenderer.GetScene().GetLOSTexture(); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_losTex), los.GetTextureSmooth()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_losTransform), los.GetTextureMatrix()[0], los.GetTextureMatrix()[12]); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_ambient), lightEnv.m_AmbientColor.AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_sunColor), lightEnv.m_SunColor.AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_sunDir), lightEnv.GetSunDir().AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_fogColor), lightEnv.m_FogColor.AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_fogParams), lightEnv.m_FogFactor, lightEnv.m_FogMax); } void TerrainRenderer::RenderTerrainShader( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup, ShadowMap* shadow) { ENSURE(m->phase == Phase_Render); std::vector& visiblePatches = m->visiblePatches[cullGroup]; std::vector& visibleDecals = m->visibleDecals[cullGroup]; if (visiblePatches.empty() && visibleDecals.empty()) return; // render the solid black sides of the map first CShaderTechniquePtr techSolid = g_Renderer.GetShaderManager().LoadEffect(str_solid); Renderer::Backend::GraphicsPipelineStateDesc solidPipelineStateDesc = techSolid->GetGraphicsPipelineStateDesc(); solidPipelineStateDesc.rasterizationState.cullMode = Renderer::Backend::CullMode::NONE; deviceCommandContext->SetGraphicsPipelineState(solidPipelineStateDesc); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shaderSolid = techSolid->GetShader(); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( shaderSolid->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetUniform( shaderSolid->GetBindingSlot(str_color), 0.0f, 0.0f, 0.0f, 1.0f); CPatchRData::RenderSides(deviceCommandContext, visiblePatches); deviceCommandContext->EndPass(); CPatchRData::RenderBases(deviceCommandContext, visiblePatches, context, shadow); // render blend passes for each patch CPatchRData::RenderBlends(deviceCommandContext, visiblePatches, context, shadow); CDecalRData::RenderDecals(deviceCommandContext, visibleDecals, context, shadow); } /////////////////////////////////////////////////////////////////// // Render un-textured patches as polygons void TerrainRenderer::RenderPatches( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, int cullGroup, const CShaderDefines& defines, const CColor& color) { ENSURE(m->phase == Phase_Render); std::vector& visiblePatches = m->visiblePatches[cullGroup]; if (visiblePatches.empty()) return; GPU_SCOPED_LABEL(deviceCommandContext, "Render terrain patches"); CShaderTechniquePtr solidTech = g_Renderer.GetShaderManager().LoadEffect(str_terrain_solid, defines); deviceCommandContext->SetGraphicsPipelineState( solidTech->GetGraphicsPipelineStateDesc()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* solidShader = solidTech->GetShader(); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( solidShader->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetUniform( solidShader->GetBindingSlot(str_color), color.AsFloatArray()); CPatchRData::RenderStreams(deviceCommandContext, visiblePatches, false); deviceCommandContext->EndPass(); } /////////////////////////////////////////////////////////////////// // Render outlines of submitted patches as lines void TerrainRenderer::RenderOutlines( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, int cullGroup) { ENSURE(m->phase == Phase_Render); std::vector& visiblePatches = m->visiblePatches[cullGroup]; if (visiblePatches.empty()) return; GPU_SCOPED_LABEL(deviceCommandContext, "Render terrain outlines"); for (size_t i = 0; i < visiblePatches.size(); ++i) visiblePatches[i]->RenderOutline(); } /////////////////////////////////////////////////////////////////// // Scissor rectangle of water patches CBoundingBoxAligned TerrainRenderer::ScissorWater(int cullGroup, const CCamera& camera) { CBoundingBoxAligned scissor; for (const CPatchRData* data : m->visiblePatches[cullGroup]) { const CBoundingBoxAligned& waterBounds = data->GetWaterBounds(); if (waterBounds.IsEmpty()) continue; const CBoundingBoxAligned waterBoundsInViewPort = camera.GetBoundsInViewPort(waterBounds); if (!waterBoundsInViewPort.IsEmpty()) scissor += waterBoundsInViewPort; } if (scissor.IsEmpty()) return scissor; return CBoundingBoxAligned( CVector3D(Clamp(scissor[0].X, -1.0f, 1.0f), Clamp(scissor[0].Y, -1.0f, 1.0f), -1.0f), CVector3D(Clamp(scissor[1].X, -1.0f, 1.0f), Clamp(scissor[1].Y, -1.0f, 1.0f), 1.0f)); } // Render fancy water bool TerrainRenderer::RenderFancyWater( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup, ShadowMap* shadow) { PROFILE3_GPU("fancy water"); GPU_SCOPED_LABEL(deviceCommandContext, "Render fancy water"); CSceneRenderer& sceneRenderer = g_Renderer.GetSceneRenderer(); WaterManager& waterManager = sceneRenderer.GetWaterManager(); CShaderDefines defines = context; // If we're using fancy water, make sure its shader is loaded if (!m->fancyWaterTech || waterManager.m_NeedsReloading) { if (waterManager.m_WaterRealDepth) defines.Add(str_USE_REAL_DEPTH, str_1); if (waterManager.m_WaterFancyEffects) defines.Add(str_USE_FANCY_EFFECTS, str_1); if (waterManager.m_WaterRefraction) defines.Add(str_USE_REFRACTION, str_1); if (waterManager.m_WaterReflection) defines.Add(str_USE_REFLECTION, str_1); m->fancyWaterTech = g_Renderer.GetShaderManager().LoadEffect(str_water_high, defines); if (!m->fancyWaterTech) { LOGERROR("Failed to load water shader. Falling back to a simple water.\n"); waterManager.m_RenderWater = false; return false; } waterManager.m_NeedsReloading = false; } CLOSTexture& losTexture = sceneRenderer.GetScene().GetLOSTexture(); // Calculating the advanced informations about Foam and all if the quality calls for it. /*if (WaterMgr->m_NeedInfoUpdate && (WaterMgr->m_WaterFoam || WaterMgr->m_WaterCoastalWaves)) { WaterMgr->m_NeedInfoUpdate = false; WaterMgr->CreateSuperfancyInfo(); }*/ const double time = waterManager.m_WaterTexTimer; const float repeatPeriod = waterManager.m_RepeatPeriod; deviceCommandContext->SetGraphicsPipelineState( m->fancyWaterTech->GetGraphicsPipelineStateDesc()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* fancyWaterShader = m->fancyWaterTech->GetShader(); const CCamera& camera = g_Renderer.GetSceneRenderer().GetViewCamera(); const double period = 8.0; // TODO: move uploading to a prepare function during loading. const CTexturePtr& currentNormalTexture = waterManager.m_NormalMap[waterManager.GetCurrentTextureIndex(period)]; const CTexturePtr& nextNormalTexture = waterManager.m_NormalMap[waterManager.GetNextTextureIndex(period)]; currentNormalTexture->UploadBackendTextureIfNeeded(deviceCommandContext); nextNormalTexture->UploadBackendTextureIfNeeded(deviceCommandContext); deviceCommandContext->SetTexture( fancyWaterShader->GetBindingSlot(str_normalMap), currentNormalTexture->GetBackendTexture()); deviceCommandContext->SetTexture( fancyWaterShader->GetBindingSlot(str_normalMap2), nextNormalTexture->GetBackendTexture()); if (waterManager.m_WaterFancyEffects) { deviceCommandContext->SetTexture( fancyWaterShader->GetBindingSlot(str_waterEffectsTex), waterManager.m_FancyTexture.get()); } if (waterManager.m_WaterRefraction && waterManager.m_WaterRealDepth) { deviceCommandContext->SetTexture( fancyWaterShader->GetBindingSlot(str_depthTex), waterManager.m_RefrFboDepthTexture.get()); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_projInvTransform), waterManager.m_RefractionProjInvMatrix.AsFloatArray()); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_viewInvTransform), waterManager.m_RefractionViewInvMatrix.AsFloatArray()); } if (waterManager.m_WaterRefraction) { deviceCommandContext->SetTexture( fancyWaterShader->GetBindingSlot(str_refractionMap), waterManager.m_RefractionTexture.get()); } if (waterManager.m_WaterReflection) { deviceCommandContext->SetTexture( fancyWaterShader->GetBindingSlot(str_reflectionMap), waterManager.m_ReflectionTexture.get()); } deviceCommandContext->SetTexture( fancyWaterShader->GetBindingSlot(str_losTex), losTexture.GetTextureSmooth()); const CLightEnv& lightEnv = sceneRenderer.GetLightEnv(); const CMatrix3D transform = sceneRenderer.GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetTexture( fancyWaterShader->GetBindingSlot(str_skyCube), sceneRenderer.GetSkyManager().GetSkyCube()); // TODO: check that this rotates in the right direction. CMatrix3D skyBoxRotation; skyBoxRotation.SetIdentity(); skyBoxRotation.RotateY(M_PI + lightEnv.GetRotation()); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_skyBoxRot), skyBoxRotation.AsFloatArray()); if (waterManager.m_WaterRefraction) { deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_refractionMatrix), waterManager.m_RefractionMatrix.AsFloatArray()); } if (waterManager.m_WaterReflection) { deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_reflectionMatrix), waterManager.m_ReflectionMatrix.AsFloatArray()); } deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_ambient), lightEnv.m_AmbientColor.AsFloatArray()); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_sunDir), lightEnv.GetSunDir().AsFloatArray()); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_sunColor), lightEnv.m_SunColor.AsFloatArray()); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_color), waterManager.m_WaterColor.AsFloatArray()); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_tint), waterManager.m_WaterTint.AsFloatArray()); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_waviness), waterManager.m_Waviness); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_murkiness), waterManager.m_Murkiness); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_windAngle), waterManager.m_WindAngle); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_repeatScale), 1.0f / repeatPeriod); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_losTransform), losTexture.GetTextureMatrix()[0], losTexture.GetTextureMatrix()[12]); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_cameraPos), camera.GetOrientation().GetTranslation().AsFloatArray()); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_fogColor), lightEnv.m_FogColor.AsFloatArray()); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_fogParams), lightEnv.m_FogFactor, lightEnv.m_FogMax); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_time), static_cast(time)); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_screenSize), static_cast(g_Renderer.GetWidth()), static_cast(g_Renderer.GetHeight())); if (waterManager.m_WaterType == L"clap") { deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_waveParams1), 30.0f, 1.5f, 20.0f, 0.03f); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_waveParams2), 0.5f, 0.0f, 0.0f, 0.0f); } else if (waterManager.m_WaterType == L"lake") { deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_waveParams1), 8.5f, 1.5f, 15.0f, 0.03f); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_waveParams2), 0.2f, 0.0f, 0.0f, 0.07f); } else { deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_waveParams1), 15.0f, 0.8f, 10.0f, 0.1f); deviceCommandContext->SetUniform( fancyWaterShader->GetBindingSlot(str_waveParams2), 0.3f, 0.0f, 0.1f, 0.3f); } if (shadow) shadow->BindTo(deviceCommandContext, fancyWaterShader); for (CPatchRData* data : m->visiblePatches[cullGroup]) { data->RenderWaterSurface(deviceCommandContext, true); if (waterManager.m_WaterFancyEffects) data->RenderWaterShore(deviceCommandContext); } deviceCommandContext->EndPass(); return true; } void TerrainRenderer::RenderSimpleWater( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, int cullGroup) { PROFILE3_GPU("simple water"); GPU_SCOPED_LABEL(deviceCommandContext, "Render Simple Water"); const WaterManager& waterManager = g_Renderer.GetSceneRenderer().GetWaterManager(); - CLOSTexture& losTexture = g_Game->GetView()->GetLOSTexture(); + CLOSTexture& losTexture = g_Renderer.GetSceneRenderer().GetScene().GetLOSTexture(); const double time = waterManager.m_WaterTexTimer; CShaderDefines context; if (g_Renderer.GetSceneRenderer().GetWaterRenderMode() == WIREFRAME) context.Add(str_MODE_WIREFRAME, str_1); CShaderTechniquePtr waterSimpleTech = g_Renderer.GetShaderManager().LoadEffect(str_water_simple, context); deviceCommandContext->SetGraphicsPipelineState( waterSimpleTech->GetGraphicsPipelineStateDesc()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* waterSimpleShader = waterSimpleTech->GetShader(); const CTexturePtr& waterTexture = waterManager.m_WaterTexture[waterManager.GetCurrentTextureIndex(1.6)]; waterTexture->UploadBackendTextureIfNeeded(deviceCommandContext); deviceCommandContext->SetTexture( waterSimpleShader->GetBindingSlot(str_baseTex), waterTexture->GetBackendTexture()); deviceCommandContext->SetTexture( waterSimpleShader->GetBindingSlot(str_losTex), losTexture.GetTextureSmooth()); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( waterSimpleShader->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetUniform( waterSimpleShader->GetBindingSlot(str_losTransform), losTexture.GetTextureMatrix()[0], losTexture.GetTextureMatrix()[12]); deviceCommandContext->SetUniform( waterSimpleShader->GetBindingSlot(str_time), static_cast(time)); deviceCommandContext->SetUniform( waterSimpleShader->GetBindingSlot(str_color), waterManager.m_WaterColor.AsFloatArray()); std::vector& visiblePatches = m->visiblePatches[cullGroup]; for (size_t i = 0; i < visiblePatches.size(); ++i) { CPatchRData* data = visiblePatches[i]; data->RenderWaterSurface(deviceCommandContext, false); } deviceCommandContext->EndPass(); } /////////////////////////////////////////////////////////////////// // Render water that is part of the terrain void TerrainRenderer::RenderWater( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup, ShadowMap* shadow) { const WaterManager& waterManager = g_Renderer.GetSceneRenderer().GetWaterManager(); if (!waterManager.WillRenderFancyWater()) RenderSimpleWater(deviceCommandContext, cullGroup); else RenderFancyWater(deviceCommandContext, context, cullGroup, shadow); } void TerrainRenderer::RenderWaterFoamOccluders( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, int cullGroup) { CSceneRenderer& sceneRenderer = g_Renderer.GetSceneRenderer(); const WaterManager& waterManager = sceneRenderer.GetWaterManager(); if (!waterManager.WillRenderFancyWater()) return; GPU_SCOPED_LABEL(deviceCommandContext, "Render water foam occluders"); // Render normals and foam to a framebuffer if we're using fancy effects. deviceCommandContext->SetFramebuffer(waterManager.m_FancyEffectsFramebuffer.get()); // Overwrite waves that would be behind the ground. CShaderTechniquePtr dummyTech = g_Renderer.GetShaderManager().LoadEffect(str_solid); Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc = dummyTech->GetGraphicsPipelineStateDesc(); pipelineStateDesc.depthStencilState.depthTestEnabled = true; pipelineStateDesc.rasterizationState.cullMode = Renderer::Backend::CullMode::NONE; deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* dummyShader = dummyTech->GetShader(); const CMatrix3D transform = sceneRenderer.GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( dummyShader->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetUniform( dummyShader->GetBindingSlot(str_color), 0.0f, 0.0f, 0.0f, 0.0f); for (CPatchRData* data : m->visiblePatches[cullGroup]) data->RenderWaterShore(deviceCommandContext); deviceCommandContext->EndPass(); deviceCommandContext->SetFramebuffer( deviceCommandContext->GetDevice()->GetCurrentBackbuffer()); } void TerrainRenderer::RenderPriorities(CCanvas2D& canvas, int cullGroup) { PROFILE("priorities"); ENSURE(m->phase == Phase_Render); CTextRenderer textRenderer; textRenderer.SetCurrentFont(CStrIntern("mono-stroke-10")); textRenderer.SetCurrentColor(CColor(1.0f, 1.0f, 0.0f, 1.0f)); std::vector& visiblePatches = m->visiblePatches[cullGroup]; for (size_t i = 0; i < visiblePatches.size(); ++i) visiblePatches[i]->RenderPriorities(textRenderer); canvas.DrawText(textRenderer); }