Index: ps/trunk/binaries/data/mods/public/shaders/arb/debug_overlay.vp =================================================================== --- ps/trunk/binaries/data/mods/public/shaders/arb/debug_overlay.vp (revision 27208) +++ ps/trunk/binaries/data/mods/public/shaders/arb/debug_overlay.vp (revision 27209) @@ -1,25 +1,25 @@ !!ARBvp1.0 PARAM transform[4] = { program.local[0..3] }; #if DEBUG_TEXTURED -PARAM textureTransform[4] = { program.local[4..7] }; +PARAM textureTransform = program.local[4]; #endif ATTRIB position = vertex.position; #if DEBUG_TEXTURED ATTRIB uv = vertex.texcoord[0]; #endif DP4 result.position.x, transform[0], position; DP4 result.position.y, transform[1], position; DP4 result.position.z, transform[2], position; DP4 result.position.w, transform[3], position; #if DEBUG_TEXTURED OUTPUT v_tex = result.texcoord[0]; -DP4 v_tex.x, textureTransform[0], uv; -DP4 v_tex.y, textureTransform[1], uv; +MUL v_tex.x, textureTransform.x, uv.x; +MUL v_tex.y, textureTransform.y, uv.z; #endif END Index: ps/trunk/binaries/data/mods/public/shaders/arb/debug_overlay.xml =================================================================== --- ps/trunk/binaries/data/mods/public/shaders/arb/debug_overlay.xml (revision 27208) +++ ps/trunk/binaries/data/mods/public/shaders/arb/debug_overlay.xml (revision 27209) @@ -1,16 +1,16 @@ - + Index: ps/trunk/binaries/data/mods/public/shaders/arb/overlay_solid.vp =================================================================== --- ps/trunk/binaries/data/mods/public/shaders/arb/overlay_solid.vp (revision 27208) +++ ps/trunk/binaries/data/mods/public/shaders/arb/overlay_solid.vp (revision 27209) @@ -1,18 +1,18 @@ !!ARBvp1.0 PARAM transform[4] = { program.local[0..3] }; -PARAM instancingTransform[4] = { program.local[4..7] }; +PARAM instancingTransform = program.local[4]; TEMP position; -DP4 position.x, instancingTransform[0], vertex.position; -DP4 position.y, instancingTransform[1], vertex.position; -DP4 position.z, instancingTransform[2], vertex.position; +MAD position.x, vertex.position.x, instancingTransform.w, instancingTransform.x; +MAD position.y, vertex.position.y, instancingTransform.w, instancingTransform.y; +MAD position.z, vertex.position.z, instancingTransform.w, instancingTransform.z; MOV position.w, 1.0; DP4 result.position.x, transform[0], position; DP4 result.position.y, transform[1], position; DP4 result.position.z, transform[2], position; DP4 result.position.w, transform[3], position; END Index: ps/trunk/binaries/data/mods/public/shaders/arb/overlay_solid.xml =================================================================== --- ps/trunk/binaries/data/mods/public/shaders/arb/overlay_solid.xml (revision 27208) +++ ps/trunk/binaries/data/mods/public/shaders/arb/overlay_solid.xml (revision 27209) @@ -1,14 +1,14 @@ - + Index: ps/trunk/binaries/data/mods/public/shaders/glsl/overlay_solid.vs =================================================================== --- ps/trunk/binaries/data/mods/public/shaders/glsl/overlay_solid.vs (revision 27208) +++ ps/trunk/binaries/data/mods/public/shaders/glsl/overlay_solid.vs (revision 27209) @@ -1,14 +1,14 @@ #version 120 #include "common/vertex.h" uniform mat4 transform; -uniform mat4 instancingTransform; +uniform vec4 instancingTransform; VERTEX_INPUT_ATTRIBUTE(0, vec3, a_vertex); void main() { - vec4 worldPos = instancingTransform * vec4(a_vertex, 1.0); + vec4 worldPos = vec4(a_vertex * instancingTransform.w + instancingTransform.xyz, 1.0); OUTPUT_VERTEX_POSITION(transform * worldPos); } Index: ps/trunk/source/renderer/OverlayRenderer.cpp =================================================================== --- ps/trunk/source/renderer/OverlayRenderer.cpp (revision 27208) +++ ps/trunk/source/renderer/OverlayRenderer.cpp (revision 27209) @@ -1,822 +1,817 @@ /* 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 "OverlayRenderer.h" #include "graphics/Camera.h" #include "graphics/LOSTexture.h" #include "graphics/Overlay.h" #include "graphics/ShaderManager.h" #include "graphics/Terrain.h" #include "graphics/TextureManager.h" #include "lib/hash.h" #include "maths/MathUtil.h" #include "maths/Quaternion.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/Profile.h" #include "renderer/DebugRenderer.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "renderer/TexturedLineRData.h" #include "renderer/VertexArray.h" #include "renderer/VertexBuffer.h" #include "renderer/VertexBufferManager.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/system/SimContext.h" #include namespace { CShaderTechniquePtr GetOverlayLineShaderTechnique(const CShaderDefines& defines) { return g_Renderer.GetShaderManager().LoadEffect(str_overlay_line, defines); } } // anonymous namespace /** * Key used to group quads into batches for more efficient rendering. Currently groups by the combination * of the main texture and the texture mask, to minimize texture swapping during rendering. */ struct QuadBatchKey { QuadBatchKey (const CTexturePtr& texture, const CTexturePtr& textureMask) : m_Texture(texture), m_TextureMask(textureMask) { } bool operator==(const QuadBatchKey& other) const { return (m_Texture == other.m_Texture && m_TextureMask == other.m_TextureMask); } CTexturePtr m_Texture; CTexturePtr m_TextureMask; }; struct QuadBatchHash { std::size_t operator()(const QuadBatchKey& d) const { size_t seed = 0; hash_combine(seed, d.m_Texture); hash_combine(seed, d.m_TextureMask); return seed; } }; /** * Holds information about a single quad rendering batch. */ class QuadBatchData : public CRenderData { public: QuadBatchData() : m_IndicesBase(0), m_NumRenderQuads(0) { } /// Holds the quad overlay structures requested to be rendered in this batch. Must be cleared /// after each frame. std::vector m_Quads; /// Start index of this batch into the dedicated quad indices VertexArray (see OverlayInternals). size_t m_IndicesBase; /// Amount of quads to actually render in this batch. Potentially (although unlikely to be) /// different from m_Quads.size() due to restrictions on the total amount of quads that can be /// rendered. Must be reset after each frame. size_t m_NumRenderQuads; }; struct OverlayRendererInternals { using QuadBatchMap = std::unordered_map; OverlayRendererInternals(); ~OverlayRendererInternals(){ } std::vector lines; std::vector texlines; std::vector sprites; std::vector quads; std::vector spheres; QuadBatchMap quadBatchMap; // Dedicated vertex/index buffers for rendering all quads (to within the limits set by // MAX_QUAD_OVERLAYS). VertexArray quadVertices; VertexArray::Attribute quadAttributePos; VertexArray::Attribute quadAttributeColor; VertexArray::Attribute quadAttributeUV; VertexIndexArray quadIndices; // Maximum amount of quad overlays we support for rendering. This limit is set to be able to // render all quads from a single dedicated VB without having to reallocate it, which is much // faster in the typical case of rendering only a handful of quads. When modifying this value, // you must take care for the new amount of quads to fit in a single backend buffer (which is // not likely to be a problem). static const size_t MAX_QUAD_OVERLAYS = 1024; // Sets of commonly-(re)used shader defines. CShaderDefines defsOverlayLineNormal; CShaderDefines defsOverlayLineAlwaysVisible; CShaderDefines defsQuadOverlay; // Geometry for a unit sphere std::vector sphereVertexes; std::vector sphereIndexes; void GenerateSphere(); // Performs one-time setup. Called from CRenderer::Open, after graphics capabilities have // been detected. Note that no backend buffer must be created before this is called, since // the shader path and graphics capabilities are not guaranteed to be stable before this // point. void Initialize(); }; const float OverlayRenderer::OVERLAY_VOFFSET = 0.2f; OverlayRendererInternals::OverlayRendererInternals() : quadVertices(Renderer::Backend::IBuffer::Type::VERTEX, true), quadIndices(false) { quadAttributePos.format = Renderer::Backend::Format::R32G32B32_SFLOAT; quadVertices.AddAttribute(&quadAttributePos); quadAttributeColor.format = Renderer::Backend::Format::R8G8B8A8_UNORM; quadVertices.AddAttribute(&quadAttributeColor); quadAttributeUV.format = Renderer::Backend::Format::R16G16_SINT; quadVertices.AddAttribute(&quadAttributeUV); // Note that we're reusing the textured overlay line shader for the quad overlay rendering. This // is because their code is almost identical; the only difference is that for the quad overlays // we want to use a vertex color stream as opposed to an objectColor uniform. To this end, the // shader has been set up to switch between the two behaviours based on the USE_OBJECTCOLOR define. defsOverlayLineNormal.Add(str_USE_OBJECTCOLOR, str_1); defsOverlayLineAlwaysVisible.Add(str_USE_OBJECTCOLOR, str_1); defsOverlayLineAlwaysVisible.Add(str_IGNORE_LOS, str_1); } void OverlayRendererInternals::Initialize() { // Perform any initialization after graphics capabilities have been detected. Notably, // only at this point can we safely allocate backend buffer (in contrast to e.g. in the constructor), // because their creation depends on the shader path, which is not reliably set before this point. quadVertices.SetNumberOfVertices(MAX_QUAD_OVERLAYS * 4); quadVertices.Layout(); // allocate backing store quadIndices.SetNumberOfVertices(MAX_QUAD_OVERLAYS * 6); quadIndices.Layout(); // allocate backing store // Since the quads in the vertex array are independent and always consist of exactly 4 vertices per quad, the // indices are always the same; we can therefore fill in all the indices once and pretty much forget about // them. We then also no longer need its backing store, since we never change any indices afterwards. VertexArrayIterator index = quadIndices.GetIterator(); for (u16 i = 0; i < static_cast(MAX_QUAD_OVERLAYS); ++i) { *index++ = i * 4 + 0; *index++ = i * 4 + 1; *index++ = i * 4 + 2; *index++ = i * 4 + 2; *index++ = i * 4 + 3; *index++ = i * 4 + 0; } quadIndices.Upload(); quadIndices.FreeBackingStore(); } OverlayRenderer::OverlayRenderer() { m = new OverlayRendererInternals(); } OverlayRenderer::~OverlayRenderer() { delete m; } void OverlayRenderer::Initialize() { m->Initialize(); } void OverlayRenderer::Submit(SOverlayLine* line) { m->lines.push_back(line); } void OverlayRenderer::Submit(SOverlayTexturedLine* line) { // Simplify the rest of the code by guaranteeing non-empty lines if (line->m_Coords.empty()) return; m->texlines.push_back(line); } void OverlayRenderer::Submit(SOverlaySprite* overlay) { m->sprites.push_back(overlay); } void OverlayRenderer::Submit(SOverlayQuad* overlay) { m->quads.push_back(overlay); } void OverlayRenderer::Submit(SOverlaySphere* overlay) { m->spheres.push_back(overlay); } void OverlayRenderer::EndFrame() { m->lines.clear(); m->texlines.clear(); m->sprites.clear(); m->quads.clear(); m->spheres.clear(); // this should leave the capacity unchanged, which is okay since it // won't be very large or very variable // Empty the batch rendering data structures, but keep their key mappings around for the next frames for (OverlayRendererInternals::QuadBatchMap::iterator it = m->quadBatchMap.begin(); it != m->quadBatchMap.end(); ++it) { QuadBatchData& quadBatchData = (it->second); quadBatchData.m_Quads.clear(); quadBatchData.m_NumRenderQuads = 0; quadBatchData.m_IndicesBase = 0; } } void OverlayRenderer::PrepareForRendering() { PROFILE3("prepare overlays"); // This is where we should do something like sort the overlays by // color/sprite/etc for more efficient rendering for (size_t i = 0; i < m->texlines.size(); ++i) { SOverlayTexturedLine* line = m->texlines[i]; if (!line->m_RenderData) { line->m_RenderData = std::make_shared(); line->m_RenderData->Update(*line); // We assume the overlay line will get replaced by the caller // if terrain changes, so we don't need to detect that here and // call Update again. Also we assume the caller won't change // any of the parameters after first submitting the line. } } // Group quad overlays by their texture/mask combination for efficient rendering // TODO: consider doing this directly in Submit() for (size_t i = 0; i < m->quads.size(); ++i) { SOverlayQuad* const quad = m->quads[i]; QuadBatchKey textures(quad->m_Texture, quad->m_TextureMask); QuadBatchData& batchRenderData = m->quadBatchMap[textures]; // will create entry if it doesn't already exist // add overlay to list of quads batchRenderData.m_Quads.push_back(quad); } const CVector3D vOffset(0, OverlayRenderer::OVERLAY_VOFFSET, 0); // Write quad overlay vertices/indices to VA backing store VertexArrayIterator vertexPos = m->quadAttributePos.GetIterator(); VertexArrayIterator vertexColor = m->quadAttributeColor.GetIterator(); VertexArrayIterator vertexUV = m->quadAttributeUV.GetIterator(); size_t indicesIdx = 0; size_t totalNumQuads = 0; for (OverlayRendererInternals::QuadBatchMap::iterator it = m->quadBatchMap.begin(); it != m->quadBatchMap.end(); ++it) { QuadBatchData& batchRenderData = (it->second); batchRenderData.m_NumRenderQuads = 0; if (batchRenderData.m_Quads.empty()) continue; // Remember the current index into the (entire) indices array as our base offset for this batch batchRenderData.m_IndicesBase = indicesIdx; // points to the index where each iteration's vertices will be appended for (size_t i = 0; i < batchRenderData.m_Quads.size() && totalNumQuads < OverlayRendererInternals::MAX_QUAD_OVERLAYS; i++) { const SOverlayQuad* quad = batchRenderData.m_Quads[i]; const SColor4ub quadColor = quad->m_Color.AsSColor4ub(); *vertexPos++ = quad->m_Corners[0] + vOffset; *vertexPos++ = quad->m_Corners[1] + vOffset; *vertexPos++ = quad->m_Corners[2] + vOffset; *vertexPos++ = quad->m_Corners[3] + vOffset; (*vertexUV)[0] = 0; (*vertexUV)[1] = 0; ++vertexUV; (*vertexUV)[0] = 0; (*vertexUV)[1] = 1; ++vertexUV; (*vertexUV)[0] = 1; (*vertexUV)[1] = 1; ++vertexUV; (*vertexUV)[0] = 1; (*vertexUV)[1] = 0; ++vertexUV; *vertexColor++ = quadColor; *vertexColor++ = quadColor; *vertexColor++ = quadColor; *vertexColor++ = quadColor; indicesIdx += 6; totalNumQuads++; batchRenderData.m_NumRenderQuads++; } } m->quadVertices.Upload(); // don't free the backing store! we'll overwrite it on the next frame to save a reallocation. m->quadVertices.PrepareForRendering(); } void OverlayRenderer::Upload( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { m->quadVertices.UploadIfNeeded(deviceCommandContext); m->quadIndices.UploadIfNeeded(deviceCommandContext); } void OverlayRenderer::RenderOverlaysBeforeWater( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { PROFILE3_GPU("overlays (before)"); GPU_SCOPED_LABEL(deviceCommandContext, "Render overlays before water"); for (SOverlayLine* line : m->lines) { if (line->m_Coords.empty()) continue; g_Renderer.GetDebugRenderer().DrawLine(line->m_Coords, line->m_Color, static_cast(line->m_Thickness)); } } void OverlayRenderer::RenderOverlaysAfterWater( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { PROFILE3_GPU("overlays (after)"); GPU_SCOPED_LABEL(deviceCommandContext, "Render overlays after water"); RenderTexturedOverlayLines(deviceCommandContext); RenderQuadOverlays(deviceCommandContext); RenderSphereOverlays(deviceCommandContext); } void OverlayRenderer::RenderTexturedOverlayLines(Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { if (m->texlines.empty()) return; CLOSTexture& los = g_Renderer.GetSceneRenderer().GetScene().GetLOSTexture(); CShaderTechniquePtr shaderTechTexLineNormal = GetOverlayLineShaderTechnique(m->defsOverlayLineNormal); if (shaderTechTexLineNormal) { Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc = shaderTechTexLineNormal->GetGraphicsPipelineStateDesc(); pipelineStateDesc.depthStencilState.depthWriteEnabled = false; 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; if (g_Renderer.GetSceneRenderer().GetOverlayRenderMode() == WIREFRAME) pipelineStateDesc.rasterizationState.polygonMode = Renderer::Backend::PolygonMode::LINE; deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shaderTexLineNormal = shaderTechTexLineNormal->GetShader(); deviceCommandContext->SetTexture( shaderTexLineNormal->GetBindingSlot(str_losTex), los.GetTexture()); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( shaderTexLineNormal->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetUniform( shaderTexLineNormal->GetBindingSlot(str_losTransform), los.GetTextureMatrix()[0], los.GetTextureMatrix()[12]); // batch render only the non-always-visible overlay lines using the normal shader RenderTexturedOverlayLines(deviceCommandContext, shaderTexLineNormal, false); deviceCommandContext->EndPass(); } CShaderTechniquePtr shaderTechTexLineAlwaysVisible = GetOverlayLineShaderTechnique(m->defsOverlayLineAlwaysVisible); if (shaderTechTexLineAlwaysVisible) { Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc = shaderTechTexLineAlwaysVisible->GetGraphicsPipelineStateDesc(); pipelineStateDesc.depthStencilState.depthWriteEnabled = false; 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; if (g_Renderer.GetSceneRenderer().GetOverlayRenderMode() == WIREFRAME) pipelineStateDesc.rasterizationState.polygonMode = Renderer::Backend::PolygonMode::LINE; deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shaderTexLineAlwaysVisible = shaderTechTexLineAlwaysVisible->GetShader(); // TODO: losTex and losTransform are unused in the always visible shader; see if these can be safely omitted deviceCommandContext->SetTexture( shaderTexLineAlwaysVisible->GetBindingSlot(str_losTex), los.GetTexture()); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( shaderTexLineAlwaysVisible->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetUniform( shaderTexLineAlwaysVisible->GetBindingSlot(str_losTransform), los.GetTextureMatrix()[0], los.GetTextureMatrix()[12]); // batch render only the always-visible overlay lines using the LoS-ignored shader RenderTexturedOverlayLines(deviceCommandContext, shaderTexLineAlwaysVisible, true); deviceCommandContext->EndPass(); } } void OverlayRenderer::RenderTexturedOverlayLines( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IShaderProgram* shader, bool alwaysVisible) { for (size_t i = 0; i < m->texlines.size(); ++i) { SOverlayTexturedLine* line = m->texlines[i]; // render only those lines matching the requested alwaysVisible status if (!line->m_RenderData || line->m_AlwaysVisible != alwaysVisible) continue; ENSURE(line->m_RenderData); line->m_RenderData->Render(deviceCommandContext, *line, shader); } } void OverlayRenderer::RenderQuadOverlays( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { if (m->quadBatchMap.empty()) return; CShaderTechniquePtr shaderTech = GetOverlayLineShaderTechnique(m->defsQuadOverlay); if (!shaderTech) return; Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc = shaderTech->GetGraphicsPipelineStateDesc(); pipelineStateDesc.depthStencilState.depthWriteEnabled = false; 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; if (g_Renderer.GetSceneRenderer().GetOverlayRenderMode() == WIREFRAME) pipelineStateDesc.rasterizationState.polygonMode = Renderer::Backend::PolygonMode::LINE; deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = shaderTech->GetShader(); CLOSTexture& los = g_Renderer.GetSceneRenderer().GetScene().GetLOSTexture(); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_losTex), los.GetTexture()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_losTransform), los.GetTextureMatrix()[0], los.GetTextureMatrix()[12]); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), transform.AsFloatArray()); const uint32_t vertexStride = m->quadVertices.GetStride(); const uint32_t firstVertexOffset = m->quadVertices.GetOffset() * vertexStride; const int32_t baseTexBindingSlot = shader->GetBindingSlot(str_baseTex); const int32_t maskTexBindingSlot = shader->GetBindingSlot(str_maskTex); for (OverlayRendererInternals::QuadBatchMap::iterator it = m->quadBatchMap.begin(); it != m->quadBatchMap.end(); ++it) { QuadBatchData& batchRenderData = it->second; const size_t batchNumQuads = batchRenderData.m_NumRenderQuads; if (batchNumQuads == 0) continue; const QuadBatchKey& maskPair = it->first; maskPair.m_Texture->UploadBackendTextureIfNeeded(deviceCommandContext); maskPair.m_TextureMask->UploadBackendTextureIfNeeded(deviceCommandContext); deviceCommandContext->SetTexture( baseTexBindingSlot, maskPair.m_Texture->GetBackendTexture()); deviceCommandContext->SetTexture( maskTexBindingSlot, maskPair.m_TextureMask->GetBackendTexture()); // TODO: move setting format out of the loop, we might want move the offset // to the index offset when it's supported. deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, m->quadAttributePos.format, m->quadAttributePos.offset, vertexStride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::COLOR, m->quadAttributeColor.format, m->quadAttributeColor.offset, vertexStride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV0, m->quadAttributeUV.format, m->quadAttributeUV.offset, vertexStride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV1, m->quadAttributeUV.format, m->quadAttributeUV.offset, vertexStride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexBuffer( 0, m->quadVertices.GetBuffer(), firstVertexOffset); deviceCommandContext->SetIndexBuffer(m->quadIndices.GetBuffer()); deviceCommandContext->DrawIndexed(m->quadIndices.GetOffset() + batchRenderData.m_IndicesBase, batchNumQuads * 6, 0); g_Renderer.GetStats().m_DrawCalls++; g_Renderer.GetStats().m_OverlayTris += batchNumQuads*2; } deviceCommandContext->EndPass(); } void OverlayRenderer::RenderForegroundOverlays( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CCamera& viewCamera) { PROFILE3_GPU("overlays (fg)"); GPU_SCOPED_LABEL(deviceCommandContext, "Render foreground overlays"); const CVector3D right = -viewCamera.GetOrientation().GetLeft(); const CVector3D up = viewCamera.GetOrientation().GetUp(); CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_foreground_overlay); Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc = tech->GetGraphicsPipelineStateDesc(); pipelineStateDesc.depthStencilState.depthTestEnabled = false; 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; if (g_Renderer.GetSceneRenderer().GetOverlayRenderMode() == WIREFRAME) pipelineStateDesc.rasterizationState.polygonMode = Renderer::Backend::PolygonMode::LINE; deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = tech->GetShader(); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), transform.AsFloatArray()); const CVector2D uvs[6] = { {0.0f, 1.0f}, {1.0f, 1.0f}, {1.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 0.0f}, {0.0f, 0.0f}, }; deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, 0, sizeof(float) * 3, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32_SFLOAT, 0, sizeof(float) * 2, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 1); deviceCommandContext->SetVertexBufferData( 1, &uvs[0], std::size(uvs) * sizeof(uvs[0])); const int32_t baseTexBindingSlot = shader->GetBindingSlot(str_baseTex); const int32_t colorMulBindingSlot = shader->GetBindingSlot(str_colorMul); for (size_t i = 0; i < m->sprites.size(); ++i) { SOverlaySprite* sprite = m->sprites[i]; if (!i || sprite->m_Texture != m->sprites[i - 1]->m_Texture) { sprite->m_Texture->UploadBackendTextureIfNeeded(deviceCommandContext); deviceCommandContext->SetTexture( baseTexBindingSlot, sprite->m_Texture->GetBackendTexture()); } deviceCommandContext->SetUniform( colorMulBindingSlot, sprite->m_Color.AsFloatArray()); const CVector3D position[6] = { sprite->m_Position + right*sprite->m_X0 + up*sprite->m_Y0, sprite->m_Position + right*sprite->m_X1 + up*sprite->m_Y0, sprite->m_Position + right*sprite->m_X1 + up*sprite->m_Y1, sprite->m_Position + right*sprite->m_X0 + up*sprite->m_Y0, sprite->m_Position + right*sprite->m_X1 + up*sprite->m_Y1, sprite->m_Position + right*sprite->m_X0 + up*sprite->m_Y1 }; deviceCommandContext->SetVertexBufferData( 0, &position[0].X, std::size(position) * sizeof(position[0])); deviceCommandContext->Draw(0, 6); g_Renderer.GetStats().m_DrawCalls++; g_Renderer.GetStats().m_OverlayTris += 2; } deviceCommandContext->EndPass(); } static void TessellateSphereFace(const CVector3D& a, u16 ai, const CVector3D& b, u16 bi, const CVector3D& c, u16 ci, std::vector& vertexes, std::vector& indexes, int level) { if (level == 0) { indexes.push_back(ai); indexes.push_back(bi); indexes.push_back(ci); } else { CVector3D d = (a + b).Normalized(); CVector3D e = (b + c).Normalized(); CVector3D f = (c + a).Normalized(); int di = vertexes.size() / 3; vertexes.push_back(d.X); vertexes.push_back(d.Y); vertexes.push_back(d.Z); int ei = vertexes.size() / 3; vertexes.push_back(e.X); vertexes.push_back(e.Y); vertexes.push_back(e.Z); int fi = vertexes.size() / 3; vertexes.push_back(f.X); vertexes.push_back(f.Y); vertexes.push_back(f.Z); TessellateSphereFace(a,ai, d,di, f,fi, vertexes, indexes, level-1); TessellateSphereFace(d,di, b,bi, e,ei, vertexes, indexes, level-1); TessellateSphereFace(f,fi, e,ei, c,ci, vertexes, indexes, level-1); TessellateSphereFace(d,di, e,ei, f,fi, vertexes, indexes, level-1); } } static void TessellateSphere(std::vector& vertexes, std::vector& indexes, int level) { /* Start with a tetrahedron, then tessellate */ float s = sqrtf(0.5f); #define VERT(a,b,c) vertexes.push_back(a); vertexes.push_back(b); vertexes.push_back(c); VERT(-s, 0, -s); VERT( s, 0, -s); VERT( s, 0, s); VERT(-s, 0, s); VERT( 0, -1, 0); VERT( 0, 1, 0); #define FACE(a,b,c) \ TessellateSphereFace( \ CVector3D(vertexes[a*3], vertexes[a*3+1], vertexes[a*3+2]), a, \ CVector3D(vertexes[b*3], vertexes[b*3+1], vertexes[b*3+2]), b, \ CVector3D(vertexes[c*3], vertexes[c*3+1], vertexes[c*3+2]), c, \ vertexes, indexes, level); FACE(0,4,1); FACE(1,4,2); FACE(2,4,3); FACE(3,4,0); FACE(1,5,0); FACE(2,5,1); FACE(3,5,2); FACE(0,5,3); #undef FACE #undef VERT } void OverlayRendererInternals::GenerateSphere() { if (sphereVertexes.empty()) TessellateSphere(sphereVertexes, sphereIndexes, 3); } void OverlayRenderer::RenderSphereOverlays( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { PROFILE3_GPU("overlays (spheres)"); if (m->spheres.empty()) return; Renderer::Backend::IShaderProgram* shader = nullptr; CShaderTechniquePtr tech; tech = g_Renderer.GetShaderManager().LoadEffect(str_overlay_solid); Renderer::Backend::GraphicsPipelineStateDesc pipelineStateDesc = tech->GetGraphicsPipelineStateDesc(); pipelineStateDesc.depthStencilState.depthWriteEnabled = false; 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; deviceCommandContext->SetGraphicsPipelineState(pipelineStateDesc); deviceCommandContext->BeginPass(); shader = tech->GetShader(); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), transform.AsFloatArray()); m->GenerateSphere(); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32B32_SFLOAT, 0, 0, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexBufferData( 0, m->sphereVertexes.data(), m->sphereVertexes.size() * sizeof(m->sphereVertexes[0])); deviceCommandContext->SetIndexBufferData( m->sphereIndexes.data(), m->sphereIndexes.size() * sizeof(m->sphereIndexes[0])); - for (size_t i = 0; i < m->spheres.size(); ++i) + for (const SOverlaySphere* sphere : m->spheres) { - SOverlaySphere* sphere = m->spheres[i]; - - CMatrix3D instancingTransform; - instancingTransform.SetIdentity(); - instancingTransform.Scale( - sphere->m_Radius, sphere->m_Radius, sphere->m_Radius); - instancingTransform.Translate(sphere->m_Center); + const CVector4D instancingTransform{ + sphere->m_Center.X, sphere->m_Center.Y, sphere->m_Center.Z, sphere->m_Radius}; deviceCommandContext->SetUniform( shader->GetBindingSlot(str_instancingTransform), instancingTransform.AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_color), sphere->m_Color.AsFloatArray()); deviceCommandContext->DrawIndexed(0, m->sphereIndexes.size(), 0); g_Renderer.GetStats().m_DrawCalls++; g_Renderer.GetStats().m_OverlayTris = m->sphereIndexes.size()/3; } deviceCommandContext->EndPass(); } Index: ps/trunk/source/simulation2/components/CCmpUnitRenderer.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpUnitRenderer.cpp (revision 27208) +++ ps/trunk/source/simulation2/components/CCmpUnitRenderer.cpp (revision 27209) @@ -1,473 +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 "simulation2/system/Component.h" #include "ICmpUnitRenderer.h" #include "simulation2/MessageTypes.h" #include "ICmpPosition.h" #include "ICmpRangeManager.h" #include "ICmpSelectable.h" #include "ICmpVisibility.h" #include "ICmpVisual.h" #include "graphics/ModelAbstract.h" #include "graphics/ObjectEntry.h" #include "graphics/Overlay.h" #include "graphics/Unit.h" #include "maths/BoundingSphere.h" #include "maths/Frustum.h" #include "maths/Matrix3D.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "renderer/RenderingOptions.h" #include "renderer/Scene.h" #include "tools/atlas/GameInterface/GameLoop.h" /** * Efficiently(ish) renders all the units in the world. * * The class maintains a list of all units that currently exist, and the data * needed for frustum-culling them. To minimise the amount of work done per * frame (despite a unit's interpolated position changing every frame), the * culling data is only updated once per turn: we store the position at the * start of the turn, and the position at the end of the turn, and assume the * unit might be anywhere between those two points (linearly). * * (Note this is a slightly invalid assumption: units don't always move linearly, * since their interpolated position depends on terrain and water. But over a * single turn it's probably going to be a good enough approximation, and will * only break for units that both start and end the turn off-screen.) * * We want to ignore rotation entirely, since it's a complex function of * interpolated position and terrain. So we store a bounding sphere, which * is rotation-independent, instead of a bounding box. */ class CCmpUnitRenderer final : public ICmpUnitRenderer { public: struct SUnit { CEntityHandle entity; CUnit* actor; int flags; /** * m_FrameNumber from when the model's transform was last updated. * This is used to avoid recomputing it multiple times per frame * if a model is visible in multiple cull groups. */ int lastTransformFrame; /** * Worst-case bounding shape, relative to position. Needs to account * for all possible animations, orientations, etc. */ CBoundingSphere boundsApprox; /** * Cached LOS visibility status. */ LosVisibility visibility; bool visibilityDirty; /** * Whether the unit has a valid position. If false, pos0 and pos1 * are meaningless. */ bool inWorld; /** * World-space positions to interpolate between. */ CVector3D pos0; CVector3D pos1; /** * Bounds encompassing the unit's bounds when it is anywhere between * pos0 and pos1. */ CBoundingSphere sweptBounds; /** * For debug overlay. */ bool culled; }; std::vector m_Units; std::vector m_UnitTagsFree; int m_FrameNumber; float m_FrameOffset; bool m_EnableDebugOverlays; std::vector m_DebugSpheres; static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_TurnStart); componentManager.SubscribeToMessageType(MT_Interpolate); componentManager.SubscribeToMessageType(MT_RenderSubmit); } DEFAULT_COMPONENT_ALLOCATOR(UnitRenderer) static std::string GetSchema() { return ""; } void Init(const CParamNode& UNUSED(paramNode)) override { m_FrameNumber = 0; m_FrameOffset = 0.0f; m_EnableDebugOverlays = false; } void Deinit() override { } void Serialize(ISerializer& UNUSED(serialize)) override { } void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) override { Init(paramNode); } void HandleMessage(const CMessage& msg, bool UNUSED(global)) override { switch (msg.GetType()) { case MT_TurnStart: { TurnStart(); break; } case MT_Interpolate: { const CMessageInterpolate& msgData = static_cast (msg); Interpolate(msgData.deltaSimTime, msgData.offset); break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector, msgData.frustum, msgData.culling); break; } } } SUnit* LookupUnit(tag_t tag) { if (tag.n < 1 || tag.n - 1 >= m_Units.size()) return NULL; return &m_Units[tag.n - 1]; } tag_t AddUnit(CEntityHandle entity, CUnit* actor, const CBoundingSphere& boundsApprox, int flags) override { ENSURE(actor != NULL); tag_t tag; if (!m_UnitTagsFree.empty()) { tag = m_UnitTagsFree.back(); m_UnitTagsFree.pop_back(); } else { m_Units.push_back(SUnit()); tag.n = m_Units.size(); } SUnit* unit = LookupUnit(tag); unit->entity = entity; unit->actor = actor; unit->lastTransformFrame = -1; unit->flags = flags; unit->boundsApprox = boundsApprox; unit->inWorld = false; unit->visibilityDirty = true; unit->pos0 = unit->pos1 = CVector3D(); return tag; } void RemoveUnit(tag_t tag) override { SUnit* unit = LookupUnit(tag); unit->actor = NULL; unit->inWorld = false; m_UnitTagsFree.push_back(tag); } void RecomputeSweptBounds(SUnit* unit) { // Compute the bounding sphere of the capsule formed by // sweeping boundsApprox from pos0 to pos1 CVector3D mid = (unit->pos0 + unit->pos1) * 0.5f + unit->boundsApprox.GetCenter(); float radius = (unit->pos1 - unit->pos0).Length() * 0.5f + unit->boundsApprox.GetRadius(); unit->sweptBounds = CBoundingSphere(mid, radius); } void UpdateUnit(tag_t tag, CUnit* actor, const CBoundingSphere& boundsApprox) override { SUnit* unit = LookupUnit(tag); unit->actor = actor; unit->boundsApprox = boundsApprox; RecomputeSweptBounds(unit); } void UpdateUnitPos(tag_t tag, bool inWorld, const CVector3D& pos0, const CVector3D& pos1) override { SUnit* unit = LookupUnit(tag); unit->inWorld = inWorld; unit->pos0 = pos0; unit->pos1 = pos1; unit->visibilityDirty = true; RecomputeSweptBounds(unit); } void TurnStart(); void Interpolate(float frameTime, float frameOffset); void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling); void UpdateVisibility(SUnit& unit) const; float GetFrameOffset() const override { return m_FrameOffset; } void SetDebugOverlay(bool enabled) override { m_EnableDebugOverlays = enabled; } void PickAllEntitiesAtPoint(std::vector >& outEntities, const CVector3D& origin, const CVector3D& dir, bool allowEditorSelectables) const override { // First, make a rough test with the worst-case bounding boxes to pick all // entities/models that could possibly be hit by the ray. std::vector candidates; for (const SUnit& unit : m_Units) { if (!unit.actor || !unit.inWorld) continue; if (unit.sweptBounds.RayIntersect(origin, dir)) candidates.push_back(&unit); } // Now make a more precise test to get rid of the remaining false positives float tmin, tmax; CVector3D center; for (size_t i = 0; i< candidates.size(); ++i) { const SUnit& unit = *candidates[i]; CmpPtr cmpVisual(unit.entity); if (!cmpVisual) continue; CBoundingBoxOriented selectionBox = cmpVisual->GetSelectionBox(); if (selectionBox.IsEmpty()) { if (!allowEditorSelectables) continue; // Fall back to using old AABB selection method for decals // see: http://trac.wildfiregames.com/ticket/1032 // Decals are flat objects without a selectionShape defined, // but they should still be selectable in the editor to move them // around or delete them after they are placed. // Check campaigns/labels/ in the Actors tab of atlas for examples. CBoundingBoxAligned aABBox = cmpVisual->GetBounds(); if (aABBox.IsEmpty()) continue; if (!aABBox.RayIntersect(origin, dir, tmin, tmax)) continue; aABBox.GetCenter(center); } else { if (!selectionBox.RayIntersect(origin, dir, tmin, tmax)) continue; center = selectionBox.m_Center; } outEntities.emplace_back(unit.entity, center); } } }; void CCmpUnitRenderer::TurnStart() { PROFILE3("UnitRenderer::TurnStart"); // Assume units have stopped moving after the previous turn. If that assumption is not // correct, we will get a UpdateUnitPos to tell us about its movement in the new turn. for (size_t i = 0; i < m_Units.size(); i++) { SUnit& unit = m_Units[i]; unit.pos0 = unit.pos1; unit.sweptBounds = CBoundingSphere(unit.pos1, unit.boundsApprox.GetRadius()); // Visibility must be recomputed on the first frame during this turn unit.visibilityDirty = true; } } void CCmpUnitRenderer::Interpolate(float frameTime, float frameOffset) { PROFILE3("UnitRenderer::Interpolate"); ++m_FrameNumber; m_FrameOffset = frameOffset; // TODO: we shouldn't update all the animations etc for units that are off-screen // (but need to be careful about e.g. sounds triggered by animations of off-screen // units) for (size_t i = 0; i < m_Units.size(); i++) { SUnit& unit = m_Units[i]; if (unit.actor) unit.actor->UpdateModel(frameTime); } m_DebugSpheres.clear(); if (m_EnableDebugOverlays) { - for (size_t i = 0; i < m_Units.size(); i++) + for (const SUnit& unit : m_Units) { - SUnit& unit = m_Units[i]; if (!(unit.actor && unit.inWorld)) continue; SOverlaySphere sphere; sphere.m_Center = unit.sweptBounds.GetCenter(); sphere.m_Radius = unit.sweptBounds.GetRadius(); if (unit.culled) sphere.m_Color = CColor(1.0f, 0.5f, 0.5f, 0.5f); else sphere.m_Color = CColor(0.5f, 0.5f, 1.0f, 0.5f); m_DebugSpheres.push_back(sphere); } } } void CCmpUnitRenderer::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) { // TODO: need a coarse culling pass based on some kind of spatial data // structure - that's the main point of this design. Once we've got a // rough list of possibly-visible units, then we can do the more precise // culling. (And once it's cheap enough, we can do multiple culling passes // per frame - one for shadow generation, one for water reflections, etc.) PROFILE3("UnitRenderer::RenderSubmit"); for (size_t i = 0; i < m_Units.size(); ++i) { SUnit& unit = m_Units[i]; unit.culled = true; if (!unit.actor) continue; if (unit.visibilityDirty) UpdateVisibility(unit); if (unit.visibility == LosVisibility::HIDDEN) continue; if (!g_AtlasGameLoop->running && !g_RenderingOptions.GetRenderActors() && (unit.flags & ACTOR_ONLY)) continue; if (!g_AtlasGameLoop->running && (unit.flags & VISIBLE_IN_ATLAS_ONLY)) continue; if (culling && !frustum.IsSphereVisible(unit.sweptBounds.GetCenter(), unit.sweptBounds.GetRadius())) continue; unit.culled = false; CModelAbstract& unitModel = unit.actor->GetModel(); if (unit.lastTransformFrame != m_FrameNumber) { CmpPtr cmpPosition(unit.entity); if (!cmpPosition) continue; CMatrix3D transform(cmpPosition->GetInterpolatedTransform(m_FrameOffset)); unitModel.SetTransform(transform); unit.lastTransformFrame = m_FrameNumber; } if (culling && !frustum.IsBoxVisible(unitModel.GetWorldBoundsRec())) continue; collector.SubmitRecursive(&unitModel); } for (size_t i = 0; i < m_DebugSpheres.size(); ++i) collector.Submit(&m_DebugSpheres[i]); } void CCmpUnitRenderer::UpdateVisibility(SUnit& unit) const { if (unit.inWorld) { // The 'always visible' flag means we should always render the unit // (regardless of whether the LOS system thinks it's visible) CmpPtr cmpVisibility(unit.entity); if (cmpVisibility && cmpVisibility->GetAlwaysVisible()) unit.visibility = LosVisibility::VISIBLE; else { CmpPtr cmpRangeManager(GetSystemEntity()); unit.visibility = cmpRangeManager->GetLosVisibility(unit.entity, GetSimContext().GetCurrentDisplayedPlayer()); } } else unit.visibility = LosVisibility::HIDDEN; // Change the visibility of the visual actor's selectable if it has one. CmpPtr cmpSelectable(unit.entity); if (cmpSelectable) cmpSelectable->SetVisibility(unit.visibility != LosVisibility::HIDDEN); unit.visibilityDirty = false; } REGISTER_COMPONENT_TYPE(UnitRenderer)