Index: ps/trunk/source/graphics/ParticleEmitter.cpp =================================================================== --- ps/trunk/source/graphics/ParticleEmitter.cpp (revision 27206) +++ ps/trunk/source/graphics/ParticleEmitter.cpp (revision 27207) @@ -1,318 +1,319 @@ /* 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 "ParticleEmitter.h" #include "graphics/LightEnv.h" #include "graphics/LOSTexture.h" #include "graphics/ParticleEmitterType.h" #include "graphics/ParticleManager.h" #include "graphics/ShaderProgram.h" #include "graphics/TextureManager.h" #include "ps/CStrInternStatic.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" CParticleEmitter::CParticleEmitter(const CParticleEmitterTypePtr& type) : m_Type(type), m_Active(true), m_NextParticleIdx(0), m_EmissionRoundingError(0.f), m_LastUpdateTime(type->m_Manager.GetCurrentTime()), m_IndexArray(false), m_VertexArray(Renderer::Backend::IBuffer::Type::VERTEX, true), m_LastFrameNumber(-1) { // If we should start with particles fully emitted, pretend that we // were created in the past so the first update will produce lots of // particles. // TODO: instead of this, maybe it would make more sense to do a full // lifetime-length update of all emitters when the game first starts // (so that e.g. buildings constructed later on won't have fully-started // emitters, but those at the start will)? if (m_Type->m_StartFull) m_LastUpdateTime -= m_Type->m_MaxLifetime; m_Particles.reserve(m_Type->m_MaxParticles); m_AttributePos.format = Renderer::Backend::Format::R32G32B32_SFLOAT; m_VertexArray.AddAttribute(&m_AttributePos); m_AttributeAxis.format = Renderer::Backend::Format::R32G32_SFLOAT; m_VertexArray.AddAttribute(&m_AttributeAxis); m_AttributeUV.format = Renderer::Backend::Format::R32G32_SFLOAT; m_VertexArray.AddAttribute(&m_AttributeUV); m_AttributeColor.format = Renderer::Backend::Format::R8G8B8A8_UNORM; m_VertexArray.AddAttribute(&m_AttributeColor); m_VertexArray.SetNumberOfVertices(m_Type->m_MaxParticles * 4); m_VertexArray.Layout(); m_IndexArray.SetNumberOfVertices(m_Type->m_MaxParticles * 6); m_IndexArray.Layout(); VertexArrayIterator index = m_IndexArray.GetIterator(); for (u16 i = 0; i < m_Type->m_MaxParticles; ++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; } m_IndexArray.Upload(); m_IndexArray.FreeBackingStore(); } void CParticleEmitter::UpdateArrayData(int frameNumber) { if (m_LastFrameNumber == frameNumber) return; m_LastFrameNumber = frameNumber; // Update m_Particles m_Type->UpdateEmitter(*this, m_Type->m_Manager.GetCurrentTime() - m_LastUpdateTime); m_LastUpdateTime = m_Type->m_Manager.GetCurrentTime(); // Regenerate the vertex array data: VertexArrayIterator attrPos = m_AttributePos.GetIterator(); VertexArrayIterator attrAxis = m_AttributeAxis.GetIterator(); VertexArrayIterator attrUV = m_AttributeUV.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); ENSURE(m_Particles.size() <= m_Type->m_MaxParticles); CBoundingBoxAligned bounds; for (size_t i = 0; i < m_Particles.size(); ++i) { // TODO: for more efficient rendering, maybe we should replace this with // a degenerate quad if alpha is 0 bounds += m_Particles[i].pos; *attrPos++ = m_Particles[i].pos; *attrPos++ = m_Particles[i].pos; *attrPos++ = m_Particles[i].pos; *attrPos++ = m_Particles[i].pos; // Compute corner offsets, split into sin/cos components so the vertex // shader can multiply by the camera-right (or left?) and camera-up vectors // to get rotating billboards: float s = sin(m_Particles[i].angle) * m_Particles[i].size/2.f; float c = cos(m_Particles[i].angle) * m_Particles[i].size/2.f; (*attrAxis)[0] = c; (*attrAxis)[1] = s; ++attrAxis; (*attrAxis)[0] = s; (*attrAxis)[1] = -c; ++attrAxis; (*attrAxis)[0] = -c; (*attrAxis)[1] = -s; ++attrAxis; (*attrAxis)[0] = -s; (*attrAxis)[1] = c; ++attrAxis; (*attrUV)[0] = 1; (*attrUV)[1] = 0; ++attrUV; (*attrUV)[0] = 0; (*attrUV)[1] = 0; ++attrUV; (*attrUV)[0] = 0; (*attrUV)[1] = 1; ++attrUV; (*attrUV)[0] = 1; (*attrUV)[1] = 1; ++attrUV; SColor4ub color = m_Particles[i].color; // Special case: If the blending depends on the source color, not the source alpha, // then pre-multiply by the alpha. (This is kind of a hack.) if (m_Type->m_BlendMode == CParticleEmitterType::BlendMode::OVERLAY || m_Type->m_BlendMode == CParticleEmitterType::BlendMode::MULTIPLY) { color.R = (color.R * color.A) / 255; color.G = (color.G * color.A) / 255; color.B = (color.B * color.A) / 255; } *attrColor++ = color; *attrColor++ = color; *attrColor++ = color; *attrColor++ = color; } m_ParticleBounds = bounds; m_VertexArray.Upload(); } void CParticleEmitter::PrepareForRendering() { m_VertexArray.PrepareForRendering(); } +void CParticleEmitter::UploadData( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext) +{ + m_VertexArray.UploadIfNeeded(deviceCommandContext); +} + void CParticleEmitter::Bind( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IShaderProgram* shader) { m_Type->m_Texture->UploadBackendTextureIfNeeded(deviceCommandContext); CLOSTexture& los = g_Renderer.GetSceneRenderer().GetScene().GetLOSTexture(); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_losTex), los.GetTextureSmooth()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_losTransform), los.GetTextureMatrix()[0], los.GetTextureMatrix()[12]); const CLightEnv& lightEnv = g_Renderer.GetSceneRenderer().GetLightEnv(); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_sunColor), lightEnv.m_SunColor.AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_fogColor), lightEnv.m_FogColor.AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_fogParams), lightEnv.m_FogFactor, lightEnv.m_FogMax); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_baseTex), m_Type->m_Texture->GetBackendTexture()); } void CParticleEmitter::RenderArray( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { if (m_Particles.empty()) return; - m_VertexArray.UploadIfNeeded(deviceCommandContext); - m_IndexArray.UploadIfNeeded(deviceCommandContext); - const uint32_t stride = m_VertexArray.GetStride(); const uint32_t firstVertexOffset = m_VertexArray.GetOffset() * stride; deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::POSITION, m_AttributePos.format, m_AttributePos.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::COLOR, m_AttributeColor.format, m_AttributeColor.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV0, m_AttributeUV.format, m_AttributeUV.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexAttributeFormat( Renderer::Backend::VertexAttributeStream::UV1, m_AttributeAxis.format, m_AttributeAxis.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0); deviceCommandContext->SetVertexBuffer( 0, m_VertexArray.GetBuffer(), firstVertexOffset); deviceCommandContext->SetIndexBuffer(m_IndexArray.GetBuffer()); deviceCommandContext->DrawIndexed(m_IndexArray.GetOffset(), m_Particles.size() * 6, 0); g_Renderer.GetStats().m_DrawCalls++; g_Renderer.GetStats().m_Particles += m_Particles.size(); } void CParticleEmitter::Unattach(const CParticleEmitterPtr& self) { m_Active = false; m_Type->m_Manager.AddUnattachedEmitter(self); } void CParticleEmitter::AddParticle(const SParticle& particle) { if (m_NextParticleIdx >= m_Particles.size()) m_Particles.push_back(particle); else m_Particles[m_NextParticleIdx] = particle; m_NextParticleIdx = (m_NextParticleIdx + 1) % m_Type->m_MaxParticles; } void CParticleEmitter::SetEntityVariable(const std::string& name, float value) { m_EntityVariables[name] = value; } - - CModelParticleEmitter::CModelParticleEmitter(const CParticleEmitterTypePtr& type) : m_Type(type) { m_Emitter = CParticleEmitterPtr(new CParticleEmitter(m_Type)); } CModelParticleEmitter::~CModelParticleEmitter() { m_Emitter->Unattach(m_Emitter); } void CModelParticleEmitter::SetEntityVariable(const std::string& name, float value) { m_Emitter->SetEntityVariable(name, value); } CModelAbstract* CModelParticleEmitter::Clone() const { return new CModelParticleEmitter(m_Type); } void CModelParticleEmitter::CalcBounds() { // TODO: we ought to compute sensible bounds here, probably based on the // current computed particle positions plus the emitter type's largest // potential bounding box at the current position m_WorldBounds = m_Type->CalculateBounds(m_Emitter->GetPosition(), m_Emitter->GetParticleBounds()); } void CModelParticleEmitter::ValidatePosition() { // TODO: do we need to do anything here? // This is a convenient (though possibly not particularly appropriate) place // to invalidate bounds so they'll be recomputed from the recent particle data InvalidateBounds(); } void CModelParticleEmitter::InvalidatePosition() { } void CModelParticleEmitter::SetTransform(const CMatrix3D& transform) { if (m_Transform == transform) return; m_Emitter->SetPosition(transform.GetTranslation()); m_Emitter->SetRotation(transform.GetRotation()); // call base class to set transform on this object CRenderableObject::SetTransform(transform); } Index: ps/trunk/source/graphics/ParticleEmitter.h =================================================================== --- ps/trunk/source/graphics/ParticleEmitter.h (revision 27206) +++ ps/trunk/source/graphics/ParticleEmitter.h (revision 27207) @@ -1,209 +1,215 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_PARTICLEEMITTER #define INCLUDED_PARTICLEEMITTER #include "graphics/ModelAbstract.h" #include "graphics/ParticleEmitterType.h" #include "maths/Quaternion.h" #include "renderer/backend/IDeviceCommandContext.h" #include "renderer/backend/IShaderProgram.h" #include "renderer/VertexArray.h" #include /** * Simulation state for a single particle. */ struct SParticle { CVector3D pos; CVector3D velocity; float angle; float angleSpeed; float size; float sizeGrowthRate; SColor4ub color; float age; float maxAge; }; typedef std::shared_ptr CParticleEmitterPtr; /** * Particle emitter. * * Emitters store particle data in two forms: * * m_Particles contains the raw data used for the CPU particle simulation. * * m_VertexArray contains the data required for rendering. * Particles are rendered as billboard quads, so the vertex array contains four vertices * per particle with different UV coordinates. The billboard position computation is * performed by a vertex shader. * * The number of particles is a constant for the entire life of the emitter, * to simplify the updating and rendering. * m_Particles acts like a ring buffer, so we don't have to worry about dynamically * allocating particles. If particles have variable lifetimes, they'll exist in the * array with alpha=0 until they're overwritten by a new particle after the maximum * lifetime. * * (It's quite likely this could be made more efficient, if the overhead of any added * complexity is not high.) */ class CParticleEmitter { public: CParticleEmitter(const CParticleEmitterTypePtr& type); /** * Set the position to be used for emission of new particles. */ void SetPosition(const CVector3D& pos) { m_Pos = pos; } CVector3D GetPosition() const { return m_Pos; } /** * Set the rotation to be used for emission of new particles (note: depends on particles). */ void SetRotation(const CQuaternion& rot) { m_Rot = rot; } const CQuaternion& GetRotation() const { return m_Rot; } /** * Get the bounding box of the center points of particles at their current positions. */ const CBoundingBoxAligned& GetParticleBounds() const { return m_ParticleBounds; } /** * Push a new particle onto the ring buffer. (May overwrite an old particle.) */ void AddParticle(const SParticle& particle); /** * Update particle and vertex array data. Must be called before RenderArray. * * If frameNumber is the same as the previous call to UpdateArrayData, * then the function will do no work and return immediately. */ void UpdateArrayData(int frameNumber); /** * Make the vertex data available for subsequent binding and rendering. */ void PrepareForRendering(); /** + * Upload the vertex data to the backend. + */ + void UploadData( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext); + + /** * Bind rendering state (textures and blend modes). */ void Bind( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IShaderProgram* shader); /** * Draw the vertex array. */ void RenderArray( Renderer::Backend::IDeviceCommandContext* deviceCommandContext); /** * Stop this emitter emitting new particles, and pass responsibility for rendering * to the CParticleManager. This should be called before dropping the last std::shared_ptr * to this object so that it will carry on rendering (until all particles have dissipated) * even when it's no longer attached to a model. * @param self the std::shared_ptr you're about to drop */ void Unattach(const CParticleEmitterPtr& self); void SetEntityVariable(const std::string& name, float value); CParticleEmitterTypePtr m_Type; /// Whether this emitter is still emitting new particles bool m_Active; CVector3D m_Pos; CQuaternion m_Rot; std::map m_EntityVariables; std::vector m_Particles; size_t m_NextParticleIdx; float m_LastUpdateTime; float m_EmissionRoundingError; private: /// Bounding box of the current particle center points CBoundingBoxAligned m_ParticleBounds; VertexIndexArray m_IndexArray; VertexArray m_VertexArray; VertexArray::Attribute m_AttributePos; VertexArray::Attribute m_AttributeAxis; VertexArray::Attribute m_AttributeUV; VertexArray::Attribute m_AttributeColor; int m_LastFrameNumber; }; /** * Particle emitter model, for attaching emitters as props on other models. */ class CModelParticleEmitter : public CModelAbstract { public: CModelParticleEmitter(const CParticleEmitterTypePtr& type); ~CModelParticleEmitter(); /// Dynamic cast virtual CModelParticleEmitter* ToCModelParticleEmitter() { return this; } virtual CModelAbstract* Clone() const; virtual void SetTerrainDirty(ssize_t UNUSED(i0), ssize_t UNUSED(j0), ssize_t UNUSED(i1), ssize_t UNUSED(j1)) { } virtual void SetEntityVariable(const std::string& name, float value); virtual void CalcBounds(); virtual void ValidatePosition(); virtual void InvalidatePosition(); virtual void SetTransform(const CMatrix3D& transform); CParticleEmitterTypePtr m_Type; CParticleEmitterPtr m_Emitter; }; #endif // INCLUDED_PARTICLEEMITTER Index: ps/trunk/source/renderer/ParticleRenderer.cpp =================================================================== --- ps/trunk/source/renderer/ParticleRenderer.cpp (revision 27206) +++ ps/trunk/source/renderer/ParticleRenderer.cpp (revision 27207) @@ -1,186 +1,193 @@ /* 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 "ParticleRenderer.h" #include "graphics/ParticleEmitter.h" #include "graphics/ShaderDefines.h" #include "graphics/ShaderManager.h" #include "graphics/TextureManager.h" #include "ps/CStrInternStatic.h" #include "ps/Profile.h" #include "renderer/DebugRenderer.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" struct ParticleRendererInternals { int frameNumber; CShaderTechniquePtr techAdd; CShaderTechniquePtr techSubtract; CShaderTechniquePtr techOverlay; CShaderTechniquePtr techMultiply; CShaderTechniquePtr techWireframe; std::vector emitters[CSceneRenderer::CULL_MAX]; }; ParticleRenderer::ParticleRenderer() { m = new ParticleRendererInternals(); m->frameNumber = 0; } ParticleRenderer::~ParticleRenderer() { delete m; } void ParticleRenderer::Submit(int cullGroup, CParticleEmitter* emitter) { m->emitters[cullGroup].push_back(emitter); } void ParticleRenderer::EndFrame() { - for (int cullGroup = 0; cullGroup < CSceneRenderer::CULL_MAX; ++cullGroup) - m->emitters[cullGroup].clear(); + for (std::vector& cullGroupEmitters : m->emitters) + cullGroupEmitters.clear(); // this should leave the capacity unchanged, which is okay since it // won't be very large or very variable } struct SortEmitterDistance { SortEmitterDistance(const CMatrix3D& m) : worldToCam(m) { } // TODO: if this is slow, we should pre-compute the distance for each emitter bool operator()(CParticleEmitter* const& a, CParticleEmitter* const& b) { CVector3D posa = a->GetPosition(); CVector3D posb = b->GetPosition(); if (posa == posb) return false; float dista = worldToCam.Transform(posa).LengthSquared(); float distb = worldToCam.Transform(posb).LengthSquared(); return distb < dista; } CMatrix3D worldToCam; }; void ParticleRenderer::PrepareForRendering(const CShaderDefines& context) { PROFILE3("prepare particles"); // Can't load the shader in the constructor because it's called before the // renderer initialisation is complete, so load it the first time through here if (!m->techWireframe) { m->techAdd = g_Renderer.GetShaderManager().LoadEffect(str_particle_add, context); m->techSubtract = g_Renderer.GetShaderManager().LoadEffect(str_particle_subtract, context); m->techOverlay = g_Renderer.GetShaderManager().LoadEffect(str_particle_overlay, context); m->techMultiply = g_Renderer.GetShaderManager().LoadEffect(str_particle_multiply, context); CShaderDefines contextWithWireframe = context; contextWithWireframe.Add(str_MODE_WIREFRAME, str_1); m->techWireframe = g_Renderer.GetShaderManager().LoadEffect(str_particle_solid, contextWithWireframe); } ++m->frameNumber; - for (int cullGroup = 0; cullGroup < CSceneRenderer::CULL_MAX; ++cullGroup) + for (std::vector& cullGroupEmitters : m->emitters) { PROFILE("update emitters"); - for (size_t i = 0; i < m->emitters[cullGroup].size(); ++i) + for (CParticleEmitter* emitter : cullGroupEmitters) { - CParticleEmitter* emitter = m->emitters[cullGroup][i]; emitter->UpdateArrayData(m->frameNumber); emitter->PrepareForRendering(); } } - for (int cullGroup = 0; cullGroup < CSceneRenderer::CULL_MAX; ++cullGroup) + CMatrix3D worldToCamera; + g_Renderer.GetSceneRenderer().GetViewCamera().GetOrientation().GetInverse(worldToCamera); + for (std::vector& cullGroupEmitters : m->emitters) { // Sort back-to-front by distance from camera PROFILE("sort emitters"); - CMatrix3D worldToCam; - g_Renderer.GetSceneRenderer().GetViewCamera().GetOrientation().GetInverse(worldToCam); - std::stable_sort(m->emitters[cullGroup].begin(), m->emitters[cullGroup].end(), SortEmitterDistance(worldToCam)); + std::stable_sort(cullGroupEmitters.begin(), cullGroupEmitters.end(), SortEmitterDistance(worldToCamera)); } // TODO: should batch by texture here when possible, maybe } +void ParticleRenderer::Upload( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext) +{ + for (std::vector& cullGroupEmitters : m->emitters) + for (CParticleEmitter* emitter : cullGroupEmitters) + emitter->UploadData(deviceCommandContext); +} + void ParticleRenderer::RenderParticles( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, int cullGroup, bool wireframe) { CShaderTechnique* lastTech = nullptr; for (CParticleEmitter* emitter : m->emitters[cullGroup]) { CShaderTechnique* currentTech = nullptr; if (wireframe) { currentTech = m->techWireframe.get(); } else { switch (emitter->m_Type->m_BlendMode) { case CParticleEmitterType::BlendMode::ADD: currentTech = m->techAdd.get(); break; case CParticleEmitterType::BlendMode::SUBTRACT: currentTech = m->techSubtract.get(); break; case CParticleEmitterType::BlendMode::OVERLAY: currentTech = m->techOverlay.get(); break; case CParticleEmitterType::BlendMode::MULTIPLY: currentTech = m->techMultiply.get(); break; } } ENSURE(currentTech); if (lastTech != currentTech) { if (lastTech) deviceCommandContext->EndPass(); lastTech = currentTech; deviceCommandContext->SetGraphicsPipelineState(lastTech->GetGraphicsPipelineStateDesc()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = lastTech->GetShader(); const CMatrix3D transform = g_Renderer.GetSceneRenderer().GetViewCamera().GetViewProjection(); const CMatrix3D modelViewMatrix = g_Renderer.GetSceneRenderer().GetViewCamera().GetOrientation().GetInverse(); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_transform), transform.AsFloatArray()); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_modelViewMatrix), modelViewMatrix.AsFloatArray()); } emitter->Bind(deviceCommandContext, lastTech->GetShader()); emitter->RenderArray(deviceCommandContext); } if (lastTech) deviceCommandContext->EndPass(); } void ParticleRenderer::RenderBounds(int cullGroup) { for (const CParticleEmitter* emitter : m->emitters[cullGroup]) { const CBoundingBoxAligned bounds = emitter->m_Type->CalculateBounds(emitter->GetPosition(), emitter->GetParticleBounds()); g_Renderer.GetDebugRenderer().DrawBoundingBox(bounds, CColor(0.0f, 1.0f, 0.0f, 1.0f), true); } } Index: ps/trunk/source/renderer/ParticleRenderer.h =================================================================== --- ps/trunk/source/renderer/ParticleRenderer.h (revision 27206) +++ ps/trunk/source/renderer/ParticleRenderer.h (revision 27207) @@ -1,71 +1,78 @@ /* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_PARTICLERENDERER #define INCLUDED_PARTICLERENDERER #include "renderer/backend/IDeviceCommandContext.h" class CParticleEmitter; class CShaderDefines; struct ParticleRendererInternals; /** * Render particles. */ class ParticleRenderer { NONCOPYABLE(ParticleRenderer); public: ParticleRenderer(); ~ParticleRenderer(); /** * Add an emitter for rendering in this frame. */ void Submit(int cullGroup, CParticleEmitter* emitter); /** * Prepare internal data structures for rendering. * Must be called after all Submit calls for a frame, and before * any rendering calls. */ void PrepareForRendering(const CShaderDefines& context); /** + * Upload internal data to the backend. Must be called after the data is + * prepared and before any rendering calls. + */ + void Upload( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext); + + /** * Reset the list of submitted overlays. */ void EndFrame(); /** * Render all the submitted particles. */ void RenderParticles( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, int cullGroup, bool wireframe = false); /** * Render bounding boxes for all the submitted emitters. */ void RenderBounds(int cullGroup); private: ParticleRendererInternals* m; }; #endif // INCLUDED_PARTICLERENDERER Index: ps/trunk/source/renderer/SceneRenderer.cpp =================================================================== --- ps/trunk/source/renderer/SceneRenderer.cpp (revision 27206) +++ ps/trunk/source/renderer/SceneRenderer.cpp (revision 27207) @@ -1,1216 +1,1218 @@ /* 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 "SceneRenderer.h" #include "graphics/Camera.h" #include "graphics/Decal.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/LOSTexture.h" #include "graphics/MaterialManager.h" #include "graphics/MiniMapTexture.h" #include "graphics/Model.h" #include "graphics/ModelDef.h" #include "graphics/ParticleManager.h" #include "graphics/Patch.h" #include "graphics/ShaderManager.h" #include "graphics/TerritoryTexture.h" #include "graphics/Terrain.h" #include "graphics/Texture.h" #include "graphics/TextureManager.h" #include "maths/Matrix3D.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/Profile.h" #include "ps/VideoMode.h" #include "ps/World.h" #include "renderer/backend/IDevice.h" #include "renderer/DebugRenderer.h" #include "renderer/HWLightingModelRenderer.h" #include "renderer/InstancingModelRenderer.h" #include "renderer/ModelRenderer.h" #include "renderer/OverlayRenderer.h" #include "renderer/ParticleRenderer.h" #include "renderer/PostprocManager.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/RenderModifiers.h" #include "renderer/ShadowMap.h" #include "renderer/SilhouetteRenderer.h" #include "renderer/SkyManager.h" #include "renderer/TerrainOverlay.h" #include "renderer/TerrainRenderer.h" #include "renderer/WaterManager.h" #include struct SScreenRect { int x1, y1, x2, y2; }; /** * Struct CSceneRendererInternals: Truly hide data that is supposed to be hidden * in this structure so it won't even appear in header files. */ class CSceneRenderer::Internals { NONCOPYABLE(Internals); public: Internals() = default; ~Internals() = default; /// Water manager WaterManager waterManager; /// Sky manager SkyManager skyManager; /// Terrain renderer TerrainRenderer terrainRenderer; /// Overlay renderer OverlayRenderer overlayRenderer; /// Particle manager CParticleManager particleManager; /// Particle renderer ParticleRenderer particleRenderer; /// Material manager CMaterialManager materialManager; /// Shadow map ShadowMap shadow; SilhouetteRenderer silhouetteRenderer; /// Various model renderers struct Models { // NOTE: The current renderer design (with ModelRenderer, ModelVertexRenderer, // RenderModifier, etc) is mostly a relic of an older design that implemented // the different materials and rendering modes through extensive subclassing // and hooking objects together in various combinations. // The new design uses the CShaderManager API to abstract away the details // of rendering, and uses a data-driven approach to materials, so there are // now a small number of generic subclasses instead of many specialised subclasses, // but most of the old infrastructure hasn't been refactored out yet and leads to // some unwanted complexity. // Submitted models are split on two axes: // - Normal vs Transp[arent] - alpha-blended models are stored in a separate // list so we can draw them above/below the alpha-blended water plane correctly // - Skinned vs Unskinned - with hardware lighting we don't need to // duplicate mesh data per model instance (except for skinned models), // so non-skinned models get different ModelVertexRenderers ModelRendererPtr NormalSkinned; ModelRendererPtr NormalUnskinned; // == NormalSkinned if unskinned shader instancing not supported ModelRendererPtr TranspSkinned; ModelRendererPtr TranspUnskinned; // == TranspSkinned if unskinned shader instancing not supported ModelVertexRendererPtr VertexRendererShader; ModelVertexRendererPtr VertexInstancingShader; ModelVertexRendererPtr VertexGPUSkinningShader; LitRenderModifierPtr ModShader; } Model; CShaderDefines globalContext; /** * Renders all non-alpha-blended models with the given context. */ void CallModelRenderers( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup, int flags) { CShaderDefines contextSkinned = context; if (g_RenderingOptions.GetGPUSkinning()) { contextSkinned.Add(str_USE_INSTANCING, str_1); contextSkinned.Add(str_USE_GPU_SKINNING, str_1); } Model.NormalSkinned->Render(deviceCommandContext, Model.ModShader, contextSkinned, cullGroup, flags); if (Model.NormalUnskinned != Model.NormalSkinned) { CShaderDefines contextUnskinned = context; contextUnskinned.Add(str_USE_INSTANCING, str_1); Model.NormalUnskinned->Render(deviceCommandContext, Model.ModShader, contextUnskinned, cullGroup, flags); } } /** * Renders all alpha-blended models with the given context. */ void CallTranspModelRenderers( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup, int flags) { CShaderDefines contextSkinned = context; if (g_RenderingOptions.GetGPUSkinning()) { contextSkinned.Add(str_USE_INSTANCING, str_1); contextSkinned.Add(str_USE_GPU_SKINNING, str_1); } Model.TranspSkinned->Render(deviceCommandContext, Model.ModShader, contextSkinned, cullGroup, flags); if (Model.TranspUnskinned != Model.TranspSkinned) { CShaderDefines contextUnskinned = context; contextUnskinned.Add(str_USE_INSTANCING, str_1); Model.TranspUnskinned->Render(deviceCommandContext, Model.ModShader, contextUnskinned, cullGroup, flags); } } }; CSceneRenderer::CSceneRenderer() { m = std::make_unique(); m_TerrainRenderMode = SOLID; m_WaterRenderMode = SOLID; m_ModelRenderMode = SOLID; m_OverlayRenderMode = SOLID; m_DisplayTerrainPriorities = false; m_LightEnv = nullptr; m_CurrentScene = nullptr; } CSceneRenderer::~CSceneRenderer() { // We no longer UnloadWaterTextures here - // that is the responsibility of the module that asked for // them to be loaded (i.e. CGameView). m.reset(); } void CSceneRenderer::ReloadShaders() { m->globalContext = CShaderDefines(); Renderer::Backend::IDevice* device = g_VideoMode.GetBackendDevice(); if (g_RenderingOptions.GetShadows()) { m->globalContext.Add(str_USE_SHADOW, str_1); if (device->GetBackend() == Renderer::Backend::Backend::GL_ARB && device->GetCapabilities().ARBShadersShadow) { m->globalContext.Add(str_USE_FP_SHADOW, str_1); } if (g_RenderingOptions.GetShadowPCF()) m->globalContext.Add(str_USE_SHADOW_PCF, str_1); const int cascadeCount = m->shadow.GetCascadeCount(); ENSURE(1 <= cascadeCount && cascadeCount <= 4); const CStrIntern cascadeCountStr[5] = {str_0, str_1, str_2, str_3, str_4}; m->globalContext.Add(str_SHADOWS_CASCADE_COUNT, cascadeCountStr[cascadeCount]); #if !CONFIG2_GLES m->globalContext.Add(str_USE_SHADOW_SAMPLER, str_1); #endif } m->globalContext.Add(str_RENDER_DEBUG_MODE, RenderDebugModeEnum::ToString(g_RenderingOptions.GetRenderDebugMode())); if (device->GetBackend() != Renderer::Backend::Backend::GL_ARB && g_RenderingOptions.GetFog()) m->globalContext.Add(str_USE_FOG, str_1); m->Model.ModShader = LitRenderModifierPtr(new ShaderRenderModifier()); ENSURE(g_RenderingOptions.GetRenderPath() != RenderPath::FIXED); m->Model.VertexRendererShader = ModelVertexRendererPtr(new ShaderModelVertexRenderer()); m->Model.VertexInstancingShader = ModelVertexRendererPtr(new InstancingModelRenderer(false, device->GetBackend() != Renderer::Backend::Backend::GL_ARB)); if (g_RenderingOptions.GetGPUSkinning()) // TODO: should check caps and GLSL etc too { m->Model.VertexGPUSkinningShader = ModelVertexRendererPtr(new InstancingModelRenderer(true, device->GetBackend() != Renderer::Backend::Backend::GL_ARB)); m->Model.NormalSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexGPUSkinningShader)); m->Model.TranspSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexGPUSkinningShader)); } else { m->Model.VertexGPUSkinningShader.reset(); m->Model.NormalSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexRendererShader)); m->Model.TranspSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexRendererShader)); } m->Model.NormalUnskinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexInstancingShader)); m->Model.TranspUnskinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexInstancingShader)); } void CSceneRenderer::Initialize() { // Let component renderers perform one-time initialization after graphics capabilities and // the shader path have been determined. m->overlayRenderer.Initialize(); } // resize renderer view void CSceneRenderer::Resize(int UNUSED(width), int UNUSED(height)) { // need to recreate the shadow map object to resize the shadow texture m->shadow.RecreateTexture(); m->waterManager.RecreateOrLoadTexturesIfNeeded(); } void CSceneRenderer::BeginFrame() { // choose model renderers for this frame m->Model.ModShader->SetShadowMap(&m->shadow); m->Model.ModShader->SetLightEnv(m_LightEnv); } void CSceneRenderer::SetSimulation(CSimulation2* simulation) { // set current simulation context for terrain renderer m->terrainRenderer.SetSimulation(simulation); } void CSceneRenderer::RenderShadowMap( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context) { PROFILE3_GPU("shadow map"); GPU_SCOPED_LABEL(deviceCommandContext, "Render shadow map"); CShaderDefines shadowsContext = context; shadowsContext.Add(str_PASS_SHADOWS, str_1); CShaderDefines contextCast = shadowsContext; contextCast.Add(str_MODE_SHADOWCAST, str_1); m->shadow.BeginRender(deviceCommandContext); const int cascadeCount = m->shadow.GetCascadeCount(); ENSURE(0 <= cascadeCount && cascadeCount <= 4); for (int cascade = 0; cascade < cascadeCount; ++cascade) { m->shadow.PrepareCamera(deviceCommandContext, cascade); const int cullGroup = CULL_SHADOWS_CASCADE_0 + cascade; { PROFILE("render patches"); m->terrainRenderer.RenderPatches(deviceCommandContext, cullGroup, shadowsContext); } { PROFILE("render models"); m->CallModelRenderers(deviceCommandContext, contextCast, cullGroup, MODELFLAG_CASTSHADOWS); } { PROFILE("render transparent models"); m->CallTranspModelRenderers(deviceCommandContext, contextCast, cullGroup, MODELFLAG_CASTSHADOWS); } } m->shadow.EndRender(deviceCommandContext); g_Renderer.SetViewport(m_ViewCamera.GetViewPort()); } void CSceneRenderer::RenderPatches( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup) { PROFILE3_GPU("patches"); GPU_SCOPED_LABEL(deviceCommandContext, "Render patches"); // Switch on wireframe if we need it. CShaderDefines localContext = context; if (m_TerrainRenderMode == WIREFRAME) localContext.Add(str_MODE_WIREFRAME, str_1); // Render all the patches, including blend pass. m->terrainRenderer.RenderTerrainShader(deviceCommandContext, localContext, cullGroup, g_RenderingOptions.GetShadows() ? &m->shadow : nullptr); if (m_TerrainRenderMode == EDGED_FACES) { localContext.Add(str_MODE_WIREFRAME, str_1); // Edged faces: need to make a second pass over the data. // Render tiles edges. m->terrainRenderer.RenderPatches( deviceCommandContext, cullGroup, localContext, CColor(0.5f, 0.5f, 1.0f, 1.0f)); // Render outline of each patch. m->terrainRenderer.RenderOutlines(deviceCommandContext, cullGroup); } } void CSceneRenderer::RenderModels( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup) { PROFILE3_GPU("models"); GPU_SCOPED_LABEL(deviceCommandContext, "Render models"); int flags = 0; CShaderDefines localContext = context; if (m_ModelRenderMode == WIREFRAME) localContext.Add(str_MODE_WIREFRAME, str_1); m->CallModelRenderers(deviceCommandContext, localContext, cullGroup, flags); if (m_ModelRenderMode == EDGED_FACES) { localContext.Add(str_MODE_WIREFRAME_SOLID, str_1); m->CallModelRenderers(deviceCommandContext, localContext, cullGroup, flags); } } void CSceneRenderer::RenderTransparentModels( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup, ETransparentMode transparentMode) { PROFILE3_GPU("transparent models"); GPU_SCOPED_LABEL(deviceCommandContext, "Render transparent models"); int flags = 0; CShaderDefines contextOpaque = context; contextOpaque.Add(str_ALPHABLEND_PASS_OPAQUE, str_1); CShaderDefines contextBlend = context; contextBlend.Add(str_ALPHABLEND_PASS_BLEND, str_1); if (m_ModelRenderMode == WIREFRAME) { contextOpaque.Add(str_MODE_WIREFRAME, str_1); contextBlend.Add(str_MODE_WIREFRAME, str_1); } if (transparentMode == TRANSPARENT || transparentMode == TRANSPARENT_OPAQUE) m->CallTranspModelRenderers(deviceCommandContext, contextOpaque, cullGroup, flags); if (transparentMode == TRANSPARENT || transparentMode == TRANSPARENT_BLEND) m->CallTranspModelRenderers(deviceCommandContext, contextBlend, cullGroup, flags); if (m_ModelRenderMode == EDGED_FACES) { CShaderDefines contextWireframe = contextOpaque; contextWireframe.Add(str_MODE_WIREFRAME, str_1); m->CallTranspModelRenderers(deviceCommandContext, contextWireframe, cullGroup, flags); } } // SetObliqueFrustumClipping: change the near plane to the given clip plane (in world space) // Based on code from Game Programming Gems 5, from http://www.terathon.com/code/oblique.html // - worldPlane is a clip plane in world space (worldPlane.Dot(v) >= 0 for any vector v passing the clipping test) void CSceneRenderer::SetObliqueFrustumClipping(CCamera& camera, const CVector4D& worldPlane) const { // First, we'll convert the given clip plane to camera space, then we'll // Get the view matrix and normal matrix (top 3x3 part of view matrix) CMatrix3D normalMatrix = camera.GetOrientation().GetTranspose(); CVector4D camPlane = normalMatrix.Transform(worldPlane); CMatrix3D matrix = camera.GetProjection(); // Calculate the clip-space corner point opposite the clipping plane // as (sgn(camPlane.x), sgn(camPlane.y), 1, 1) and // transform it into camera space by multiplying it // by the inverse of the projection matrix CVector4D q; q.X = (Sign(camPlane.X) - matrix[8] / matrix[11]) / matrix[0]; q.Y = (Sign(camPlane.Y) - matrix[9] / matrix[11]) / matrix[5]; q.Z = 1.0f / matrix[11]; q.W = (1.0f - matrix[10] / matrix[11]) / matrix[14]; // Calculate the scaled plane vector CVector4D c = camPlane * (2.0f * matrix[11] / camPlane.Dot(q)); // Replace the third row of the projection matrix matrix[2] = c.X; matrix[6] = c.Y; matrix[10] = c.Z - matrix[11]; matrix[14] = c.W; // Load it back into the camera camera.SetProjection(matrix); } void CSceneRenderer::ComputeReflectionCamera(CCamera& camera, const CBoundingBoxAligned& scissor) const { WaterManager& wm = m->waterManager; CMatrix3D projection; if (m_ViewCamera.GetProjectionType() == CCamera::ProjectionType::PERSPECTIVE) { const float aspectRatio = 1.0f; // Expand fov slightly since ripples can reflect parts of the scene that // are slightly outside the normal camera view, and we want to avoid any // noticeable edge-filtering artifacts projection.SetPerspective(m_ViewCamera.GetFOV() * 1.05f, aspectRatio, m_ViewCamera.GetNearPlane(), m_ViewCamera.GetFarPlane()); } else projection = m_ViewCamera.GetProjection(); camera = m_ViewCamera; // Temporarily change the camera to one that is reflected. // Also, for texturing purposes, make it render to a view port the size of the // water texture, stretch the image according to our aspect ratio so it covers // the whole screen despite being rendered into a square, and cover slightly more // of the view so we can see wavy reflections of slightly off-screen objects. camera.m_Orientation.Scale(1, -1, 1); camera.m_Orientation.Translate(0, 2 * wm.m_WaterHeight, 0); camera.UpdateFrustum(scissor); // Clip slightly above the water to improve reflections of objects on the water // when the reflections are distorted. camera.ClipFrustum(CVector4D(0, 1, 0, -wm.m_WaterHeight + 2.0f)); SViewPort vp; vp.m_Height = wm.m_RefTextureSize; vp.m_Width = wm.m_RefTextureSize; vp.m_X = 0; vp.m_Y = 0; camera.SetViewPort(vp); camera.SetProjection(projection); CMatrix3D scaleMat; scaleMat.SetScaling(g_Renderer.GetHeight() / static_cast(std::max(1, g_Renderer.GetWidth())), 1.0f, 1.0f); camera.SetProjection(scaleMat * camera.GetProjection()); CVector4D camPlane(0, 1, 0, -wm.m_WaterHeight + 0.5f); SetObliqueFrustumClipping(camera, camPlane); } void CSceneRenderer::ComputeRefractionCamera(CCamera& camera, const CBoundingBoxAligned& scissor) const { WaterManager& wm = m->waterManager; CMatrix3D projection; if (m_ViewCamera.GetProjectionType() == CCamera::ProjectionType::PERSPECTIVE) { const float aspectRatio = 1.0f; // Expand fov slightly since ripples can reflect parts of the scene that // are slightly outside the normal camera view, and we want to avoid any // noticeable edge-filtering artifacts projection.SetPerspective(m_ViewCamera.GetFOV() * 1.05f, aspectRatio, m_ViewCamera.GetNearPlane(), m_ViewCamera.GetFarPlane()); } else projection = m_ViewCamera.GetProjection(); camera = m_ViewCamera; // Temporarily change the camera to make it render to a view port the size of the // water texture, stretch the image according to our aspect ratio so it covers // the whole screen despite being rendered into a square, and cover slightly more // of the view so we can see wavy refractions of slightly off-screen objects. camera.UpdateFrustum(scissor); camera.ClipFrustum(CVector4D(0, -1, 0, wm.m_WaterHeight + 0.5f)); // add some to avoid artifacts near steep shores. SViewPort vp; vp.m_Height = wm.m_RefTextureSize; vp.m_Width = wm.m_RefTextureSize; vp.m_X = 0; vp.m_Y = 0; camera.SetViewPort(vp); camera.SetProjection(projection); CMatrix3D scaleMat; scaleMat.SetScaling(g_Renderer.GetHeight() / static_cast(std::max(1, g_Renderer.GetWidth())), 1.0f, 1.0f); camera.SetProjection(scaleMat * camera.GetProjection()); } // RenderReflections: render the water reflections to the reflection texture void CSceneRenderer::RenderReflections( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, const CBoundingBoxAligned& scissor) { PROFILE3_GPU("water reflections"); GPU_SCOPED_LABEL(deviceCommandContext, "Render water reflections"); WaterManager& wm = m->waterManager; // Remember old camera CCamera normalCamera = m_ViewCamera; ComputeReflectionCamera(m_ViewCamera, scissor); const CBoundingBoxAligned reflectionScissor = m->terrainRenderer.ScissorWater(CULL_DEFAULT, m_ViewCamera); if (reflectionScissor.IsEmpty()) { m_ViewCamera = normalCamera; return; } g_Renderer.SetViewport(m_ViewCamera.GetViewPort()); // Save the model-view-projection matrix so the shaders can use it for projective texturing wm.m_ReflectionMatrix = m_ViewCamera.GetViewProjection(); float vpHeight = wm.m_RefTextureSize; float vpWidth = wm.m_RefTextureSize; SScreenRect screenScissor; screenScissor.x1 = static_cast(floor((reflectionScissor[0].X * 0.5f + 0.5f) * vpWidth)); screenScissor.y1 = static_cast(floor((reflectionScissor[0].Y * 0.5f + 0.5f) * vpHeight)); screenScissor.x2 = static_cast(ceil((reflectionScissor[1].X * 0.5f + 0.5f) * vpWidth)); screenScissor.y2 = static_cast(ceil((reflectionScissor[1].Y * 0.5f + 0.5f) * vpHeight)); Renderer::Backend::IDeviceCommandContext::Rect scissorRect; scissorRect.x = screenScissor.x1; scissorRect.y = screenScissor.y1; scissorRect.width = screenScissor.x2 - screenScissor.x1; scissorRect.height = screenScissor.y2 - screenScissor.y1; deviceCommandContext->SetScissors(1, &scissorRect); deviceCommandContext->SetGraphicsPipelineState( Renderer::Backend::MakeDefaultGraphicsPipelineStateDesc()); deviceCommandContext->BeginFramebufferPass(wm.m_ReflectionFramebuffer.get()); deviceCommandContext->ClearFramebuffer(); CShaderDefines reflectionsContext = context; reflectionsContext.Add(str_PASS_REFLECTIONS, str_1); // Render terrain and models RenderPatches(deviceCommandContext, reflectionsContext, CULL_REFLECTIONS); RenderModels(deviceCommandContext, reflectionsContext, CULL_REFLECTIONS); RenderTransparentModels(deviceCommandContext, reflectionsContext, CULL_REFLECTIONS, TRANSPARENT); // Particles are always oriented to face the camera in the vertex shader, // so they don't need the inverted cull face. if (g_RenderingOptions.GetParticles()) { RenderParticles(deviceCommandContext, CULL_REFLECTIONS); } deviceCommandContext->SetScissors(0, nullptr); deviceCommandContext->EndFramebufferPass(); // Reset old camera m_ViewCamera = normalCamera; g_Renderer.SetViewport(m_ViewCamera.GetViewPort()); } // RenderRefractions: render the water refractions to the refraction texture void CSceneRenderer::RenderRefractions( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, const CBoundingBoxAligned &scissor) { PROFILE3_GPU("water refractions"); GPU_SCOPED_LABEL(deviceCommandContext, "Render water refractions"); WaterManager& wm = m->waterManager; // Remember old camera CCamera normalCamera = m_ViewCamera; ComputeRefractionCamera(m_ViewCamera, scissor); const CBoundingBoxAligned refractionScissor = m->terrainRenderer.ScissorWater(CULL_DEFAULT, m_ViewCamera); if (refractionScissor.IsEmpty()) { m_ViewCamera = normalCamera; return; } CVector4D camPlane(0, -1, 0, wm.m_WaterHeight + 2.0f); SetObliqueFrustumClipping(m_ViewCamera, camPlane); g_Renderer.SetViewport(m_ViewCamera.GetViewPort()); // Save the model-view-projection matrix so the shaders can use it for projective texturing wm.m_RefractionMatrix = m_ViewCamera.GetViewProjection(); wm.m_RefractionProjInvMatrix = m_ViewCamera.GetProjection().GetInverse(); wm.m_RefractionViewInvMatrix = m_ViewCamera.GetOrientation(); float vpHeight = wm.m_RefTextureSize; float vpWidth = wm.m_RefTextureSize; SScreenRect screenScissor; screenScissor.x1 = static_cast(floor((refractionScissor[0].X * 0.5f + 0.5f) * vpWidth)); screenScissor.y1 = static_cast(floor((refractionScissor[0].Y * 0.5f + 0.5f) * vpHeight)); screenScissor.x2 = static_cast(ceil((refractionScissor[1].X * 0.5f + 0.5f) * vpWidth)); screenScissor.y2 = static_cast(ceil((refractionScissor[1].Y * 0.5f + 0.5f) * vpHeight)); Renderer::Backend::IDeviceCommandContext::Rect scissorRect; scissorRect.x = screenScissor.x1; scissorRect.y = screenScissor.y1; scissorRect.width = screenScissor.x2 - screenScissor.x1; scissorRect.height = screenScissor.y2 - screenScissor.y1; deviceCommandContext->SetScissors(1, &scissorRect); deviceCommandContext->SetGraphicsPipelineState( Renderer::Backend::MakeDefaultGraphicsPipelineStateDesc()); deviceCommandContext->BeginFramebufferPass(wm.m_RefractionFramebuffer.get()); deviceCommandContext->ClearFramebuffer(); // Render terrain and models RenderPatches(deviceCommandContext, context, CULL_REFRACTIONS); // Render debug-related terrain overlays to make it visible under water. ITerrainOverlay::RenderOverlaysBeforeWater(deviceCommandContext); RenderModels(deviceCommandContext, context, CULL_REFRACTIONS); RenderTransparentModels(deviceCommandContext, context, CULL_REFRACTIONS, TRANSPARENT_OPAQUE); deviceCommandContext->SetScissors(0, nullptr); deviceCommandContext->EndFramebufferPass(); // Reset old camera m_ViewCamera = normalCamera; g_Renderer.SetViewport(m_ViewCamera.GetViewPort()); } void CSceneRenderer::RenderSilhouettes( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context) { PROFILE3_GPU("silhouettes"); GPU_SCOPED_LABEL(deviceCommandContext, "Render silhouettes"); CShaderDefines contextOccluder = context; contextOccluder.Add(str_MODE_SILHOUETTEOCCLUDER, str_1); CShaderDefines contextDisplay = context; contextDisplay.Add(str_MODE_SILHOUETTEDISPLAY, str_1); // Render silhouettes of units hidden behind terrain or occluders. // To avoid breaking the standard rendering of alpha-blended objects, this // has to be done in a separate pass. // First we render all occluders into depth, then render all units with // inverted depth test so any behind an occluder will get drawn in a constant // color. deviceCommandContext->SetGraphicsPipelineState( Renderer::Backend::MakeDefaultGraphicsPipelineStateDesc()); deviceCommandContext->ClearFramebuffer(false, true, true); // Render occluders: { PROFILE("render patches"); m->terrainRenderer.RenderPatches(deviceCommandContext, CULL_SILHOUETTE_OCCLUDER, contextOccluder); } { PROFILE("render model occluders"); m->CallModelRenderers(deviceCommandContext, contextOccluder, CULL_SILHOUETTE_OCCLUDER, 0); } { PROFILE("render transparent occluders"); m->CallTranspModelRenderers(deviceCommandContext, contextOccluder, CULL_SILHOUETTE_OCCLUDER, 0); } // Since we can't sort, we'll use the stencil buffer to ensure we only draw // a pixel once (using the color of whatever model happens to be drawn first). { PROFILE("render model casters"); m->CallModelRenderers(deviceCommandContext, contextDisplay, CULL_SILHOUETTE_CASTER, 0); } { PROFILE("render transparent casters"); m->CallTranspModelRenderers(deviceCommandContext, contextDisplay, CULL_SILHOUETTE_CASTER, 0); } } void CSceneRenderer::RenderParticles( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, int cullGroup) { PROFILE3_GPU("particles"); GPU_SCOPED_LABEL(deviceCommandContext, "Render particles"); m->particleRenderer.RenderParticles( deviceCommandContext, cullGroup, m_ModelRenderMode == WIREFRAME); if (m_ModelRenderMode == EDGED_FACES) { m->particleRenderer.RenderParticles( deviceCommandContext, cullGroup, true); m->particleRenderer.RenderBounds(cullGroup); } } // RenderSubmissions: force rendering of any batched objects void CSceneRenderer::RenderSubmissions( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CBoundingBoxAligned& waterScissor) { PROFILE3("render submissions"); GPU_SCOPED_LABEL(deviceCommandContext, "Render submissions"); m->skyManager.LoadAndUploadSkyTexturesIfNeeded(deviceCommandContext); GetScene().GetLOSTexture().InterpolateLOS(deviceCommandContext); GetScene().GetTerritoryTexture().UpdateIfNeeded(deviceCommandContext); GetScene().GetMiniMapTexture().Render( deviceCommandContext, GetScene().GetLOSTexture(), GetScene().GetTerritoryTexture()); CShaderDefines context = m->globalContext; int cullGroup = CULL_DEFAULT; // Set the camera g_Renderer.SetViewport(m_ViewCamera.GetViewPort()); // Prepare model renderers { PROFILE3("prepare models"); m->Model.NormalSkinned->PrepareModels(); m->Model.TranspSkinned->PrepareModels(); if (m->Model.NormalUnskinned != m->Model.NormalSkinned) m->Model.NormalUnskinned->PrepareModels(); if (m->Model.TranspUnskinned != m->Model.TranspSkinned) m->Model.TranspUnskinned->PrepareModels(); } m->terrainRenderer.PrepareForRendering(); m->overlayRenderer.PrepareForRendering(); m->particleRenderer.PrepareForRendering(context); { PROFILE3("upload models"); m->Model.NormalSkinned->UploadModels(deviceCommandContext); m->Model.TranspSkinned->UploadModels(deviceCommandContext); if (m->Model.NormalUnskinned != m->Model.NormalSkinned) m->Model.NormalUnskinned->UploadModels(deviceCommandContext); if (m->Model.TranspUnskinned != m->Model.TranspSkinned) m->Model.TranspUnskinned->UploadModels(deviceCommandContext); } m->overlayRenderer.Upload(deviceCommandContext); + m->particleRenderer.Upload(deviceCommandContext); + if (g_RenderingOptions.GetShadows()) { RenderShadowMap(deviceCommandContext, context); } if (m->waterManager.m_RenderWater) { if (waterScissor.GetVolume() > 0 && m->waterManager.WillRenderFancyWater()) { m->waterManager.UpdateQuality(); PROFILE3_GPU("water scissor"); if (g_RenderingOptions.GetWaterReflection()) RenderReflections(deviceCommandContext, context, waterScissor); if (g_RenderingOptions.GetWaterRefraction()) RenderRefractions(deviceCommandContext, context, waterScissor); if (g_RenderingOptions.GetWaterFancyEffects()) m->terrainRenderer.RenderWaterFoamOccluders(deviceCommandContext, cullGroup); } } deviceCommandContext->SetGraphicsPipelineState( Renderer::Backend::MakeDefaultGraphicsPipelineStateDesc()); CPostprocManager& postprocManager = g_Renderer.GetPostprocManager(); if (postprocManager.IsEnabled()) { // We have to update the post process manager with real near/far planes // that we use for the scene rendering. postprocManager.SetDepthBufferClipPlanes( m_ViewCamera.GetNearPlane(), m_ViewCamera.GetFarPlane() ); postprocManager.Initialize(); postprocManager.CaptureRenderOutput(deviceCommandContext); } else { deviceCommandContext->BeginFramebufferPass( deviceCommandContext->GetDevice()->GetCurrentBackbuffer()); } { PROFILE3_GPU("clear buffers"); // We don't need to clear the color attachment of the framebuffer if the sky // is going to be rendered. Because it covers the whole view. deviceCommandContext->ClearFramebuffer(!m->skyManager.IsSkyVisible(), true, true); } m->skyManager.RenderSky(deviceCommandContext); // render submitted patches and models RenderPatches(deviceCommandContext, context, cullGroup); // render debug-related terrain overlays ITerrainOverlay::RenderOverlaysBeforeWater(deviceCommandContext); // render other debug-related overlays before water (so they can be seen when underwater) m->overlayRenderer.RenderOverlaysBeforeWater(deviceCommandContext); RenderModels(deviceCommandContext, context, cullGroup); // render water if (m->waterManager.m_RenderWater && g_Game && waterScissor.GetVolume() > 0) { if (m->waterManager.WillRenderFancyWater()) { // Render transparent stuff, but only the solid parts that can occlude block water. RenderTransparentModels(deviceCommandContext, context, cullGroup, TRANSPARENT_OPAQUE); m->terrainRenderer.RenderWater(deviceCommandContext, context, cullGroup, &m->shadow); // Render transparent stuff again, but only the blended parts that overlap water. RenderTransparentModels(deviceCommandContext, context, cullGroup, TRANSPARENT_BLEND); } else { m->terrainRenderer.RenderWater(deviceCommandContext, context, cullGroup, &m->shadow); // Render transparent stuff, so it can overlap models/terrain. RenderTransparentModels(deviceCommandContext, context, cullGroup, TRANSPARENT); } } else { // render transparent stuff, so it can overlap models/terrain RenderTransparentModels(deviceCommandContext, context, cullGroup, TRANSPARENT); } // render debug-related terrain overlays ITerrainOverlay::RenderOverlaysAfterWater(deviceCommandContext, cullGroup); // render some other overlays after water (so they can be displayed on top of water) m->overlayRenderer.RenderOverlaysAfterWater(deviceCommandContext); // particles are transparent so render after water if (g_RenderingOptions.GetParticles()) { RenderParticles(deviceCommandContext, cullGroup); } if (postprocManager.IsEnabled()) { deviceCommandContext->EndFramebufferPass(); if (g_Renderer.GetPostprocManager().IsMultisampleEnabled()) g_Renderer.GetPostprocManager().ResolveMultisampleFramebuffer(deviceCommandContext); postprocManager.ApplyPostproc(deviceCommandContext); postprocManager.ReleaseRenderOutput(deviceCommandContext); deviceCommandContext->BeginFramebufferPass( deviceCommandContext->GetDevice()->GetCurrentBackbuffer()); } if (g_RenderingOptions.GetSilhouettes()) { RenderSilhouettes(deviceCommandContext, context); } // render debug lines if (g_RenderingOptions.GetDisplayFrustum()) DisplayFrustum(); if (g_RenderingOptions.GetDisplayShadowsFrustum()) m->shadow.RenderDebugBounds(); m->silhouetteRenderer.RenderDebugBounds(deviceCommandContext); m->silhouetteRenderer.RenderDebugOverlays(deviceCommandContext); // render overlays that should appear on top of all other objects m->overlayRenderer.RenderForegroundOverlays(deviceCommandContext, m_ViewCamera); deviceCommandContext->EndFramebufferPass(); } void CSceneRenderer::EndFrame() { // empty lists m->terrainRenderer.EndFrame(); m->overlayRenderer.EndFrame(); m->particleRenderer.EndFrame(); m->silhouetteRenderer.EndFrame(); // Finish model renderers m->Model.NormalSkinned->EndFrame(); m->Model.TranspSkinned->EndFrame(); if (m->Model.NormalUnskinned != m->Model.NormalSkinned) m->Model.NormalUnskinned->EndFrame(); if (m->Model.TranspUnskinned != m->Model.TranspSkinned) m->Model.TranspUnskinned->EndFrame(); } void CSceneRenderer::DisplayFrustum() { g_Renderer.GetDebugRenderer().DrawCameraFrustum(m_CullCamera, CColor(1.0f, 1.0f, 1.0f, 0.25f), 2); g_Renderer.GetDebugRenderer().DrawCameraFrustum(m_CullCamera, CColor(1.0f, 1.0f, 1.0f, 1.0f), 2, true); } // Text overlay rendering void CSceneRenderer::RenderTextOverlays(CCanvas2D& canvas) { PROFILE3_GPU("text overlays"); if (m_DisplayTerrainPriorities) m->terrainRenderer.RenderPriorities(canvas, CULL_DEFAULT); } // SetSceneCamera: setup projection and transform of camera and adjust viewport to current view // The camera always represents the actual camera used to render a scene, not any virtual camera // used for shadow rendering or reflections. void CSceneRenderer::SetSceneCamera(const CCamera& viewCamera, const CCamera& cullCamera) { m_ViewCamera = viewCamera; m_CullCamera = cullCamera; if (g_RenderingOptions.GetShadows()) m->shadow.SetupFrame(m_CullCamera, m_LightEnv->GetSunDir()); } void CSceneRenderer::Submit(CPatch* patch) { if (m_CurrentCullGroup == CULL_DEFAULT) { m->shadow.AddShadowReceiverBound(patch->GetWorldBounds()); m->silhouetteRenderer.AddOccluder(patch); } if (CULL_SHADOWS_CASCADE_0 <= m_CurrentCullGroup && m_CurrentCullGroup <= CULL_SHADOWS_CASCADE_3) { const int cascade = m_CurrentCullGroup - CULL_SHADOWS_CASCADE_0; m->shadow.AddShadowCasterBound(cascade, patch->GetWorldBounds()); } m->terrainRenderer.Submit(m_CurrentCullGroup, patch); } void CSceneRenderer::Submit(SOverlayLine* overlay) { // Overlays are only needed in the default cull group for now, // so just ignore submissions to any other group if (m_CurrentCullGroup == CULL_DEFAULT) m->overlayRenderer.Submit(overlay); } void CSceneRenderer::Submit(SOverlayTexturedLine* overlay) { if (m_CurrentCullGroup == CULL_DEFAULT) m->overlayRenderer.Submit(overlay); } void CSceneRenderer::Submit(SOverlaySprite* overlay) { if (m_CurrentCullGroup == CULL_DEFAULT) m->overlayRenderer.Submit(overlay); } void CSceneRenderer::Submit(SOverlayQuad* overlay) { if (m_CurrentCullGroup == CULL_DEFAULT) m->overlayRenderer.Submit(overlay); } void CSceneRenderer::Submit(SOverlaySphere* overlay) { if (m_CurrentCullGroup == CULL_DEFAULT) m->overlayRenderer.Submit(overlay); } void CSceneRenderer::Submit(CModelDecal* decal) { // Decals can't cast shadows since they're flat on the terrain. // They can receive shadows, but the terrain under them will have // already been passed to AddShadowCasterBound, so don't bother // doing it again here. m->terrainRenderer.Submit(m_CurrentCullGroup, decal); } void CSceneRenderer::Submit(CParticleEmitter* emitter) { m->particleRenderer.Submit(m_CurrentCullGroup, emitter); } void CSceneRenderer::SubmitNonRecursive(CModel* model) { if (m_CurrentCullGroup == CULL_DEFAULT) { m->shadow.AddShadowReceiverBound(model->GetWorldBounds()); if (model->GetFlags() & MODELFLAG_SILHOUETTE_OCCLUDER) m->silhouetteRenderer.AddOccluder(model); if (model->GetFlags() & MODELFLAG_SILHOUETTE_DISPLAY) m->silhouetteRenderer.AddCaster(model); } if (CULL_SHADOWS_CASCADE_0 <= m_CurrentCullGroup && m_CurrentCullGroup <= CULL_SHADOWS_CASCADE_3) { if (!(model->GetFlags() & MODELFLAG_CASTSHADOWS)) return; const int cascade = m_CurrentCullGroup - CULL_SHADOWS_CASCADE_0; m->shadow.AddShadowCasterBound(cascade, model->GetWorldBounds()); } bool requiresSkinning = (model->GetModelDef()->GetNumBones() != 0); if (model->GetMaterial().UsesAlphaBlending()) { if (requiresSkinning) m->Model.TranspSkinned->Submit(m_CurrentCullGroup, model); else m->Model.TranspUnskinned->Submit(m_CurrentCullGroup, model); } else { if (requiresSkinning) m->Model.NormalSkinned->Submit(m_CurrentCullGroup, model); else m->Model.NormalUnskinned->Submit(m_CurrentCullGroup, model); } } // Render the given scene void CSceneRenderer::RenderScene( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Scene& scene) { m_CurrentScene = &scene; CFrustum frustum = m_CullCamera.GetFrustum(); m_CurrentCullGroup = CULL_DEFAULT; scene.EnumerateObjects(frustum, this); m->particleManager.RenderSubmit(*this, frustum); if (g_RenderingOptions.GetSilhouettes()) { m->silhouetteRenderer.ComputeSubmissions(m_ViewCamera); m_CurrentCullGroup = CULL_DEFAULT; m->silhouetteRenderer.RenderSubmitOverlays(*this); m_CurrentCullGroup = CULL_SILHOUETTE_OCCLUDER; m->silhouetteRenderer.RenderSubmitOccluders(*this); m_CurrentCullGroup = CULL_SILHOUETTE_CASTER; m->silhouetteRenderer.RenderSubmitCasters(*this); } if (g_RenderingOptions.GetShadows()) { for (int cascade = 0; cascade <= m->shadow.GetCascadeCount(); ++cascade) { m_CurrentCullGroup = CULL_SHADOWS_CASCADE_0 + cascade; const CFrustum shadowFrustum = m->shadow.GetShadowCasterCullFrustum(cascade); scene.EnumerateObjects(shadowFrustum, this); } } CBoundingBoxAligned waterScissor; if (m->waterManager.m_RenderWater) { waterScissor = m->terrainRenderer.ScissorWater(CULL_DEFAULT, m_ViewCamera); if (waterScissor.GetVolume() > 0 && m->waterManager.WillRenderFancyWater()) { if (g_RenderingOptions.GetWaterReflection()) { m_CurrentCullGroup = CULL_REFLECTIONS; CCamera reflectionCamera; ComputeReflectionCamera(reflectionCamera, waterScissor); scene.EnumerateObjects(reflectionCamera.GetFrustum(), this); } if (g_RenderingOptions.GetWaterRefraction()) { m_CurrentCullGroup = CULL_REFRACTIONS; CCamera refractionCamera; ComputeRefractionCamera(refractionCamera, waterScissor); scene.EnumerateObjects(refractionCamera.GetFrustum(), this); } // Render the waves to the Fancy effects texture m->waterManager.RenderWaves(deviceCommandContext, frustum); } } m_CurrentCullGroup = -1; RenderSubmissions(deviceCommandContext, waterScissor); m_CurrentScene = NULL; } Scene& CSceneRenderer::GetScene() { ENSURE(m_CurrentScene); return *m_CurrentScene; } void CSceneRenderer::MakeShadersDirty() { m->waterManager.m_NeedsReloading = true; } WaterManager& CSceneRenderer::GetWaterManager() { return m->waterManager; } SkyManager& CSceneRenderer::GetSkyManager() { return m->skyManager; } CParticleManager& CSceneRenderer::GetParticleManager() { return m->particleManager; } TerrainRenderer& CSceneRenderer::GetTerrainRenderer() { return m->terrainRenderer; } CMaterialManager& CSceneRenderer::GetMaterialManager() { return m->materialManager; } ShadowMap& CSceneRenderer::GetShadowMap() { return m->shadow; } void CSceneRenderer::ResetState() { // Clear all emitters, that were created in previous games GetParticleManager().ClearUnattachedEmitters(); }