// Copyright (c) 2008-2023 the Urho3D project // License: MIT #include "../Precompiled.h" #include "../Core/Profiler.h" #include "../Core/WorkQueue.h" #include "../Graphics/Camera.h" #include "../Graphics/DebugRenderer.h" #include "../Graphics/Geometry.h" #include "../Graphics/Graphics.h" #include "../Graphics/GraphicsEvents.h" #include "../GraphicsAPI/GraphicsDefs.h" #include "../Graphics/Material.h" #include "../Graphics/OcclusionBuffer.h" #include "../Graphics/Octree.h" #include "../Graphics/Renderer.h" #include "../Graphics/RenderPath.h" #include "../Graphics/Skybox.h" #include "../Graphics/Technique.h" #include "../Graphics/View.h" #include "../GraphicsAPI/GraphicsImpl.h" #include "../GraphicsAPI/ShaderVariation.h" #include "../GraphicsAPI/Texture2D.h" #include "../GraphicsAPI/Texture2DArray.h" #include "../GraphicsAPI/Texture3D.h" #include "../GraphicsAPI/TextureCube.h" #include "../GraphicsAPI/VertexBuffer.h" #include "../IO/FileSystem.h" #include "../IO/Log.h" #include "../Resource/ResourceCache.h" #include "../Scene/Scene.h" #include "../UI/UI.h" #include "../DebugNew.h" namespace Urho3D { /// %Frustum octree query for shadowcasters. class ShadowCasterOctreeQuery : public FrustumOctreeQuery { public: /// Construct with frustum and query parameters. ShadowCasterOctreeQuery(Vector& result, const Frustum& frustum, DrawableTypes drawableTypes = DrawableTypes::Any, unsigned viewMask = DEFAULT_VIEWMASK) : FrustumOctreeQuery(result, frustum, drawableTypes, viewMask) { } /// Intersection test for drawables. void TestDrawables(Drawable** start, Drawable** end, bool inside) override { while (start != end) { Drawable* drawable = *start++; if (drawable->GetCastShadows() && !!(drawable->GetDrawableType() & drawableTypes_) && (drawable->GetViewMask() & viewMask_)) { if (inside || frustum_.IsInsideFast(drawable->GetWorldBoundingBox())) result_.Push(drawable); } } } }; /// %Frustum octree query for zones and occluders. class ZoneOccluderOctreeQuery : public FrustumOctreeQuery { public: /// Construct with frustum and query parameters. ZoneOccluderOctreeQuery(Vector& result, const Frustum& frustum, DrawableTypes drawableTypes = DrawableTypes::Any, unsigned viewMask = DEFAULT_VIEWMASK) : FrustumOctreeQuery(result, frustum, drawableTypes, viewMask) { } /// Intersection test for drawables. void TestDrawables(Drawable** start, Drawable** end, bool inside) override { while (start != end) { Drawable* drawable = *start++; DrawableTypes type = drawable->GetDrawableType(); if ((type == DrawableTypes::Zone || (type == DrawableTypes::Geometry && drawable->IsOccluder())) && (drawable->GetViewMask() & viewMask_)) { if (inside || frustum_.IsInsideFast(drawable->GetWorldBoundingBox())) result_.Push(drawable); } } } }; /// %Frustum octree query with occlusion. class OccludedFrustumOctreeQuery : public FrustumOctreeQuery { public: /// Construct with frustum, occlusion buffer and query parameters. OccludedFrustumOctreeQuery(Vector& result, const Frustum& frustum, OcclusionBuffer* buffer, DrawableTypes drawableTypes = DrawableTypes::Any, unsigned viewMask = DEFAULT_VIEWMASK) : FrustumOctreeQuery(result, frustum, drawableTypes, viewMask), buffer_(buffer) { } /// Intersection test for an octant. Intersection TestOctant(const BoundingBox& box, bool inside) override { if (inside) return buffer_->IsVisible(box) ? INSIDE : OUTSIDE; else { Intersection result = frustum_.IsInside(box); if (result != OUTSIDE && !buffer_->IsVisible(box)) result = OUTSIDE; return result; } } /// Intersection test for drawables. Note: drawable occlusion is performed later in worker threads. void TestDrawables(Drawable** start, Drawable** end, bool inside) override { while (start != end) { Drawable* drawable = *start++; if (!!(drawable->GetDrawableType() & drawableTypes_) && (drawable->GetViewMask() & viewMask_)) { if (inside || frustum_.IsInsideFast(drawable->GetWorldBoundingBox())) result_.Push(drawable); } } } /// Occlusion buffer. OcclusionBuffer* buffer_; }; void CheckVisibilityWork(const WorkItem* item, i32 threadIndex) { auto* view = reinterpret_cast(item->aux_); auto** start = reinterpret_cast(item->start_); auto** end = reinterpret_cast(item->end_); OcclusionBuffer* buffer = view->occlusionBuffer_; const Matrix3x4& viewMatrix = view->cullCamera_->GetView(); Vector3 viewZ = Vector3(viewMatrix.m20_, viewMatrix.m21_, viewMatrix.m22_); Vector3 absViewZ = viewZ.Abs(); unsigned cameraViewMask = view->cullCamera_->GetViewMask(); bool cameraZoneOverride = view->cameraZoneOverride_; PerThreadSceneResult& result = view->sceneResults_[threadIndex]; while (start != end) { Drawable* drawable = *start++; if (!buffer || !drawable->IsOccludee() || buffer->IsVisible(drawable->GetWorldBoundingBox())) { drawable->UpdateBatches(view->frame_); // If draw distance non-zero, update and check it float maxDistance = drawable->GetDrawDistance(); if (maxDistance > 0.0f) { if (drawable->GetDistance() > maxDistance) continue; } drawable->MarkInView(view->frame_); // For geometries, find zone, clear lights and calculate view space Z range if (drawable->GetDrawableType() == DrawableTypes::Geometry) { Zone* drawableZone = drawable->GetZone(); if (!cameraZoneOverride && (drawable->IsZoneDirty() || !drawableZone || (drawableZone->GetViewMask() & cameraViewMask) == 0)) view->FindZone(drawable); const BoundingBox& geomBox = drawable->GetWorldBoundingBox(); Vector3 center = geomBox.Center(); Vector3 edge = geomBox.Size() * 0.5f; // Do not add "infinite" objects like skybox to prevent shadow map focusing behaving erroneously if (edge.LengthSquared() < M_LARGE_VALUE * M_LARGE_VALUE) { float viewCenterZ = viewZ.DotProduct(center) + viewMatrix.m23_; float viewEdgeZ = absViewZ.DotProduct(edge); float minZ = viewCenterZ - viewEdgeZ; float maxZ = viewCenterZ + viewEdgeZ; drawable->SetMinMaxZ(viewCenterZ - viewEdgeZ, viewCenterZ + viewEdgeZ); result.minZ_ = Min(result.minZ_, minZ); result.maxZ_ = Max(result.maxZ_, maxZ); } else drawable->SetMinMaxZ(M_LARGE_VALUE, M_LARGE_VALUE); result.geometries_.Push(drawable); } else if (drawable->GetDrawableType() == DrawableTypes::Light) { Light* light = static_cast(drawable); // Skip lights with zero brightness or black color if (!light->GetEffectiveColor().Equals(Color::BLACK)) result.lights_.Push(light); } } } } void ProcessLightWork(const WorkItem* item, i32 threadIndex) { auto* view = reinterpret_cast(item->aux_); auto* query = reinterpret_cast(item->start_); view->ProcessLight(*query, threadIndex); } void UpdateDrawableGeometriesWork(const WorkItem* item, i32 threadIndex) { const FrameInfo& frame = *(reinterpret_cast(item->aux_)); auto** start = reinterpret_cast(item->start_); auto** end = reinterpret_cast(item->end_); while (start != end) { Drawable* drawable = *start++; // We may leave null pointer holes in the queue if a drawable is found out to require a main thread update if (drawable) drawable->UpdateGeometry(frame); } } void SortBatchQueueFrontToBackWork(const WorkItem* item, i32 threadIndex) { auto* queue = reinterpret_cast(item->start_); queue->SortFrontToBack(); } void SortBatchQueueBackToFrontWork(const WorkItem* item, i32 threadIndex) { auto* queue = reinterpret_cast(item->start_); queue->SortBackToFront(); } void SortLightQueueWork(const WorkItem* item, i32 threadIndex) { auto* start = reinterpret_cast(item->start_); start->litBaseBatches_.SortFrontToBack(); start->litBatches_.SortFrontToBack(); } void SortShadowQueueWork(const WorkItem* item, i32 threadIndex) { LightBatchQueue* start = reinterpret_cast(item->start_); for (ShadowBatchQueue& shadowSplit : start->shadowSplits_) shadowSplit.shadowBatches_.SortFrontToBack(); } StringHash ParseTextureTypeXml(ResourceCache* cache, const String& filename); View::View(Context* context) : Object(context), graphics_(GetSubsystem()), renderer_(GetSubsystem()) { // Create octree query and scene results vector for each thread i32 numThreads = GetSubsystem()->GetNumThreads() + 1; // Worker threads + main thread tempDrawables_.Resize(numThreads); sceneResults_.Resize(numThreads); } bool View::Define(RenderSurface* renderTarget, Viewport* viewport) { sourceView_ = nullptr; renderPath_ = viewport->GetRenderPath(); if (!renderPath_) return false; renderTarget_ = renderTarget; drawDebug_ = viewport->GetDrawDebug(); // Validate the rect and calculate size. If zero rect, use whole rendertarget size int rtWidth = renderTarget ? renderTarget->GetWidth() : graphics_->GetWidth(); int rtHeight = renderTarget ? renderTarget->GetHeight() : graphics_->GetHeight(); const IntRect& rect = viewport->GetRect(); if (rect != IntRect::ZERO) { viewRect_.left_ = Clamp(rect.left_, 0, rtWidth - 1); viewRect_.top_ = Clamp(rect.top_, 0, rtHeight - 1); viewRect_.right_ = Clamp(rect.right_, viewRect_.left_ + 1, rtWidth); viewRect_.bottom_ = Clamp(rect.bottom_, viewRect_.top_ + 1, rtHeight); } else viewRect_ = IntRect(0, 0, rtWidth, rtHeight); viewSize_ = viewRect_.Size(); rtSize_ = IntVector2(rtWidth, rtHeight); // On OpenGL flip the viewport if rendering to a texture for consistent UV addressing with Direct3D9 if (Graphics::GetGAPI() == GAPI_OPENGL && renderTarget_) { viewRect_.bottom_ = rtHeight - viewRect_.top_; viewRect_.top_ = viewRect_.bottom_ - viewSize_.y_; } scene_ = viewport->GetScene(); cullCamera_ = viewport->GetCullCamera(); camera_ = viewport->GetCamera(); if (!cullCamera_) cullCamera_ = camera_; else { // If view specifies a culling camera (view preparation sharing), check if already prepared sourceView_ = renderer_->GetPreparedView(cullCamera_); if (sourceView_ && sourceView_->scene_ == scene_ && sourceView_->renderPath_ == renderPath_) { // Copy properties needed later in rendering deferred_ = sourceView_->deferred_; deferredAmbient_ = sourceView_->deferredAmbient_; useLitBase_ = sourceView_->useLitBase_; hasScenePasses_ = sourceView_->hasScenePasses_; noStencil_ = sourceView_->noStencil_; lightVolumeCommand_ = sourceView_->lightVolumeCommand_; forwardLightsCommand_ = sourceView_->forwardLightsCommand_; octree_ = sourceView_->octree_; return true; } else { // Mismatch in scene or renderpath, fall back to unique view preparation sourceView_ = nullptr; } } // Set default passes gBufferPassIndex_ = NINDEX; basePassIndex_ = Technique::GetPassIndex("base"); alphaPassIndex_ = Technique::GetPassIndex("alpha"); lightPassIndex_ = Technique::GetPassIndex("light"); litBasePassIndex_ = Technique::GetPassIndex("litbase"); litAlphaPassIndex_ = Technique::GetPassIndex("litalpha"); deferred_ = false; deferredAmbient_ = false; useLitBase_ = false; hasScenePasses_ = false; noStencil_ = false; lightVolumeCommand_ = nullptr; forwardLightsCommand_ = nullptr; scenePasses_.Clear(); geometriesUpdated_ = false; if (Graphics::GetGAPI() == GAPI_OPENGL) { #if defined(URHO3D_GLES2) // On OpenGL ES we assume a stencil is not available or would not give a good performance, and disable light stencil // optimizations in any case noStencil_ = true; #else for (const RenderPathCommand& command : renderPath_->commands_) { if (!command.enabled_) continue; if (command.depthStencilName_.Length()) { // Using a readable depth texture will disable light stencil optimizations on OpenGL, as for compatibility reasons // we are using a depth format without stencil channel noStencil_ = true; break; } } #endif } // Make sure that all necessary batch queues exist for (RenderPathCommand& command : renderPath_->commands_) { if (!command.enabled_) continue; if (command.type_ == CMD_SCENEPASS) { hasScenePasses_ = true; ScenePassInfo info{}; info.passIndex_ = command.passIndex_ = Technique::GetPassIndex(command.pass_); info.allowInstancing_ = command.sortMode_ != SORT_BACKTOFRONT; info.markToStencil_ = !noStencil_ && command.markToStencil_; info.vertexLights_ = command.vertexLights_; // Check scenepass metadata for defining custom passes which interact with lighting if (!command.metadata_.Empty()) { if (command.metadata_ == "gbuffer") gBufferPassIndex_ = command.passIndex_; else if (command.metadata_ == "base" && command.pass_ != "base") { basePassIndex_ = command.passIndex_; litBasePassIndex_ = Technique::GetPassIndex("lit" + command.pass_); } else if (command.metadata_ == "alpha" && command.pass_ != "alpha") { alphaPassIndex_ = command.passIndex_; litAlphaPassIndex_ = Technique::GetPassIndex("lit" + command.pass_); } } HashMap::Iterator j = batchQueues_.Find(info.passIndex_); if (j == batchQueues_.End()) j = batchQueues_.Insert(Pair(info.passIndex_, BatchQueue())); info.batchQueue_ = &j->second_; SetQueueShaderDefines(*info.batchQueue_, command); scenePasses_.Push(info); } // Allow a custom forward light pass else if (command.type_ == CMD_FORWARDLIGHTS && !command.pass_.Empty()) lightPassIndex_ = command.passIndex_ = Technique::GetPassIndex(command.pass_); } octree_ = nullptr; // Get default zone first in case we do not have zones defined cameraZone_ = farClipZone_ = renderer_->GetDefaultZone(); if (hasScenePasses_) { if (!scene_ || !cullCamera_ || !cullCamera_->IsEnabledEffective()) return false; // If scene is loading scene content asynchronously, it is incomplete and should not be rendered if (scene_->IsAsyncLoading() && scene_->GetAsyncLoadMode() > LOAD_RESOURCES_ONLY) return false; octree_ = scene_->GetComponent(); if (!octree_) return false; // Do not accept view if camera projection is illegal // (there is a possibility of crash if occlusion is used and it can not clip properly) if (!cullCamera_->IsProjectionValid()) return false; } // Go through commands to check for deferred rendering and other flags for (const RenderPathCommand& command : renderPath_->commands_) { if (!command.enabled_) continue; // Check if ambient pass and G-buffer rendering happens at the same time if (command.type_ == CMD_SCENEPASS && command.outputs_.Size() > 1) { if (CheckViewportWrite(command)) deferredAmbient_ = true; } else if (command.type_ == CMD_LIGHTVOLUMES) { lightVolumeCommand_ = &command; deferred_ = true; } else if (command.type_ == CMD_FORWARDLIGHTS) { forwardLightsCommand_ = &command; useLitBase_ = command.useLitBase_; } } drawShadows_ = renderer_->GetDrawShadows(); materialQuality_ = renderer_->GetMaterialQuality(); maxOccluderTriangles_ = renderer_->GetMaxOccluderTriangles(); minInstances_ = renderer_->GetMinInstances(); // Set possible quality overrides from the camera // Note that the culling camera is used here (its settings are authoritative) while the render camera // will be just used for the final view & projection matrices ViewOverrideFlags viewOverrideFlags = cullCamera_ ? cullCamera_->GetViewOverrideFlags() : VO_NONE; if (viewOverrideFlags & VO_LOW_MATERIAL_QUALITY) materialQuality_ = QUALITY_LOW; if (viewOverrideFlags & VO_DISABLE_SHADOWS) drawShadows_ = false; if (viewOverrideFlags & VO_DISABLE_OCCLUSION) maxOccluderTriangles_ = 0; // Occlusion buffer has constant width. If resulting height would be too large due to aspect ratio, disable occlusion if (viewSize_.y_ > viewSize_.x_ * 4) maxOccluderTriangles_ = 0; return true; } void View::Update(const FrameInfo& frame) { // No need to update if using another prepared view if (sourceView_) return; frame_.camera_ = cullCamera_; frame_.timeStep_ = frame.timeStep_; frame_.frameNumber_ = frame.frameNumber_; frame_.viewSize_ = viewSize_; using namespace BeginViewUpdate; SendViewEvent(E_BEGINVIEWUPDATE); int maxSortedInstances = renderer_->GetMaxSortedInstances(); // Clear buffers, geometry, light, occluder & batch list renderTargets_.Clear(); geometries_.Clear(); lights_.Clear(); zones_.Clear(); occluders_.Clear(); activeOccluders_ = 0; vertexLightQueues_.Clear(); for (HashMap::Iterator i = batchQueues_.Begin(); i != batchQueues_.End(); ++i) i->second_.Clear(maxSortedInstances); if (hasScenePasses_ && (!cullCamera_ || !octree_)) { SendViewEvent(E_ENDVIEWUPDATE); return; } // Set automatic aspect ratio if required if (cullCamera_ && cullCamera_->GetAutoAspectRatio()) cullCamera_->SetAspectRatioInternal((float)frame_.viewSize_.x_ / (float)frame_.viewSize_.y_); GetDrawables(); GetBatches(); renderer_->StorePreparedView(this, cullCamera_); SendViewEvent(E_ENDVIEWUPDATE); } void View::Render() { SendViewEvent(E_BEGINVIEWRENDER); if (hasScenePasses_ && (!octree_ || !camera_)) { SendViewEvent(E_ENDVIEWRENDER); return; } UpdateGeometries(); // Allocate screen buffers as necessary AllocateScreenBuffers(); SendViewEvent(E_VIEWBUFFERSREADY); // Forget parameter sources from the previous view graphics_->ClearParameterSources(); if (renderer_->GetDynamicInstancing() && graphics_->GetInstancingSupport()) PrepareInstancingBuffer(); // It is possible, though not recommended, that the same camera is used for multiple main views. Set automatic aspect ratio // to ensure correct projection will be used if (camera_ && camera_->GetAutoAspectRatio()) camera_->SetAspectRatioInternal((float)(viewSize_.x_) / (float)(viewSize_.y_)); // Bind the face selection and indirection cube maps for point light shadows #ifndef URHO3D_GLES2 if (renderer_->GetDrawShadows()) { graphics_->SetTexture(TU_FACESELECT, renderer_->GetFaceSelectCubeMap()); graphics_->SetTexture(TU_INDIRECTION, renderer_->GetIndirectionCubeMap()); } #endif if (Graphics::GetGAPI() == GAPI_OPENGL && renderTarget_) { // On OpenGL, flip the projection if rendering to a texture so that the texture can be addressed in the same way // as a render texture produced on Direct3D9 // Note that the state of the FlipVertical mode is toggled here rather than enabled // The reason for this is that we want the mode to be the opposite of what the user has currently set for the // camera when rendering to texture for OpenGL // This mode is returned to the original state by toggling it again below, after the render if (camera_) camera_->SetFlipVertical(!camera_->GetFlipVertical()); } // Render ExecuteRenderPathCommands(); // Reset state after commands graphics_->SetFillMode(FILL_SOLID); graphics_->SetLineAntiAlias(false); graphics_->SetClipPlane(false); graphics_->SetColorWrite(true); graphics_->SetDepthBias(0.0f, 0.0f); graphics_->SetScissorTest(false); graphics_->SetStencilTest(false); // Draw the associated debug geometry now if enabled if (drawDebug_ && octree_ && camera_) { auto* debug = octree_->GetComponent(); if (debug && debug->IsEnabledEffective() && debug->HasContent()) { // If used resolve from backbuffer, blit first to the backbuffer to ensure correct depth buffer on OpenGL // Otherwise use the last rendertarget and blit after debug geometry if (usedResolve_ && currentRenderTarget_ != renderTarget_) { BlitFramebuffer(currentRenderTarget_->GetParentTexture(), renderTarget_, false); currentRenderTarget_ = renderTarget_; lastCustomDepthSurface_ = nullptr; } graphics_->SetRenderTarget(0, currentRenderTarget_); for (i32 i = 1; i < MAX_RENDERTARGETS; ++i) graphics_->SetRenderTarget(i, (RenderSurface*)nullptr); // If a custom depth surface was used, use it also for debug rendering graphics_->SetDepthStencil(lastCustomDepthSurface_ ? lastCustomDepthSurface_ : GetDepthStencil(currentRenderTarget_)); IntVector2 rtSizeNow = graphics_->GetRenderTargetDimensions(); IntRect viewport = (currentRenderTarget_ == renderTarget_) ? viewRect_ : IntRect(0, 0, rtSizeNow.x_, rtSizeNow.y_); graphics_->SetViewport(viewport); debug->SetView(camera_); debug->Render(); } } if (Graphics::GetGAPI() == GAPI_OPENGL && renderTarget_) { // Restores original setting of FlipVertical when flipped by code above. if (camera_) camera_->SetFlipVertical(!camera_->GetFlipVertical()); } // Run framebuffer blitting if necessary. If scene was resolved from backbuffer, do not touch depth // (backbuffer should contain proper depth already) if (currentRenderTarget_ != renderTarget_) BlitFramebuffer(currentRenderTarget_->GetParentTexture(), renderTarget_, !usedResolve_); SendViewEvent(E_ENDVIEWRENDER); } Graphics* View::GetGraphics() const { return graphics_; } Renderer* View::GetRenderer() const { return renderer_; } View* View::GetSourceView() const { return sourceView_; } void View::SetGlobalShaderParameters() { graphics_->SetShaderParameter(VSP_DELTATIME, frame_.timeStep_); graphics_->SetShaderParameter(PSP_DELTATIME, frame_.timeStep_); if (scene_) { float elapsedTime = scene_->GetElapsedTime(); graphics_->SetShaderParameter(VSP_ELAPSEDTIME, elapsedTime); graphics_->SetShaderParameter(PSP_ELAPSEDTIME, elapsedTime); } SendViewEvent(E_VIEWGLOBALSHADERPARAMETERS); } void View::SetCameraShaderParameters(Camera* camera) { if (!camera) return; Matrix3x4 cameraEffectiveTransform = camera->GetEffectiveWorldTransform(); graphics_->SetShaderParameter(VSP_CAMERAPOS, cameraEffectiveTransform.Translation()); graphics_->SetShaderParameter(VSP_VIEWINV, cameraEffectiveTransform); graphics_->SetShaderParameter(VSP_VIEW, camera->GetView()); graphics_->SetShaderParameter(PSP_CAMERAPOS, cameraEffectiveTransform.Translation()); float nearClip = camera->GetNearClip(); float farClip = camera->GetFarClip(); graphics_->SetShaderParameter(VSP_NEARCLIP, nearClip); graphics_->SetShaderParameter(VSP_FARCLIP, farClip); graphics_->SetShaderParameter(PSP_NEARCLIP, nearClip); graphics_->SetShaderParameter(PSP_FARCLIP, farClip); Vector4 depthMode = Vector4::ZERO; if (camera->IsOrthographic()) { depthMode.x_ = 1.0f; if (Graphics::GetGAPI() == GAPI_OPENGL) { depthMode.z_ = 0.5f; depthMode.w_ = 0.5f; } else { depthMode.z_ = 1.0f; } } else { depthMode.w_ = 1.0f / camera->GetFarClip(); } graphics_->SetShaderParameter(VSP_DEPTHMODE, depthMode); Vector4 depthReconstruct (farClip / (farClip - nearClip), -nearClip / (farClip - nearClip), camera->IsOrthographic() ? 1.0f : 0.0f, camera->IsOrthographic() ? 0.0f : 1.0f); graphics_->SetShaderParameter(PSP_DEPTHRECONSTRUCT, depthReconstruct); Vector3 nearVector, farVector; camera->GetFrustumSize(nearVector, farVector); graphics_->SetShaderParameter(VSP_FRUSTUMSIZE, farVector); Matrix4 projection = camera->GetGPUProjection(); if (Graphics::GetGAPI() == GAPI_OPENGL) { // Add constant depth bias manually to the projection matrix due to glPolygonOffset() inconsistency float constantBias = 2.0f * graphics_->GetDepthConstantBias(); projection.m22_ += projection.m32_ * constantBias; projection.m23_ += projection.m33_ * constantBias; } graphics_->SetShaderParameter(VSP_VIEWPROJ, projection * camera->GetView()); // If in a scene pass and the command defines shader parameters, set them now if (passCommand_) SetCommandShaderParameters(*passCommand_); } void View::SetCommandShaderParameters(const RenderPathCommand& command) { const HashMap& parameters = command.shaderParameters_; for (HashMap::ConstIterator k = parameters.Begin(); k != parameters.End(); ++k) graphics_->SetShaderParameter(k->first_, k->second_); } void View::SetGBufferShaderParameters(const IntVector2& texSize, const IntRect& viewRect) { auto texWidth = (float)texSize.x_; auto texHeight = (float)texSize.y_; float widthRange = 0.5f * viewRect.Width() / texWidth; float heightRange = 0.5f * viewRect.Height() / texHeight; Vector4 bufferUVOffset; if (Graphics::GetGAPI() == GAPI_OPENGL) { bufferUVOffset = Vector4((float)viewRect.left_ / texWidth + widthRange, 1.0f - ((float)viewRect.top_ / texHeight + heightRange), widthRange, heightRange); } else { bufferUVOffset = Vector4((float)viewRect.left_ / texWidth + widthRange, (float)viewRect.top_ / texHeight + heightRange, widthRange, heightRange); } graphics_->SetShaderParameter(VSP_GBUFFEROFFSETS, bufferUVOffset); float invSizeX = 1.0f / texWidth; float invSizeY = 1.0f / texHeight; graphics_->SetShaderParameter(PSP_GBUFFERINVSIZE, Vector2(invSizeX, invSizeY)); } void View::GetDrawables() { if (!octree_ || !cullCamera_) return; URHO3D_PROFILE(GetDrawables); auto* queue = GetSubsystem(); Vector& tempDrawables = tempDrawables_[0]; // Get zones and occluders first { ZoneOccluderOctreeQuery query(tempDrawables, cullCamera_->GetFrustum(), DrawableTypes::Geometry | DrawableTypes::Zone, cullCamera_->GetViewMask()); octree_->GetDrawables(query); } highestZonePriority_ = M_MIN_INT; int bestPriority = M_MIN_INT; Node* cameraNode = cullCamera_->GetNode(); Vector3 cameraPos = cameraNode->GetWorldPosition(); for (Vector::ConstIterator i = tempDrawables.Begin(); i != tempDrawables.End(); ++i) { Drawable* drawable = *i; DrawableTypes type = drawable->GetDrawableType(); if (type == DrawableTypes::Zone) { Zone* zone = static_cast(drawable); zones_.Push(zone); int priority = zone->GetPriority(); if (priority > highestZonePriority_) highestZonePriority_ = priority; if (priority > bestPriority && zone->IsInside(cameraPos)) { cameraZone_ = zone; bestPriority = priority; } } else occluders_.Push(drawable); } // Determine the zone at far clip distance. If not found, or camera zone has override mode, use camera zone cameraZoneOverride_ = cameraZone_->GetOverride(); if (!cameraZoneOverride_) { Vector3 farClipPos = cameraPos + cameraNode->GetWorldDirection() * Vector3(0.0f, 0.0f, cullCamera_->GetFarClip()); bestPriority = M_MIN_INT; for (Vector::Iterator i = zones_.Begin(); i != zones_.End(); ++i) { int priority = (*i)->GetPriority(); if (priority > bestPriority && (*i)->IsInside(farClipPos)) { farClipZone_ = *i; bestPriority = priority; } } } if (farClipZone_ == renderer_->GetDefaultZone()) farClipZone_ = cameraZone_; // If occlusion in use, get & render the occluders occlusionBuffer_ = nullptr; if (maxOccluderTriangles_ > 0) { UpdateOccluders(occluders_, cullCamera_); if (occluders_.Size()) { URHO3D_PROFILE(DrawOcclusion); occlusionBuffer_ = renderer_->GetOcclusionBuffer(cullCamera_); DrawOccluders(occlusionBuffer_, occluders_); } } else occluders_.Clear(); // Get lights and geometries. Coarse occlusion for octants is used at this point if (occlusionBuffer_) { OccludedFrustumOctreeQuery query (tempDrawables, cullCamera_->GetFrustum(), occlusionBuffer_, DrawableTypes::Geometry | DrawableTypes::Light, cullCamera_->GetViewMask()); octree_->GetDrawables(query); } else { FrustumOctreeQuery query(tempDrawables, cullCamera_->GetFrustum(), DrawableTypes::Geometry | DrawableTypes::Light, cullCamera_->GetViewMask()); octree_->GetDrawables(query); } // Check drawable occlusion, find zones for moved drawables and collect geometries & lights in worker threads { for (PerThreadSceneResult& result : sceneResults_) { result.geometries_.Clear(); result.lights_.Clear(); result.minZ_ = M_INFINITY; result.maxZ_ = 0.0f; } int numWorkItems = queue->GetNumThreads() + 1; // Worker threads + main thread int drawablesPerItem = tempDrawables.Size() / numWorkItems; Vector::Iterator start = tempDrawables.Begin(); // Create a work item for each thread for (int i = 0; i < numWorkItems; ++i) { SharedPtr item = queue->GetFreeItem(); item->priority_ = WI_MAX_PRIORITY; item->workFunction_ = CheckVisibilityWork; item->aux_ = this; Vector::Iterator end = tempDrawables.End(); if (i < numWorkItems - 1 && end - start > drawablesPerItem) end = start + drawablesPerItem; item->start_ = &(*start); item->end_ = &(*end); queue->AddWorkItem(item); start = end; } queue->Complete(WI_MAX_PRIORITY); } // Combine lights, geometries & scene Z range from the threads geometries_.Clear(); lights_.Clear(); minZ_ = M_INFINITY; maxZ_ = 0.0f; if (sceneResults_.Size() > 1) { for (const PerThreadSceneResult& result : sceneResults_) { geometries_.Push(result.geometries_); lights_.Push(result.lights_); minZ_ = Min(minZ_, result.minZ_); maxZ_ = Max(maxZ_, result.maxZ_); } } else { // If just 1 thread, copy the results directly PerThreadSceneResult& result = sceneResults_[0]; minZ_ = result.minZ_; maxZ_ = result.maxZ_; Swap(geometries_, result.geometries_); Swap(lights_, result.lights_); } if (minZ_ == M_INFINITY) minZ_ = 0.0f; // Sort the lights to brightest/closest first, and per-vertex lights first so that per-vertex base pass can be evaluated first for (Light* light : lights_) { light->SetIntensitySortValue(cullCamera_->GetDistance(light->GetNode()->GetWorldPosition())); light->SetLightQueue(nullptr); } Sort(lights_.Begin(), lights_.End(), CompareLights); } void View::GetBatches() { if (!octree_ || !cullCamera_) return; nonThreadedGeometries_.Clear(); threadedGeometries_.Clear(); ProcessLights(); GetLightBatches(); GetBaseBatches(); } void View::ProcessLights() { // Process lit geometries and shadow casters for each light URHO3D_PROFILE(ProcessLights); auto* queue = GetSubsystem(); lightQueryResults_.Resize(lights_.Size()); for (i32 i = 0; i < lightQueryResults_.Size(); ++i) { SharedPtr item = queue->GetFreeItem(); item->priority_ = WI_MAX_PRIORITY; item->workFunction_ = ProcessLightWork; item->aux_ = this; LightQueryResult& query = lightQueryResults_[i]; query.light_ = lights_[i]; item->start_ = &query; queue->AddWorkItem(item); } // Ensure all lights have been processed before proceeding queue->Complete(WI_MAX_PRIORITY); } void View::GetLightBatches() { BatchQueue* alphaQueue = batchQueues_.Contains(alphaPassIndex_) ? &batchQueues_[alphaPassIndex_] : nullptr; // Build light queues and lit batches { URHO3D_PROFILE(GetLightBatches); // Preallocate light queues: per-pixel lights which have lit geometries i32 numLightQueues = 0; i32 usedLightQueues = 0; for (Vector::ConstIterator i = lightQueryResults_.Begin(); i != lightQueryResults_.End(); ++i) { if (!i->light_->GetPerVertex() && i->litGeometries_.Size()) ++numLightQueues; } lightQueues_.Resize(numLightQueues); maxLightsDrawables_.Clear(); i32 maxSortedInstances = renderer_->GetMaxSortedInstances(); for (Vector::Iterator i = lightQueryResults_.Begin(); i != lightQueryResults_.End(); ++i) { LightQueryResult& query = *i; // If light has no affected geometries, no need to process further if (query.litGeometries_.Empty()) continue; Light* light = query.light_; // Per-pixel light if (!light->GetPerVertex()) { i32 shadowSplits = query.numSplits_; // Initialize light queue and store it to the light so that it can be found later LightBatchQueue& lightQueue = lightQueues_[usedLightQueues++]; light->SetLightQueue(&lightQueue); lightQueue.light_ = light; lightQueue.negative_ = light->IsNegative(); lightQueue.shadowMap_ = nullptr; lightQueue.litBaseBatches_.Clear(maxSortedInstances); lightQueue.litBatches_.Clear(maxSortedInstances); if (forwardLightsCommand_) { SetQueueShaderDefines(lightQueue.litBaseBatches_, *forwardLightsCommand_); SetQueueShaderDefines(lightQueue.litBatches_, *forwardLightsCommand_); } else { lightQueue.litBaseBatches_.hasExtraDefines_ = false; lightQueue.litBatches_.hasExtraDefines_ = false; } lightQueue.volumeBatches_.Clear(); // Allocate shadow map now if (shadowSplits > 0) { lightQueue.shadowMap_ = renderer_->GetShadowMap(light, cullCamera_, viewSize_.x_, viewSize_.y_); // If did not manage to get a shadow map, convert the light to unshadowed if (!lightQueue.shadowMap_) shadowSplits = 0; } // Setup shadow batch queues lightQueue.shadowSplits_.Resize(shadowSplits); for (i32 j = 0; j < shadowSplits; ++j) { ShadowBatchQueue& shadowQueue = lightQueue.shadowSplits_[j]; Camera* shadowCamera = query.shadowCameras_[j]; shadowQueue.shadowCamera_ = shadowCamera; shadowQueue.nearSplit_ = query.shadowNearSplits_[j]; shadowQueue.farSplit_ = query.shadowFarSplits_[j]; shadowQueue.shadowBatches_.Clear(maxSortedInstances); // Setup the shadow split viewport and finalize shadow camera parameters shadowQueue.shadowViewport_ = GetShadowMapViewport(light, j, lightQueue.shadowMap_); FinalizeShadowCamera(shadowCamera, light, shadowQueue.shadowViewport_, query.shadowCasterBox_[j]); // Loop through shadow casters for (Vector::ConstIterator k = query.shadowCasters_.Begin() + query.shadowCasterBegin_[j]; k < query.shadowCasters_.Begin() + query.shadowCasterEnd_[j]; ++k) { Drawable* drawable = *k; // If drawable is not in actual view frustum, mark it in view here and check its geometry update type if (!drawable->IsInView(frame_, true)) { drawable->MarkInView(frame_.frameNumber_); UpdateGeometryType type = drawable->GetUpdateGeometryType(); if (type == UPDATE_MAIN_THREAD) nonThreadedGeometries_.Push(drawable); else if (type == UPDATE_WORKER_THREAD) threadedGeometries_.Push(drawable); } const Vector& batches = drawable->GetBatches(); for (const SourceBatch& srcBatch : batches) { Technique* tech = GetTechnique(drawable, srcBatch.material_); if (!srcBatch.geometry_ || !srcBatch.numWorldTransforms_ || !tech) continue; Pass* pass = tech->GetSupportedPass(Technique::shadowPassIndex); // Skip if material has no shadow pass if (!pass) continue; Batch destBatch(srcBatch); destBatch.pass_ = pass; destBatch.zone_ = nullptr; AddBatchToQueue(shadowQueue.shadowBatches_, destBatch, tech); } } } // Process lit geometries for (Vector::ConstIterator j = query.litGeometries_.Begin(); j != query.litGeometries_.End(); ++j) { Drawable* drawable = *j; drawable->AddLight(light); // If drawable limits maximum lights, only record the light, and check maximum count / build batches later if (!drawable->GetMaxLights()) GetLitBatches(drawable, lightQueue, alphaQueue); else maxLightsDrawables_.Insert(drawable); } // In deferred modes, store the light volume batch now. Since light mask 8 lowest bits are output to the stencil, // lights that have all zeroes in the low 8 bits can be skipped; they would not affect geometry anyway if (deferred_ && (light->GetLightMask() & 0xffu) != 0) { Batch volumeBatch; volumeBatch.geometry_ = renderer_->GetLightGeometry(light); volumeBatch.geometryType_ = GEOM_STATIC; volumeBatch.worldTransform_ = &light->GetVolumeTransform(cullCamera_); volumeBatch.numWorldTransforms_ = 1; volumeBatch.lightQueue_ = &lightQueue; volumeBatch.distance_ = light->GetDistance(); volumeBatch.material_ = nullptr; volumeBatch.pass_ = nullptr; volumeBatch.zone_ = nullptr; renderer_->SetLightVolumeBatchShaders(volumeBatch, cullCamera_, lightVolumeCommand_->vertexShaderName_, lightVolumeCommand_->pixelShaderName_, lightVolumeCommand_->vertexShaderDefines_, lightVolumeCommand_->pixelShaderDefines_); lightQueue.volumeBatches_.Push(volumeBatch); } } // Per-vertex light else { // Add the vertex light to lit drawables. It will be processed later during base pass batch generation for (Vector::ConstIterator j = query.litGeometries_.Begin(); j != query.litGeometries_.End(); ++j) { Drawable* drawable = *j; drawable->AddVertexLight(light); } } } } // Process drawables with limited per-pixel light count if (maxLightsDrawables_.Size()) { URHO3D_PROFILE(GetMaxLightsBatches); for (HashSet::Iterator i = maxLightsDrawables_.Begin(); i != maxLightsDrawables_.End(); ++i) { Drawable* drawable = *i; drawable->LimitLights(); const Vector& lights = drawable->GetLights(); for (const Light* light : lights) { // Find the correct light queue again LightBatchQueue* queue = light->GetLightQueue(); if (queue) GetLitBatches(drawable, *queue, alphaQueue); } } } } void View::GetBaseBatches() { URHO3D_PROFILE(GetBaseBatches); for (Vector::ConstIterator i = geometries_.Begin(); i != geometries_.End(); ++i) { Drawable* drawable = *i; UpdateGeometryType type = drawable->GetUpdateGeometryType(); if (type == UPDATE_MAIN_THREAD) nonThreadedGeometries_.Push(drawable); else if (type == UPDATE_WORKER_THREAD) threadedGeometries_.Push(drawable); const Vector& batches = drawable->GetBatches(); bool vertexLightsProcessed = false; for (i32 j = 0; j < batches.Size(); ++j) { const SourceBatch& srcBatch = batches[j]; // Check here if the material refers to a rendertarget texture with camera(s) attached // Only check this for backbuffer views (null rendertarget) if (srcBatch.material_ && srcBatch.material_->GetAuxViewFrameNumber() != frame_.frameNumber_ && !renderTarget_) CheckMaterialForAuxView(srcBatch.material_); Technique* tech = GetTechnique(drawable, srcBatch.material_); if (!srcBatch.geometry_ || !srcBatch.numWorldTransforms_ || !tech) continue; // Check each of the scene passes for (const ScenePassInfo& info : scenePasses_) { // Skip forward base pass if the corresponding litbase pass already exists if (info.passIndex_ == basePassIndex_ && j < 32 && drawable->HasBasePass(j)) continue; Pass* pass = tech->GetSupportedPass(info.passIndex_); if (!pass) continue; Batch destBatch(srcBatch); destBatch.pass_ = pass; destBatch.zone_ = GetZone(drawable); destBatch.isBase_ = true; destBatch.lightMask_ = (unsigned char)GetLightMask(drawable); if (info.vertexLights_) { const Vector& drawableVertexLights = drawable->GetVertexLights(); if (drawableVertexLights.Size() && !vertexLightsProcessed) { // Limit vertex lights. If this is a deferred opaque batch, remove converted per-pixel lights, // as they will be rendered as light volumes in any case, and drawing them also as vertex lights // would result in double lighting drawable->LimitVertexLights(deferred_ && destBatch.pass_->GetBlendMode() == BLEND_REPLACE); vertexLightsProcessed = true; } if (drawableVertexLights.Size()) { // Find a vertex light queue. If not found, create new hash64 hash = GetVertexLightQueueHash(drawableVertexLights); HashMap::Iterator i = vertexLightQueues_.Find(hash); if (i == vertexLightQueues_.End()) { i = vertexLightQueues_.Insert(MakePair(hash, LightBatchQueue())); i->second_.light_ = nullptr; i->second_.shadowMap_ = nullptr; i->second_.vertexLights_ = drawableVertexLights; } destBatch.lightQueue_ = &(i->second_); } } else destBatch.lightQueue_ = nullptr; bool allowInstancing = info.allowInstancing_; if (allowInstancing && info.markToStencil_ && destBatch.lightMask_ != (destBatch.zone_->GetLightMask() & 0xffu)) allowInstancing = false; AddBatchToQueue(*info.batchQueue_, destBatch, tech, allowInstancing); } } } } void View::UpdateGeometries() { // Update geometries in the source view if necessary (prepare order may differ from render order) if (sourceView_ && !sourceView_->geometriesUpdated_) { sourceView_->UpdateGeometries(); return; } URHO3D_PROFILE(SortAndUpdateGeometry); auto* queue = GetSubsystem(); // Sort batches { for (const RenderPathCommand& command : renderPath_->commands_) { if (!IsNecessary(command)) continue; if (command.type_ == CMD_SCENEPASS) { SharedPtr item = queue->GetFreeItem(); item->priority_ = WI_MAX_PRIORITY; item->workFunction_ = command.sortMode_ == SORT_FRONTTOBACK ? SortBatchQueueFrontToBackWork : SortBatchQueueBackToFrontWork; item->start_ = &batchQueues_[command.passIndex_]; queue->AddWorkItem(item); } } for (Vector::Iterator i = lightQueues_.Begin(); i != lightQueues_.End(); ++i) { SharedPtr lightItem = queue->GetFreeItem(); lightItem->priority_ = WI_MAX_PRIORITY; lightItem->workFunction_ = SortLightQueueWork; lightItem->start_ = &(*i); queue->AddWorkItem(lightItem); if (i->shadowSplits_.Size()) { SharedPtr shadowItem = queue->GetFreeItem(); shadowItem->priority_ = WI_MAX_PRIORITY; shadowItem->workFunction_ = SortShadowQueueWork; shadowItem->start_ = &(*i); queue->AddWorkItem(shadowItem); } } } // Update geometries. Split into threaded and non-threaded updates. { if (threadedGeometries_.Size()) { // In special cases (context loss, multi-view) a drawable may theoretically first have reported a threaded update, but will actually // require a main thread update. Check these cases first and move as applicable. The threaded work routine will tolerate the null // pointer holes that we leave to the threaded update queue. for (Vector::Iterator i = threadedGeometries_.Begin(); i != threadedGeometries_.End(); ++i) { if ((*i)->GetUpdateGeometryType() == UPDATE_MAIN_THREAD) { nonThreadedGeometries_.Push(*i); *i = nullptr; } } int numWorkItems = queue->GetNumThreads() + 1; // Worker threads + main thread int drawablesPerItem = threadedGeometries_.Size() / numWorkItems; Vector::Iterator start = threadedGeometries_.Begin(); for (int i = 0; i < numWorkItems; ++i) { Vector::Iterator end = threadedGeometries_.End(); if (i < numWorkItems - 1 && end - start > drawablesPerItem) end = start + drawablesPerItem; SharedPtr item = queue->GetFreeItem(); item->priority_ = WI_MAX_PRIORITY; item->workFunction_ = UpdateDrawableGeometriesWork; item->aux_ = const_cast(&frame_); item->start_ = &(*start); item->end_ = &(*end); queue->AddWorkItem(item); start = end; } } // While the work queue is processed, update non-threaded geometries for (Vector::ConstIterator i = nonThreadedGeometries_.Begin(); i != nonThreadedGeometries_.End(); ++i) (*i)->UpdateGeometry(frame_); } // Finally ensure all threaded work has completed queue->Complete(WI_MAX_PRIORITY); geometriesUpdated_ = true; } void View::GetLitBatches(Drawable* drawable, LightBatchQueue& lightQueue, BatchQueue* alphaQueue) { Light* light = lightQueue.light_; Zone* zone = GetZone(drawable); const Vector& batches = drawable->GetBatches(); bool allowLitBase = useLitBase_ && !lightQueue.negative_ && light == drawable->GetFirstLight() && drawable->GetVertexLights().Empty() && !zone->GetAmbientGradient(); for (i32 i = 0; i < batches.Size(); ++i) { const SourceBatch& srcBatch = batches[i]; Technique* tech = GetTechnique(drawable, srcBatch.material_); if (!srcBatch.geometry_ || !srcBatch.numWorldTransforms_ || !tech) continue; // Do not create pixel lit forward passes for materials that render into the G-buffer if (gBufferPassIndex_ != NINDEX && tech->HasPass(gBufferPassIndex_)) continue; Batch destBatch(srcBatch); bool isLitAlpha = false; // Check for lit base pass. Because it uses the replace blend mode, it must be ensured to be the first light // Also vertex lighting or ambient gradient require the non-lit base pass, so skip in those cases if (i < 32 && allowLitBase) { destBatch.pass_ = tech->GetSupportedPass(litBasePassIndex_); if (destBatch.pass_) { destBatch.isBase_ = true; drawable->SetBasePass(i); } else destBatch.pass_ = tech->GetSupportedPass(lightPassIndex_); } else destBatch.pass_ = tech->GetSupportedPass(lightPassIndex_); // If no lit pass, check for lit alpha if (!destBatch.pass_) { destBatch.pass_ = tech->GetSupportedPass(litAlphaPassIndex_); isLitAlpha = true; } // Skip if material does not receive light at all if (!destBatch.pass_) continue; destBatch.lightQueue_ = &lightQueue; destBatch.zone_ = zone; if (!isLitAlpha) { if (destBatch.isBase_) AddBatchToQueue(lightQueue.litBaseBatches_, destBatch, tech); else AddBatchToQueue(lightQueue.litBatches_, destBatch, tech); } else if (alphaQueue) { // Transparent batches can not be instanced, and shadows on transparencies can only be rendered if shadow maps are // not reused AddBatchToQueue(*alphaQueue, destBatch, tech, false, !renderer_->GetReuseShadowMaps()); } } } void View::ExecuteRenderPathCommands() { View* actualView = sourceView_ ? sourceView_ : this; // If not reusing shadowmaps, render all of them first if (!renderer_->GetReuseShadowMaps() && renderer_->GetDrawShadows() && !actualView->lightQueues_.Empty()) { URHO3D_PROFILE(RenderShadowMaps); for (Vector::Iterator i = actualView->lightQueues_.Begin(); i != actualView->lightQueues_.End(); ++i) { if (NeedRenderShadowMap(*i)) RenderShadowMap(*i); } } { URHO3D_PROFILE(ExecuteRenderPath); // Set for safety in case of empty renderpath currentRenderTarget_ = substituteRenderTarget_ ? substituteRenderTarget_ : renderTarget_; currentViewportTexture_ = nullptr; passCommand_ = nullptr; bool viewportModified = false; bool isPingponging = false; usedResolve_ = false; i32 lastCommandIndex = 0; for (i32 i = 0; i < renderPath_->commands_.Size(); ++i) { RenderPathCommand& command = renderPath_->commands_[i]; if (actualView->IsNecessary(command)) lastCommandIndex = i; } for (i32 i = 0; i < renderPath_->commands_.Size(); ++i) { RenderPathCommand& command = renderPath_->commands_[i]; if (!actualView->IsNecessary(command)) continue; bool viewportRead = actualView->CheckViewportRead(command); bool viewportWrite = actualView->CheckViewportWrite(command); bool beginPingpong = actualView->CheckPingpong(i); // Has the viewport been modified and will be read as a texture by the current command? if (viewportRead && viewportModified) { // Start pingponging without a blit if already rendering to the substitute render target if (currentRenderTarget_ && currentRenderTarget_ == substituteRenderTarget_ && beginPingpong) isPingponging = true; // If not using pingponging, simply resolve/copy to the first viewport texture if (!isPingponging) { if (!currentRenderTarget_) { graphics_->ResolveToTexture(dynamic_cast(viewportTextures_[0]), viewRect_); currentViewportTexture_ = viewportTextures_[0]; viewportModified = false; usedResolve_ = true; } else { if (viewportWrite) { BlitFramebuffer(currentRenderTarget_->GetParentTexture(), GetRenderSurfaceFromTexture(viewportTextures_[0]), false); currentViewportTexture_ = viewportTextures_[0]; viewportModified = false; } else { // If the current render target is already a texture, and we are not writing to it, can read that // texture directly instead of blitting. However keep the viewport dirty flag in case a later command // will do both read and write, and then we need to blit / resolve currentViewportTexture_ = currentRenderTarget_->GetParentTexture(); } } } else { // Swap the pingpong double buffer sides. Texture 0 will be read next viewportTextures_[1] = viewportTextures_[0]; viewportTextures_[0] = currentRenderTarget_->GetParentTexture(); currentViewportTexture_ = viewportTextures_[0]; viewportModified = false; } } if (beginPingpong) isPingponging = true; // Determine viewport write target if (viewportWrite) { if (isPingponging) { currentRenderTarget_ = GetRenderSurfaceFromTexture(viewportTextures_[1]); // If the render path ends into a quad, it can be redirected to the final render target // However, on OpenGL we can not reliably do this in case the final target is the backbuffer, and we want to // render depth buffer sensitive debug geometry afterward (backbuffer and textures can not share depth) if (Graphics::GetGAPI() != GAPI_OPENGL) { if (i == lastCommandIndex && command.type_ == CMD_QUAD) currentRenderTarget_ = renderTarget_; } else { if (i == lastCommandIndex && command.type_ == CMD_QUAD && renderTarget_) currentRenderTarget_ = renderTarget_; } } else currentRenderTarget_ = substituteRenderTarget_ ? substituteRenderTarget_ : renderTarget_; } switch (command.type_) { case CMD_CLEAR: { URHO3D_PROFILE(ClearRenderTarget); Color clearColor = command.clearColor_; if (command.useFogColor_) clearColor = actualView->farClipZone_->GetFogColor(); SetRenderTargets(command); graphics_->Clear(command.clearFlags_, clearColor, command.clearDepth_, command.clearStencil_); } break; case CMD_SCENEPASS: { BatchQueue& queue = actualView->batchQueues_[command.passIndex_]; if (!queue.IsEmpty()) { URHO3D_PROFILE(RenderScenePass); SetRenderTargets(command); bool allowDepthWrite = SetTextures(command); graphics_->SetClipPlane(camera_->GetUseClipping(), camera_->GetClipPlane(), camera_->GetView(), camera_->GetGPUProjection()); if (command.shaderParameters_.Size()) { // If pass defines shader parameters, reset parameter sources now to ensure they all will be set // (will be set after camera shader parameters) graphics_->ClearParameterSources(); passCommand_ = &command; } queue.Draw(this, camera_, command.markToStencil_, false, allowDepthWrite); passCommand_ = nullptr; } } break; case CMD_QUAD: { URHO3D_PROFILE(RenderQuad); SetRenderTargets(command); SetTextures(command); RenderQuad(command); } break; case CMD_FORWARDLIGHTS: // Render shadow maps + opaque objects' additive lighting if (!actualView->lightQueues_.Empty()) { URHO3D_PROFILE(RenderLights); SetRenderTargets(command); for (Vector::Iterator i = actualView->lightQueues_.Begin(); i != actualView->lightQueues_.End(); ++i) { // If reusing shadowmaps, render each of them before the lit batches if (renderer_->GetReuseShadowMaps() && NeedRenderShadowMap(*i)) { RenderShadowMap(*i); SetRenderTargets(command); } bool allowDepthWrite = SetTextures(command); graphics_->SetClipPlane(camera_->GetUseClipping(), camera_->GetClipPlane(), camera_->GetView(), camera_->GetGPUProjection()); if (command.shaderParameters_.Size()) { graphics_->ClearParameterSources(); passCommand_ = &command; } // Draw base (replace blend) batches first i->litBaseBatches_.Draw(this, camera_, false, false, allowDepthWrite); // Then, if there are additive passes, optimize the light and draw them if (!i->litBatches_.IsEmpty()) { renderer_->OptimizeLightByScissor(i->light_, camera_); if (!noStencil_) renderer_->OptimizeLightByStencil(i->light_, camera_); i->litBatches_.Draw(this, camera_, false, true, allowDepthWrite); } passCommand_ = nullptr; } graphics_->SetScissorTest(false); graphics_->SetStencilTest(false); } break; case CMD_LIGHTVOLUMES: // Render shadow maps + light volumes if (!actualView->lightQueues_.Empty()) { URHO3D_PROFILE(RenderLightVolumes); SetRenderTargets(command); for (Vector::Iterator i = actualView->lightQueues_.Begin(); i != actualView->lightQueues_.End(); ++i) { // If reusing shadowmaps, render each of them before the lit batches if (renderer_->GetReuseShadowMaps() && NeedRenderShadowMap(*i)) { RenderShadowMap(*i); SetRenderTargets(command); } SetTextures(command); if (command.shaderParameters_.Size()) { graphics_->ClearParameterSources(); passCommand_ = &command; } for (Batch& volumeBatch : i->volumeBatches_) { SetupLightVolumeBatch(volumeBatch); volumeBatch.Draw(this, camera_, false); } passCommand_ = nullptr; } graphics_->SetScissorTest(false); graphics_->SetStencilTest(false); } break; case CMD_RENDERUI: { SetRenderTargets(command); GetSubsystem()->Render(true); } break; case CMD_SENDEVENT: { using namespace RenderPathEvent; VariantMap& eventData = GetEventDataMap(); eventData[P_NAME] = command.eventName_; renderer_->SendEvent(E_RENDERPATHEVENT, eventData); } break; default: break; } // If current command output to the viewport, mark it modified if (viewportWrite) viewportModified = true; } } } void View::SetRenderTargets(RenderPathCommand& command) { i32 index = 0; bool useColorWrite = true; bool useCustomDepth = false; bool useViewportOutput = false; while (index < command.outputs_.Size()) { if (!command.outputs_[index].first_.Compare("viewport", false)) { graphics_->SetRenderTarget(index, currentRenderTarget_); useViewportOutput = true; } else { Texture* texture = FindNamedTexture(command.outputs_[index].first_, true, false); // Check for depth only rendering (by specifying a depth texture as the sole output) if (!index && command.outputs_.Size() == 1 && texture && (texture->GetFormat() == Graphics::GetReadableDepthFormat() || texture->GetFormat() == Graphics::GetDepthStencilFormat())) { useColorWrite = false; useCustomDepth = true; graphics_->SetRenderTarget(0, (RenderSurface*)nullptr); graphics_->SetDepthStencil(GetRenderSurfaceFromTexture(texture)); } else graphics_->SetRenderTarget(index, GetRenderSurfaceFromTexture(texture, command.outputs_[index].second_)); } ++index; } while (index < MAX_RENDERTARGETS) { graphics_->SetRenderTarget(index, (RenderSurface*)nullptr); ++index; } if (command.depthStencilName_.Length()) { Texture* depthTexture = FindNamedTexture(command.depthStencilName_, true, false); if (depthTexture) { useCustomDepth = true; lastCustomDepthSurface_ = GetRenderSurfaceFromTexture(depthTexture); graphics_->SetDepthStencil(lastCustomDepthSurface_); } } // When rendering to the final destination rendertarget, use the actual viewport. Otherwise texture rendertargets should use // their full size as the viewport IntVector2 rtSizeNow = graphics_->GetRenderTargetDimensions(); IntRect viewport = (useViewportOutput && currentRenderTarget_ == renderTarget_) ? viewRect_ : IntRect(0, 0, rtSizeNow.x_, rtSizeNow.y_); if (!useCustomDepth) graphics_->SetDepthStencil(GetDepthStencil(graphics_->GetRenderTarget(0))); graphics_->SetViewport(viewport); graphics_->SetColorWrite(useColorWrite); } bool View::SetTextures(RenderPathCommand& command) { bool allowDepthWrite = true; for (i32 i = 0; i < MAX_TEXTURE_UNITS; ++i) { if (command.textureNames_[i].Empty()) continue; // Bind the rendered output if (!command.textureNames_[i].Compare("viewport", false)) { graphics_->SetTexture(i, currentViewportTexture_); continue; } #ifdef DESKTOP_GRAPHICS_OR_GLES3 Texture* texture = FindNamedTexture(command.textureNames_[i], false, i == TU_VOLUMEMAP); #else Texture* texture = FindNamedTexture(command.textureNames_[i], false, false); #endif if (texture) { graphics_->SetTexture(i, texture); // Check if the current depth stencil is being sampled if (graphics_->GetDepthStencil() && texture == graphics_->GetDepthStencil()->GetParentTexture()) allowDepthWrite = false; } else { // If requesting a texture fails, clear the texture name to prevent redundant attempts command.textureNames_[i] = String::EMPTY; } } return allowDepthWrite; } void View::RenderQuad(RenderPathCommand& command) { if (command.vertexShaderName_.Empty() || command.pixelShaderName_.Empty()) return; // If shader can not be found, clear it from the command to prevent redundant attempts ShaderVariation* vs = graphics_->GetShader(VS, command.vertexShaderName_, command.vertexShaderDefines_); if (!vs) command.vertexShaderName_ = String::EMPTY; ShaderVariation* ps = graphics_->GetShader(PS, command.pixelShaderName_, command.pixelShaderDefines_); if (!ps) command.pixelShaderName_ = String::EMPTY; // Set shaders & shader parameters and textures graphics_->SetShaders(vs, ps); SetGlobalShaderParameters(); SetCameraShaderParameters(camera_); // During renderpath commands the G-Buffer or viewport texture is assumed to always be viewport-sized IntRect viewport = graphics_->GetViewport(); IntVector2 viewSize = IntVector2(viewport.Width(), viewport.Height()); SetGBufferShaderParameters(viewSize, IntRect(0, 0, viewSize.x_, viewSize.y_)); // Set per-rendertarget inverse size / offset shader parameters as necessary for (const RenderTargetInfo& rtInfo : renderPath_->renderTargets_) { if (!rtInfo.enabled_) continue; StringHash nameHash(rtInfo.name_); if (!renderTargets_.Contains(nameHash)) continue; String invSizeName = rtInfo.name_ + "InvSize"; String offsetsName = rtInfo.name_ + "Offsets"; auto width = (float)renderTargets_[nameHash]->GetWidth(); auto height = (float)renderTargets_[nameHash]->GetHeight(); graphics_->SetShaderParameter(invSizeName, Vector2(1.0f / width, 1.0f / height)); } // Set command's shader parameters last to allow them to override any of the above SetCommandShaderParameters(command); graphics_->SetBlendMode(command.blendMode_); graphics_->SetDepthTest(CMP_ALWAYS); graphics_->SetDepthWrite(false); graphics_->SetFillMode(FILL_SOLID); graphics_->SetLineAntiAlias(false); graphics_->SetClipPlane(false); graphics_->SetScissorTest(false); graphics_->SetStencilTest(false); DrawFullscreenQuad(false); } bool View::IsNecessary(const RenderPathCommand& command) { return command.enabled_ && command.outputs_.Size() && (command.type_ != CMD_SCENEPASS || !batchQueues_[command.passIndex_].IsEmpty()); } bool View::CheckViewportRead(const RenderPathCommand& command) { for (const auto& textureName : command.textureNames_) { if (!textureName.Empty() && !textureName.Compare("viewport", false)) return true; } return false; } bool View::CheckViewportWrite(const RenderPathCommand& command) { for (const Pair& output : command.outputs_) { if (!output.first_.Compare("viewport", false)) return true; } return false; } bool View::CheckPingpong(i32 index) { assert(index >= 0); // Current command must be a viewport-reading & writing quad to begin the pingpong chain RenderPathCommand& current = renderPath_->commands_[index]; if (current.type_ != CMD_QUAD || !CheckViewportRead(current) || !CheckViewportWrite(current)) return false; // If there are commands other than quads that target the viewport, we must keep rendering to the final target and resolving // to a viewport texture when necessary instead of pingponging, as a scene pass is not guaranteed to fill the entire viewport for (i32 i = index + 1; i < renderPath_->commands_.Size(); ++i) { RenderPathCommand& command = renderPath_->commands_[i]; if (!IsNecessary(command)) continue; if (CheckViewportWrite(command)) { if (command.type_ != CMD_QUAD) return false; } } return true; } void View::AllocateScreenBuffers() { View* actualView = sourceView_ ? sourceView_ : this; bool hasScenePassToRTs = false; bool hasCustomDepth = false; bool hasViewportRead = false; bool hasPingpong = false; bool needSubstitute = false; i32 numViewportTextures = 0; lastCustomDepthSurface_ = nullptr; // Check for commands with special meaning: has custom depth, renders a scene pass to other than the destination viewport, // read the viewport, or pingpong between viewport textures. These may trigger the need to substitute the destination RT for (i32 i = 0; i < renderPath_->commands_.Size(); ++i) { const RenderPathCommand& command = renderPath_->commands_[i]; if (!actualView->IsNecessary(command)) continue; if (!hasViewportRead && CheckViewportRead(command)) hasViewportRead = true; if (!hasPingpong && CheckPingpong(i)) hasPingpong = true; if (command.depthStencilName_.Length()) hasCustomDepth = true; if (!hasScenePassToRTs && command.type_ == CMD_SCENEPASS) { for (const Pair& output : command.outputs_) { if (output.first_.Compare("viewport", false)) { hasScenePassToRTs = true; break; } } } } if (Graphics::GetGAPI() == GAPI_OPENGL) { // Due to FBO limitations, in OpenGL deferred modes need to render to texture first and then blit to the backbuffer // Also, if rendering to a texture with full deferred rendering, it must be RGBA to comply with the rest of the buffers, // unless using OpenGL 3 if (((deferred_ || hasScenePassToRTs) && !renderTarget_) || (!Graphics::GetGL3Support() && deferredAmbient_ && renderTarget_ && renderTarget_->GetParentTexture()->GetFormat() != Graphics::GetRGBAFormat())) needSubstitute = true; // Also need substitute if rendering to backbuffer using a custom (readable) depth buffer if (!renderTarget_ && hasCustomDepth) needSubstitute = true; } // If backbuffer is antialiased when using deferred rendering, need to reserve a buffer if (deferred_ && !renderTarget_ && graphics_->GetMultiSample() > 1) needSubstitute = true; // If viewport is smaller than whole texture/backbuffer in deferred rendering, need to reserve a buffer, as the G-buffer // textures will be sized equal to the viewport if (viewSize_.x_ < rtSize_.x_ || viewSize_.y_ < rtSize_.y_) { if (deferred_ || hasScenePassToRTs || hasCustomDepth) needSubstitute = true; } // Follow final rendertarget format, or use RGB to match the backbuffer format unsigned format = renderTarget_ ? renderTarget_->GetParentTexture()->GetFormat() : Graphics::GetRGBFormat(); // If HDR rendering is enabled use RGBA16f and reserve a buffer if (renderer_->GetHDRRendering()) { format = Graphics::GetRGBAFloat16Format(); needSubstitute = true; } // On OpenGL 2 ensure that all MRT buffers are RGBA in deferred rendering if (deferred_ && !renderer_->GetHDRRendering() && Graphics::GetGAPI() == GAPI_OPENGL && !Graphics::GetGL3Support()) format = Graphics::GetRGBAFormat(); if (hasViewportRead) { ++numViewportTextures; // If OpenGL ES, use substitute target to avoid resolve from the backbuffer, which may be slow. However if multisampling // is specified, there is no choice #ifdef URHO3D_GLES2 if (!renderTarget_ && graphics_->GetMultiSample() < 2) needSubstitute = true; #endif // If we have viewport read and target is a cube map, must allocate a substitute target instead as BlitFramebuffer() // does not support reading a cube map if (renderTarget_ && renderTarget_->GetParentTexture()->GetType() == TextureCube::GetTypeStatic()) needSubstitute = true; // If rendering to a texture, but the viewport is less than the whole texture, use a substitute to ensure // postprocessing shaders will never read outside the viewport if (renderTarget_ && (viewSize_.x_ < renderTarget_->GetWidth() || viewSize_.y_ < renderTarget_->GetHeight())) needSubstitute = true; if (hasPingpong && !needSubstitute) ++numViewportTextures; } // Allocate screen buffers. Enable filtering in case the quad commands need that // Follow the sRGB mode of the destination render target bool sRGB = renderTarget_ ? renderTarget_->GetParentTexture()->GetSRGB() : graphics_->GetSRGB(); substituteRenderTarget_ = needSubstitute ? GetRenderSurfaceFromTexture(renderer_->GetScreenBuffer(viewSize_.x_, viewSize_.y_, format, 1, false, false, true, sRGB)) : nullptr; for (i32 i = 0; i < MAX_VIEWPORT_TEXTURES; ++i) { viewportTextures_[i] = i < numViewportTextures ? renderer_->GetScreenBuffer(viewSize_.x_, viewSize_.y_, format, 1, false, false, true, sRGB) : nullptr; } // If using a substitute render target and pingponging, the substitute can act as the second viewport texture if (numViewportTextures == 1 && substituteRenderTarget_) viewportTextures_[1] = substituteRenderTarget_->GetParentTexture(); // Allocate extra render targets defined by the render path for (const RenderTargetInfo& rtInfo : renderPath_->renderTargets_) { if (!rtInfo.enabled_) continue; float width = rtInfo.size_.x_; float height = rtInfo.size_.y_; if (rtInfo.sizeMode_ == SIZE_VIEWPORTDIVISOR) { width = (float)viewSize_.x_ / Max(width, M_EPSILON); height = (float)viewSize_.y_ / Max(height, M_EPSILON); } else if (rtInfo.sizeMode_ == SIZE_VIEWPORTMULTIPLIER) { width = (float)viewSize_.x_ * width; height = (float)viewSize_.y_ * height; } auto intWidth = RoundToInt(width); auto intHeight = RoundToInt(height); // If the rendertarget is persistent, key it with a hash derived from the RT name and the view's pointer renderTargets_[rtInfo.name_] = renderer_->GetScreenBuffer(intWidth, intHeight, rtInfo.format_, rtInfo.multiSample_, rtInfo.autoResolve_, rtInfo.cubemap_, rtInfo.filtered_, rtInfo.sRGB_, rtInfo.persistent_ ? StringHash(rtInfo.name_).Value() + (unsigned)(size_t)this : 0); } } void View::BlitFramebuffer(Texture* source, RenderSurface* destination, bool depthWrite) { if (!source) return; URHO3D_PROFILE(BlitFramebuffer); // If blitting to the destination rendertarget, use the actual viewport. Intermediate textures on the other hand // are always viewport-sized IntVector2 srcSize(source->GetWidth(), source->GetHeight()); IntVector2 destSize = destination ? IntVector2(destination->GetWidth(), destination->GetHeight()) : IntVector2( graphics_->GetWidth(), graphics_->GetHeight()); IntRect srcRect = (GetRenderSurfaceFromTexture(source) == renderTarget_) ? viewRect_ : IntRect(0, 0, srcSize.x_, srcSize.y_); IntRect destRect = (destination == renderTarget_) ? viewRect_ : IntRect(0, 0, destSize.x_, destSize.y_); graphics_->SetBlendMode(BLEND_REPLACE); graphics_->SetDepthTest(CMP_ALWAYS); graphics_->SetDepthWrite(depthWrite); graphics_->SetFillMode(FILL_SOLID); graphics_->SetLineAntiAlias(false); graphics_->SetClipPlane(false); graphics_->SetScissorTest(false); graphics_->SetStencilTest(false); graphics_->SetRenderTarget(0, destination); for (i32 i = 1; i < MAX_RENDERTARGETS; ++i) graphics_->SetRenderTarget(i, (RenderSurface*)nullptr); graphics_->SetDepthStencil(GetDepthStencil(destination)); graphics_->SetViewport(destRect); static const char* shaderName = "CopyFramebuffer"; graphics_->SetShaders(graphics_->GetShader(VS, shaderName), graphics_->GetShader(PS, shaderName)); SetGBufferShaderParameters(srcSize, srcRect); graphics_->SetTexture(TU_DIFFUSE, source); DrawFullscreenQuad(true); } void View::DrawFullscreenQuad(bool setIdentityProjection) { Geometry* geometry = renderer_->GetQuadGeometry(); // If no camera, no choice but to use identity projection if (!camera_) setIdentityProjection = true; if (setIdentityProjection) { Matrix3x4 model = Matrix3x4::IDENTITY; Matrix4 projection = Matrix4::IDENTITY; if (Graphics::GetGAPI() == GAPI_OPENGL) { if (camera_ && camera_->GetFlipVertical()) projection.m11_ = -1.0f; model.m23_ = 0.0f; } else { model.m23_ = 0.5f; } graphics_->SetShaderParameter(VSP_MODEL, model); graphics_->SetShaderParameter(VSP_VIEWPROJ, projection); } else graphics_->SetShaderParameter(VSP_MODEL, Light::GetFullscreenQuadTransform(camera_)); graphics_->SetCullMode(CULL_NONE); graphics_->ClearTransformSources(); geometry->Draw(graphics_); } void View::UpdateOccluders(Vector& occluders, Camera* camera) { float occluderSizeThreshold_ = renderer_->GetOccluderSizeThreshold(); float halfViewSize = camera->GetHalfViewSize(); float invOrthoSize = 1.0f / camera->GetOrthoSize(); for (Vector::Iterator i = occluders.Begin(); i != occluders.End();) { Drawable* occluder = *i; bool erase = false; if (!occluder->IsInView(frame_, true)) occluder->UpdateBatches(frame_); // Check occluder's draw distance (in main camera view) float maxDistance = occluder->GetDrawDistance(); if (maxDistance <= 0.0f || occluder->GetDistance() <= maxDistance) { // Check that occluder is big enough on the screen const BoundingBox& box = occluder->GetWorldBoundingBox(); float diagonal = box.Size().Length(); float compare; if (!camera->IsOrthographic()) { // Occluders which are near the camera are more useful then occluders at the end of the camera's draw distance float cameraMaxDistanceFraction = occluder->GetDistance() / camera->GetFarClip(); compare = diagonal * halfViewSize / (occluder->GetDistance() * cameraMaxDistanceFraction); // Give higher priority to occluders which the camera is inside their AABB const Vector3& cameraPos = camera->GetNode() ? camera->GetNode()->GetWorldPosition() : Vector3::ZERO; if (box.IsInside(cameraPos)) compare *= diagonal; // size^2 } else compare = diagonal * invOrthoSize; if (compare < occluderSizeThreshold_) erase = true; else { // Best occluders have big triangles (low density) float density = occluder->GetNumOccluderTriangles() / diagonal; // Lower value is higher priority occluder->SetSortValue(density / compare); } } else erase = true; if (erase) i = occluders.Erase(i); else ++i; } // Sort occluders so that if triangle budget is exceeded, best occluders have been drawn if (occluders.Size()) Sort(occluders.Begin(), occluders.End(), CompareDrawables); } void View::DrawOccluders(OcclusionBuffer* buffer, const Vector& occluders) { buffer->SetMaxTriangles(maxOccluderTriangles_); buffer->Clear(); if (!buffer->IsThreaded()) { // If not threaded, draw occluders one by one and test the next occluder against already rasterized depth for (i32 i = 0; i < occluders.Size(); ++i) { Drawable* occluder = occluders[i]; if (i > 0) { // For subsequent occluders, do a test against the pixel-level occlusion buffer to see if rendering is necessary if (!buffer->IsVisible(occluder->GetWorldBoundingBox())) continue; } // Check for running out of triangles ++activeOccluders_; bool success = occluder->DrawOcclusion(buffer); // Draw triangles submitted by this occluder buffer->DrawTriangles(); if (!success) break; } } else { // In threaded mode submit all triangles first, then render (cannot test in this case) for (Drawable* occluder : occluders) { // Check for running out of triangles ++activeOccluders_; if (!occluder->DrawOcclusion(buffer)) break; } buffer->DrawTriangles(); } // Finally build the depth mip levels buffer->BuildDepthHierarchy(); } void View::ProcessLight(LightQueryResult& query, i32 threadIndex) { assert(threadIndex >= 0); Light* light = query.light_; LightType type = light->GetLightType(); unsigned lightMask = light->GetLightMask(); const Frustum& frustum = cullCamera_->GetFrustum(); // Check if light should be shadowed bool isShadowed = drawShadows_ && light->GetCastShadows() && !light->GetPerVertex() && light->GetShadowIntensity() < 1.0f; // If shadow distance non-zero, check it if (isShadowed && light->GetShadowDistance() > 0.0f && light->GetDistance() > light->GetShadowDistance()) isShadowed = false; // OpenGL ES can not support point light shadows #if defined(URHO3D_GLES2) if (isShadowed && type == LIGHT_POINT) isShadowed = false; #endif // Get lit geometries. They must match the light mask and be inside the main camera frustum to be considered Vector& tempDrawables = tempDrawables_[threadIndex]; query.litGeometries_.Clear(); switch (type) { case LIGHT_DIRECTIONAL: for (Drawable* geometry : geometries_) { if (GetLightMask(geometry) & lightMask) query.litGeometries_.Push(geometry); } break; case LIGHT_SPOT: { FrustumOctreeQuery octreeQuery(tempDrawables, light->GetFrustum(), DrawableTypes::Geometry, cullCamera_->GetViewMask()); octree_->GetDrawables(octreeQuery); for (Drawable* tempDrawable : tempDrawables) { if (tempDrawable->IsInView(frame_) && (GetLightMask(tempDrawable) & lightMask)) query.litGeometries_.Push(tempDrawable); } } break; case LIGHT_POINT: { SphereOctreeQuery octreeQuery(tempDrawables, Sphere(light->GetNode()->GetWorldPosition(), light->GetRange()), DrawableTypes::Geometry, cullCamera_->GetViewMask()); octree_->GetDrawables(octreeQuery); for (Drawable* tempDrawable : tempDrawables) { if (tempDrawable->IsInView(frame_) && (GetLightMask(tempDrawable) & lightMask)) query.litGeometries_.Push(tempDrawable); } } break; } // If no lit geometries or not shadowed, no need to process shadow cameras if (query.litGeometries_.Empty() || !isShadowed) { query.numSplits_ = 0; return; } // Determine number of shadow cameras and setup their initial positions SetupShadowCameras(query); // Process each split for shadow casters query.shadowCasters_.Clear(); for (i32 i = 0; i < query.numSplits_; ++i) { Camera* shadowCamera = query.shadowCameras_[i]; const Frustum& shadowCameraFrustum = shadowCamera->GetFrustum(); query.shadowCasterBegin_[i] = query.shadowCasterEnd_[i] = query.shadowCasters_.Size(); // For point light check that the face is visible: if not, can skip the split if (type == LIGHT_POINT && frustum.IsInsideFast(BoundingBox(shadowCameraFrustum)) == OUTSIDE) continue; // For directional light check that the split is inside the visible scene: if not, can skip the split if (type == LIGHT_DIRECTIONAL) { if (minZ_ > query.shadowFarSplits_[i]) continue; if (maxZ_ < query.shadowNearSplits_[i]) continue; // Reuse lit geometry query for all except directional lights ShadowCasterOctreeQuery query(tempDrawables, shadowCameraFrustum, DrawableTypes::Geometry, cullCamera_->GetViewMask()); octree_->GetDrawables(query); } // Check which shadow casters actually contribute to the shadowing ProcessShadowCasters(query, tempDrawables, i); } // If no shadow casters, the light can be rendered unshadowed. At this point we have not allocated a shadow map yet, so the // only cost has been the shadow camera setup & queries if (query.shadowCasters_.Empty()) query.numSplits_ = 0; } void View::ProcessShadowCasters(LightQueryResult& query, const Vector& drawables, i32 splitIndex) { assert(splitIndex >= 0); Light* light = query.light_; unsigned lightMask = light->GetLightMask(); Camera* shadowCamera = query.shadowCameras_[splitIndex]; const Frustum& shadowCameraFrustum = shadowCamera->GetFrustum(); const Matrix3x4& lightView = shadowCamera->GetView(); const Matrix4& lightProj = shadowCamera->GetProjection(); LightType type = light->GetLightType(); query.shadowCasterBox_[splitIndex].Clear(); // Transform scene frustum into shadow camera's view space for shadow caster visibility check. For point & spot lights, // we can use the whole scene frustum. For directional lights, use the intersection of the scene frustum and the split // frustum, so that shadow casters do not get rendered into unnecessary splits Frustum lightViewFrustum; if (type != LIGHT_DIRECTIONAL) lightViewFrustum = cullCamera_->GetSplitFrustum(minZ_, maxZ_).Transformed(lightView); else lightViewFrustum = cullCamera_->GetSplitFrustum(Max(minZ_, query.shadowNearSplits_[splitIndex]), Min(maxZ_, query.shadowFarSplits_[splitIndex])).Transformed(lightView); BoundingBox lightViewFrustumBox(lightViewFrustum); // Check for degenerate split frustum: in that case there is no need to get shadow casters if (lightViewFrustum.vertices_[0] == lightViewFrustum.vertices_[4]) return; BoundingBox lightViewBox; BoundingBox lightProjBox; for (Vector::ConstIterator i = drawables.Begin(); i != drawables.End(); ++i) { Drawable* drawable = *i; // In case this is a point or spot light query result reused for optimization, we may have non-shadowcasters included. // Check for that first if (!drawable->GetCastShadows()) continue; // Check shadow mask if (!(GetShadowMask(drawable) & lightMask)) continue; // For point light, check that this drawable is inside the split shadow camera frustum if (type == LIGHT_POINT && shadowCameraFrustum.IsInsideFast(drawable->GetWorldBoundingBox()) == OUTSIDE) continue; // Check shadow distance // Note: as lights are processed threaded, it is possible a drawable's UpdateBatches() function is called several // times. However, this should not cause problems as no scene modification happens at this point. if (!drawable->IsInView(frame_, true)) drawable->UpdateBatches(frame_); float maxShadowDistance = drawable->GetShadowDistance(); float drawDistance = drawable->GetDrawDistance(); if (drawDistance > 0.0f && (maxShadowDistance <= 0.0f || drawDistance < maxShadowDistance)) maxShadowDistance = drawDistance; if (maxShadowDistance > 0.0f && drawable->GetDistance() > maxShadowDistance) continue; // Project shadow caster bounding box to light view space for visibility check lightViewBox = drawable->GetWorldBoundingBox().Transformed(lightView); if (IsShadowCasterVisible(drawable, lightViewBox, shadowCamera, lightView, lightViewFrustum, lightViewFrustumBox)) { // Merge to shadow caster bounding box (only needed for focused spot lights) and add to the list if (type == LIGHT_SPOT && light->GetShadowFocus().focus_) { lightProjBox = lightViewBox.Projected(lightProj); query.shadowCasterBox_[splitIndex].Merge(lightProjBox); } query.shadowCasters_.Push(drawable); } } query.shadowCasterEnd_[splitIndex] = query.shadowCasters_.Size(); } bool View::IsShadowCasterVisible(Drawable* drawable, BoundingBox lightViewBox, Camera* shadowCamera, const Matrix3x4& lightView, const Frustum& lightViewFrustum, const BoundingBox& lightViewFrustumBox) { if (shadowCamera->IsOrthographic()) { // Extrude the light space bounding box up to the far edge of the frustum's light space bounding box lightViewBox.max_.z_ = Max(lightViewBox.max_.z_, lightViewFrustumBox.max_.z_); return lightViewFrustum.IsInsideFast(lightViewBox) != OUTSIDE; } else { // If light is not directional, can do a simple check: if object is visible, its shadow is too if (drawable->IsInView(frame_)) return true; // For perspective lights, extrusion direction depends on the position of the shadow caster Vector3 center = lightViewBox.Center(); Ray extrusionRay(center, center); float extrusionDistance = shadowCamera->GetFarClip(); float originalDistance = Clamp(center.Length(), M_EPSILON, extrusionDistance); // Because of the perspective, the bounding box must also grow when it is extruded to the distance float sizeFactor = extrusionDistance / originalDistance; // Calculate the endpoint box and merge it to the original. Because it's axis-aligned, it will be larger // than necessary, so the test will be conservative Vector3 newCenter = extrusionDistance * extrusionRay.direction_; Vector3 newHalfSize = lightViewBox.Size() * sizeFactor * 0.5f; BoundingBox extrudedBox(newCenter - newHalfSize, newCenter + newHalfSize); lightViewBox.Merge(extrudedBox); return lightViewFrustum.IsInsideFast(lightViewBox) != OUTSIDE; } } IntRect View::GetShadowMapViewport(Light* light, int splitIndex, Texture2D* shadowMap) { int width = shadowMap->GetWidth(); int height = shadowMap->GetHeight(); switch (light->GetLightType()) { case LIGHT_DIRECTIONAL: { int numSplits = light->GetNumShadowSplits(); if (numSplits == 1) return {0, 0, width, height}; else if (numSplits == 2) return {splitIndex * width / 2, 0, (splitIndex + 1) * width / 2, height}; else return {(splitIndex & 1) * width / 2, (splitIndex / 2) * height / 2, ((splitIndex & 1) + 1) * width / 2, (splitIndex / 2 + 1) * height / 2}; } case LIGHT_SPOT: return {0, 0, width, height}; case LIGHT_POINT: return {(splitIndex & 1) * width / 2, (splitIndex / 2) * height / 3, ((splitIndex & 1) + 1) * width / 2, (splitIndex / 2 + 1) * height / 3}; } return {}; } void View::SetupShadowCameras(LightQueryResult& query) { Light* light = query.light_; i32 splits = 0; switch (light->GetLightType()) { case LIGHT_DIRECTIONAL: { const CascadeParameters& cascade = light->GetShadowCascade(); float nearSplit = cullCamera_->GetNearClip(); float farSplit; int numSplits = light->GetNumShadowSplits(); while (splits < numSplits) { // If split is completely beyond camera far clip, we are done if (nearSplit > cullCamera_->GetFarClip()) break; farSplit = Min(cullCamera_->GetFarClip(), cascade.splits_[splits]); if (farSplit <= nearSplit) break; // Setup the shadow camera for the split Camera* shadowCamera = renderer_->GetShadowCamera(); query.shadowCameras_[splits] = shadowCamera; query.shadowNearSplits_[splits] = nearSplit; query.shadowFarSplits_[splits] = farSplit; SetupDirLightShadowCamera(shadowCamera, light, nearSplit, farSplit); nearSplit = farSplit; ++splits; } } break; case LIGHT_SPOT: { Camera* shadowCamera = renderer_->GetShadowCamera(); query.shadowCameras_[0] = shadowCamera; Node* cameraNode = shadowCamera->GetNode(); Node* lightNode = light->GetNode(); cameraNode->SetTransform(lightNode->GetWorldPosition(), lightNode->GetWorldRotation()); shadowCamera->SetNearClip(light->GetShadowNearFarRatio() * light->GetRange()); shadowCamera->SetFarClip(light->GetRange()); shadowCamera->SetFov(light->GetFov()); shadowCamera->SetAspectRatio(light->GetAspectRatio()); splits = 1; } break; case LIGHT_POINT: { static const Vector3* directions[] = { &Vector3::RIGHT, &Vector3::LEFT, &Vector3::UP, &Vector3::DOWN, &Vector3::FORWARD, &Vector3::BACK }; for (i32 i = 0; i < MAX_CUBEMAP_FACES; ++i) { Camera* shadowCamera = renderer_->GetShadowCamera(); query.shadowCameras_[i] = shadowCamera; Node* cameraNode = shadowCamera->GetNode(); // When making a shadowed point light, align the splits along X, Y and Z axes regardless of light rotation cameraNode->SetPosition(light->GetNode()->GetWorldPosition()); cameraNode->SetDirection(*directions[i]); shadowCamera->SetNearClip(light->GetShadowNearFarRatio() * light->GetRange()); shadowCamera->SetFarClip(light->GetRange()); shadowCamera->SetFov(90.0f); shadowCamera->SetAspectRatio(1.0f); } splits = MAX_CUBEMAP_FACES; } break; } query.numSplits_ = splits; } void View::SetupDirLightShadowCamera(Camera* shadowCamera, Light* light, float nearSplit, float farSplit) { Node* shadowCameraNode = shadowCamera->GetNode(); Node* lightNode = light->GetNode(); float extrusionDistance = Min(cullCamera_->GetFarClip(), light->GetShadowMaxExtrusion()); const FocusParameters& parameters = light->GetShadowFocus(); // Calculate initial position & rotation Vector3 pos = cullCamera_->GetNode()->GetWorldPosition() - extrusionDistance * lightNode->GetWorldDirection(); shadowCameraNode->SetTransform(pos, lightNode->GetWorldRotation()); // Calculate main camera shadowed frustum in light's view space farSplit = Min(farSplit, cullCamera_->GetFarClip()); // Use the scene Z bounds to limit frustum size if applicable if (parameters.focus_) { nearSplit = Max(minZ_, nearSplit); farSplit = Min(maxZ_, farSplit); } Frustum splitFrustum = cullCamera_->GetSplitFrustum(nearSplit, farSplit); Polyhedron frustumVolume; frustumVolume.Define(splitFrustum); // If focusing enabled, clip the frustum volume by the combined bounding box of the lit geometries within the frustum if (parameters.focus_) { BoundingBox litGeometriesBox; unsigned lightMask = light->GetLightMask(); for (Drawable* drawable : geometries_) { if (drawable->GetMinZ() <= farSplit && drawable->GetMaxZ() >= nearSplit && (GetLightMask(drawable) & lightMask)) litGeometriesBox.Merge(drawable->GetWorldBoundingBox()); } if (litGeometriesBox.Defined()) { frustumVolume.Clip(litGeometriesBox); // If volume became empty, restore it to avoid zero size if (frustumVolume.Empty()) frustumVolume.Define(splitFrustum); } } // Transform frustum volume to light space const Matrix3x4& lightView = shadowCamera->GetView(); frustumVolume.Transform(lightView); // Fit the frustum volume inside a bounding box. If uniform size, use a sphere instead BoundingBox shadowBox; if (!parameters.nonUniform_) shadowBox.Define(Sphere(frustumVolume)); else shadowBox.Define(frustumVolume); shadowCamera->SetOrthographic(true); shadowCamera->SetAspectRatio(1.0f); shadowCamera->SetNearClip(0.0f); shadowCamera->SetFarClip(shadowBox.max_.z_); // Center shadow camera on the bounding box. Can not snap to texels yet as the shadow map viewport is unknown QuantizeDirLightShadowCamera(shadowCamera, light, IntRect(0, 0, 0, 0), shadowBox); } void View::FinalizeShadowCamera(Camera* shadowCamera, Light* light, const IntRect& shadowViewport, const BoundingBox& shadowCasterBox) { const FocusParameters& parameters = light->GetShadowFocus(); auto shadowMapWidth = (float)(shadowViewport.Width()); LightType type = light->GetLightType(); if (type == LIGHT_DIRECTIONAL) { BoundingBox shadowBox; shadowBox.max_.y_ = shadowCamera->GetOrthoSize() * 0.5f; shadowBox.max_.x_ = shadowCamera->GetAspectRatio() * shadowBox.max_.y_; shadowBox.min_.y_ = -shadowBox.max_.y_; shadowBox.min_.x_ = -shadowBox.max_.x_; // Requantize and snap to shadow map texels QuantizeDirLightShadowCamera(shadowCamera, light, shadowViewport, shadowBox); } if (type == LIGHT_SPOT && parameters.focus_) { float viewSizeX = Max(Abs(shadowCasterBox.min_.x_), Abs(shadowCasterBox.max_.x_)); float viewSizeY = Max(Abs(shadowCasterBox.min_.y_), Abs(shadowCasterBox.max_.y_)); float viewSize = Max(viewSizeX, viewSizeY); // Scale the quantization parameters, because view size is in projection space (-1.0 - 1.0) float invOrthoSize = 1.0f / shadowCamera->GetOrthoSize(); float quantize = parameters.quantize_ * invOrthoSize; float minView = parameters.minView_ * invOrthoSize; viewSize = Max(ceilf(viewSize / quantize) * quantize, minView); if (viewSize < 1.0f) shadowCamera->SetZoom(1.0f / viewSize); } // Perform a finalization step for all lights: ensure zoom out of 2 pixels to eliminate border filtering issues // For point lights use 4 pixels, as they must not cross sides of the virtual cube map (maximum 3x3 PCF) if (shadowCamera->GetZoom() >= 1.0f) { if (light->GetLightType() != LIGHT_POINT) shadowCamera->SetZoom(shadowCamera->GetZoom() * ((shadowMapWidth - 2.0f) / shadowMapWidth)); else { if (Graphics::GetGAPI() == GAPI_OPENGL) shadowCamera->SetZoom(shadowCamera->GetZoom() * ((shadowMapWidth - 3.0f) / shadowMapWidth)); else shadowCamera->SetZoom(shadowCamera->GetZoom() * ((shadowMapWidth - 4.0f) / shadowMapWidth)); } } } void View::QuantizeDirLightShadowCamera(Camera* shadowCamera, Light* light, const IntRect& shadowViewport, const BoundingBox& viewBox) { Node* shadowCameraNode = shadowCamera->GetNode(); const FocusParameters& parameters = light->GetShadowFocus(); auto shadowMapWidth = (float)(shadowViewport.Width()); float minX = viewBox.min_.x_; float minY = viewBox.min_.y_; float maxX = viewBox.max_.x_; float maxY = viewBox.max_.y_; Vector2 center((minX + maxX) * 0.5f, (minY + maxY) * 0.5f); Vector2 viewSize(maxX - minX, maxY - minY); // Quantize size to reduce swimming // Note: if size is uniform and there is no focusing, quantization is unnecessary if (parameters.nonUniform_) { viewSize.x_ = ceilf(sqrtf(viewSize.x_ / parameters.quantize_)); viewSize.y_ = ceilf(sqrtf(viewSize.y_ / parameters.quantize_)); viewSize.x_ = Max(viewSize.x_ * viewSize.x_ * parameters.quantize_, parameters.minView_); viewSize.y_ = Max(viewSize.y_ * viewSize.y_ * parameters.quantize_, parameters.minView_); } else if (parameters.focus_) { viewSize.x_ = Max(viewSize.x_, viewSize.y_); viewSize.x_ = ceilf(sqrtf(viewSize.x_ / parameters.quantize_)); viewSize.x_ = Max(viewSize.x_ * viewSize.x_ * parameters.quantize_, parameters.minView_); viewSize.y_ = viewSize.x_; } shadowCamera->SetOrthoSize(viewSize); // Center shadow camera to the view space bounding box Quaternion rot(shadowCameraNode->GetWorldRotation()); Vector3 adjust(center.x_, center.y_, 0.0f); shadowCameraNode->Translate(rot * adjust, TransformSpace::World); // If the shadow map viewport is known, snap to whole texels if (shadowMapWidth > 0.0f) { Vector3 viewPos(rot.Inverse() * shadowCameraNode->GetWorldPosition()); // Take into account that shadow map border will not be used float invActualSize = 1.0f / (shadowMapWidth - 2.0f); Vector2 texelSize(viewSize.x_ * invActualSize, viewSize.y_ * invActualSize); Vector3 snap(-fmodf(viewPos.x_, texelSize.x_), -fmodf(viewPos.y_, texelSize.y_), 0.0f); shadowCameraNode->Translate(rot * snap, TransformSpace::World); } } void View::FindZone(Drawable* drawable) { Vector3 center = drawable->GetWorldBoundingBox().Center(); int bestPriority = M_MIN_INT; Zone* newZone = nullptr; // If bounding box center is in view, the zone assignment is conclusive also for next frames. Otherwise it is temporary // (possibly incorrect) and must be re-evaluated on the next frame bool temporary = !cullCamera_->GetFrustum().IsInside(center); // First check if the current zone remains a conclusive result Zone* lastZone = drawable->GetZone(); if (lastZone && (lastZone->GetViewMask() & cullCamera_->GetViewMask()) && lastZone->GetPriority() >= highestZonePriority_ && (drawable->GetZoneMask() & lastZone->GetZoneMask()) && lastZone->IsInside(center)) newZone = lastZone; else { for (Vector::Iterator i = zones_.Begin(); i != zones_.End(); ++i) { Zone* zone = *i; int priority = zone->GetPriority(); if (priority > bestPriority && (drawable->GetZoneMask() & zone->GetZoneMask()) && zone->IsInside(center)) { newZone = zone; bestPriority = priority; } } } drawable->SetZone(newZone, temporary); } Technique* View::GetTechnique(Drawable* drawable, Material* material) { if (!material) return renderer_->GetDefaultMaterial()->GetTechniques()[0].technique_; const Vector& techniques = material->GetTechniques(); // If only one technique, no choice if (techniques.Size() == 1) return techniques[0].technique_; else { float lodDistance = drawable->GetLodDistance(); // Check for suitable technique. Techniques should be ordered like this: // Most distant & highest quality // Most distant & lowest quality // Second most distant & highest quality // ... for (const TechniqueEntry& entry : techniques) { Technique* tech = entry.technique_; if (!tech || (!tech->IsSupported()) || materialQuality_ < entry.qualityLevel_) continue; if (lodDistance >= entry.lodDistance_) return tech; } // If no suitable technique found, fallback to the last return techniques.Size() ? techniques.Back().technique_ : nullptr; } } void View::CheckMaterialForAuxView(Material* material) { const HashMap>& textures = material->GetTextures(); for (HashMap>::ConstIterator i = textures.Begin(); i != textures.End(); ++i) { Texture* texture = i->second_.Get(); if (texture && texture->GetUsage() == TEXTURE_RENDERTARGET) { // Have to check cube & 2D textures separately if (texture->GetType() == Texture2D::GetTypeStatic()) { auto* tex2D = static_cast(texture); RenderSurface* target = tex2D->GetRenderSurface(); if (target && target->GetUpdateMode() == SURFACE_UPDATEVISIBLE) target->QueueUpdate(); } else if (texture->GetType() == TextureCube::GetTypeStatic()) { auto* texCube = static_cast(texture); for (i32 j = 0; j < MAX_CUBEMAP_FACES; ++j) { RenderSurface* target = texCube->GetRenderSurface((CubeMapFace)j); if (target && target->GetUpdateMode() == SURFACE_UPDATEVISIBLE) target->QueueUpdate(); } } } } // Flag as processed so we can early-out next time we come across this material on the same frame material->MarkForAuxView(frame_.frameNumber_); } void View::SetQueueShaderDefines(BatchQueue& queue, const RenderPathCommand& command) { String vsDefines = command.vertexShaderDefines_.Trimmed(); String psDefines = command.pixelShaderDefines_.Trimmed(); if (vsDefines.Length() || psDefines.Length()) { queue.hasExtraDefines_ = true; queue.vsExtraDefines_ = vsDefines; queue.psExtraDefines_ = psDefines; queue.vsExtraDefinesHash_ = StringHash(vsDefines); queue.psExtraDefinesHash_ = StringHash(psDefines); } else queue.hasExtraDefines_ = false; } void View::AddBatchToQueue(BatchQueue& queue, Batch& batch, Technique* tech, bool allowInstancing, bool allowShadows) { if (!batch.material_) batch.material_ = renderer_->GetDefaultMaterial(); // Convert to instanced if possible if (allowInstancing && batch.geometryType_ == GEOM_STATIC && batch.geometry_->GetIndexBuffer()) batch.geometryType_ = GEOM_INSTANCED; if (batch.geometryType_ == GEOM_INSTANCED) { BatchGroupKey key(batch); HashMap::Iterator i = queue.batchGroups_.Find(key); if (i == queue.batchGroups_.End()) { // Create a new group based on the batch // In case the group remains below the instancing limit, do not enable instancing shaders yet BatchGroup newGroup(batch); newGroup.geometryType_ = GEOM_STATIC; renderer_->SetBatchShaders(newGroup, tech, allowShadows, queue); newGroup.CalculateSortKey(); i = queue.batchGroups_.Insert(MakePair(key, newGroup)); } int oldSize = i->second_.instances_.Size(); i->second_.AddTransforms(batch); // Convert to using instancing shaders when the instancing limit is reached if (oldSize < minInstances_ && (int)i->second_.instances_.Size() >= minInstances_) { i->second_.geometryType_ = GEOM_INSTANCED; renderer_->SetBatchShaders(i->second_, tech, allowShadows, queue); i->second_.CalculateSortKey(); } } else { renderer_->SetBatchShaders(batch, tech, allowShadows, queue); batch.CalculateSortKey(); // If batch is static with multiple world transforms and cannot instance, we must push copies of the batch individually if (batch.geometryType_ == GEOM_STATIC && batch.numWorldTransforms_ > 1) { i32 numTransforms = batch.numWorldTransforms_; batch.numWorldTransforms_ = 1; for (i32 i = 0; i < numTransforms; ++i) { // Move the transform pointer to generate copies of the batch which only refer to 1 world transform queue.batches_.Push(batch); ++batch.worldTransform_; } } else queue.batches_.Push(batch); } } void View::PrepareInstancingBuffer() { // Prepare instancing buffer from the source view /// \todo If rendering the same view several times back-to-back, would not need to refill the buffer if (sourceView_) { sourceView_->PrepareInstancingBuffer(); return; } URHO3D_PROFILE(PrepareInstancingBuffer); i32 totalInstances = 0; for (HashMap::Iterator i = batchQueues_.Begin(); i != batchQueues_.End(); ++i) totalInstances += i->second_.GetNumInstances(); for (Vector::Iterator i = lightQueues_.Begin(); i != lightQueues_.End(); ++i) { for (const ShadowBatchQueue& shadowSplit : i->shadowSplits_) totalInstances += shadowSplit.shadowBatches_.GetNumInstances(); totalInstances += i->litBaseBatches_.GetNumInstances(); totalInstances += i->litBatches_.GetNumInstances(); } if (!totalInstances || !renderer_->ResizeInstancingBuffer(totalInstances)) return; VertexBuffer* instancingBuffer = renderer_->GetInstancingBuffer(); i32 freeIndex = 0; void* dest = instancingBuffer->Lock(0, totalInstances, true); if (!dest) return; const i32 stride = instancingBuffer->GetVertexSize(); for (HashMap::Iterator i = batchQueues_.Begin(); i != batchQueues_.End(); ++i) i->second_.SetInstancingData(dest, stride, freeIndex); for (Vector::Iterator i = lightQueues_.Begin(); i != lightQueues_.End(); ++i) { for (ShadowBatchQueue& shadowSplit : i->shadowSplits_) shadowSplit.shadowBatches_.SetInstancingData(dest, stride, freeIndex); i->litBaseBatches_.SetInstancingData(dest, stride, freeIndex); i->litBatches_.SetInstancingData(dest, stride, freeIndex); } instancingBuffer->Unlock(); } void View::SetupLightVolumeBatch(Batch& batch) { Light* light = batch.lightQueue_->light_; LightType type = light->GetLightType(); Vector3 cameraPos = camera_->GetNode()->GetWorldPosition(); float lightDist; graphics_->SetBlendMode(light->IsNegative() ? BLEND_SUBTRACT : BLEND_ADD); graphics_->SetDepthBias(0.0f, 0.0f); graphics_->SetDepthWrite(false); graphics_->SetFillMode(FILL_SOLID); graphics_->SetLineAntiAlias(false); graphics_->SetClipPlane(false); if (type != LIGHT_DIRECTIONAL) { if (type == LIGHT_POINT) lightDist = Sphere(light->GetNode()->GetWorldPosition(), light->GetRange() * 1.25f).Distance(cameraPos); else lightDist = light->GetFrustum().Distance(cameraPos); // Draw front faces if not inside light volume if (lightDist < camera_->GetNearClip() * 2.0f) { renderer_->SetCullMode(CULL_CW, camera_); graphics_->SetDepthTest(CMP_GREATER); } else { renderer_->SetCullMode(CULL_CCW, camera_); graphics_->SetDepthTest(CMP_LESSEQUAL); } } else { // In case the same camera is used for multiple views with differing aspect ratios (not recommended) // refresh the directional light's model transform before rendering light->GetVolumeTransform(camera_); graphics_->SetCullMode(CULL_NONE); graphics_->SetDepthTest(CMP_ALWAYS); } graphics_->SetScissorTest(false); if (!noStencil_) graphics_->SetStencilTest(true, CMP_NOTEQUAL, OP_KEEP, OP_KEEP, OP_KEEP, 0, light->GetLightMask()); else graphics_->SetStencilTest(false); } bool View::NeedRenderShadowMap(const LightBatchQueue& queue) { // Must have a shadow map, and either forward or deferred lit batches return queue.shadowMap_ && (!queue.litBatches_.IsEmpty() || !queue.litBaseBatches_.IsEmpty() || !queue.volumeBatches_.Empty()); } void View::RenderShadowMap(const LightBatchQueue& queue) { URHO3D_PROFILE(RenderShadowMap); Texture2D* shadowMap = queue.shadowMap_; graphics_->SetTexture(TU_SHADOWMAP, nullptr); graphics_->SetFillMode(FILL_SOLID); graphics_->SetClipPlane(false); graphics_->SetStencilTest(false); // Set shadow depth bias BiasParameters parameters = queue.light_->GetShadowBias(); // The shadow map is a depth stencil texture if (shadowMap->GetUsage() == TEXTURE_DEPTHSTENCIL) { graphics_->SetColorWrite(false); graphics_->SetDepthStencil(shadowMap); graphics_->SetRenderTarget(0, shadowMap->GetRenderSurface()->GetLinkedRenderTarget()); // Disable other render targets for (i32 i = 1; i < MAX_RENDERTARGETS; ++i) graphics_->SetRenderTarget(i, (RenderSurface*) nullptr); graphics_->SetViewport(IntRect(0, 0, shadowMap->GetWidth(), shadowMap->GetHeight())); graphics_->Clear(CLEAR_DEPTH); } else // if the shadow map is a color rendertarget { graphics_->SetColorWrite(true); graphics_->SetRenderTarget(0, shadowMap); // Disable other render targets for (i32 i = 1; i < MAX_RENDERTARGETS; ++i) graphics_->SetRenderTarget(i, (RenderSurface*) nullptr); graphics_->SetDepthStencil(renderer_->GetDepthStencil(shadowMap->GetWidth(), shadowMap->GetHeight(), shadowMap->GetMultiSample(), shadowMap->GetAutoResolve())); graphics_->SetViewport(IntRect(0, 0, shadowMap->GetWidth(), shadowMap->GetHeight())); graphics_->Clear(CLEAR_DEPTH | CLEAR_COLOR, Color::WHITE); parameters = BiasParameters(0.0f, 0.0f); } // Render each of the splits for (i32 i = 0; i < queue.shadowSplits_.Size(); ++i) { const ShadowBatchQueue& shadowQueue = queue.shadowSplits_[i]; float multiplier = 1.0f; // For directional light cascade splits, adjust depth bias according to the far clip ratio of the splits if (i > 0 && queue.light_->GetLightType() == LIGHT_DIRECTIONAL) { multiplier = Max(shadowQueue.shadowCamera_->GetFarClip() / queue.shadowSplits_[0].shadowCamera_->GetFarClip(), 1.0f); multiplier = 1.0f + (multiplier - 1.0f) * queue.light_->GetShadowCascade().biasAutoAdjust_; // Quantize multiplier to prevent creation of too many rasterizer states on D3D11 multiplier = (int)(multiplier * 10.0f) / 10.0f; } // Perform further modification of depth bias on OpenGL ES, as shadow calculations' precision is limited float addition = 0.0f; #ifdef MOBILE_GRAPHICS multiplier *= renderer_->GetMobileShadowBiasMul(); addition = renderer_->GetMobileShadowBiasAdd(); #endif graphics_->SetDepthBias(multiplier * parameters.constantBias_ + addition, multiplier * parameters.slopeScaledBias_); if (!shadowQueue.shadowBatches_.IsEmpty()) { graphics_->SetViewport(shadowQueue.shadowViewport_); shadowQueue.shadowBatches_.Draw(this, shadowQueue.shadowCamera_, false, false, true); } } // Scale filter blur amount to shadow map viewport size so that different shadow map resolutions don't behave differently float blurScale = queue.shadowSplits_[0].shadowViewport_.Width() / 1024.0f; renderer_->ApplyShadowMapFilter(this, shadowMap, blurScale); // reset some parameters graphics_->SetColorWrite(true); graphics_->SetDepthBias(0.0f, 0.0f); } RenderSurface* View::GetDepthStencil(RenderSurface* renderTarget) { // If using the backbuffer, return the backbuffer depth-stencil if (!renderTarget) return nullptr; // Then check for linked depth-stencil RenderSurface* depthStencil = renderTarget->GetLinkedDepthStencil(); // Finally get one from Renderer if (!depthStencil) depthStencil = renderer_->GetDepthStencil(renderTarget->GetWidth(), renderTarget->GetHeight(), renderTarget->GetMultiSample(), renderTarget->GetAutoResolve()); return depthStencil; } RenderSurface* View::GetRenderSurfaceFromTexture(Texture* texture, CubeMapFace face) { if (!texture) return nullptr; if (texture->GetType() == Texture2D::GetTypeStatic()) return static_cast(texture)->GetRenderSurface(); else if (texture->GetType() == TextureCube::GetTypeStatic()) return static_cast(texture)->GetRenderSurface(face); else return nullptr; } void View::SendViewEvent(StringHash eventType) { using namespace BeginViewRender; VariantMap& eventData = GetEventDataMap(); eventData[P_VIEW] = this; eventData[P_SURFACE] = renderTarget_; eventData[P_TEXTURE] = (renderTarget_ ? renderTarget_->GetParentTexture() : nullptr); eventData[P_SCENE] = scene_; eventData[P_CAMERA] = cullCamera_; renderer_->SendEvent(eventType, eventData); } Texture* View::FindNamedTexture(const String& name, bool isRenderTarget, bool isVolumeMap) { // Check rendertargets first StringHash nameHash(name); if (renderTargets_.Contains(nameHash)) return renderTargets_[nameHash]; // Then the resource system auto* cache = GetSubsystem(); // Check existing resources first. This does not load resources, so we can afford to guess the resource type wrong // without having to rely on the file extension Texture* texture = cache->GetExistingResource(name); if (!texture) texture = cache->GetExistingResource(name); if (!texture) texture = cache->GetExistingResource(name); if (!texture) texture = cache->GetExistingResource(name); if (texture) return texture; // If not a rendertarget (which will never be loaded from a file), finally also try to load the texture // This will log an error if not found; the texture binding will be cleared in that case to not constantly spam the log if (!isRenderTarget) { if (GetExtension(name) == ".xml") { // Assume 3D textures are only bound to the volume map unit, otherwise it's a cube texture #ifdef DESKTOP_GRAPHICS_OR_GLES3 StringHash type = ParseTextureTypeXml(cache, name); if (!type && isVolumeMap) type = Texture3D::GetTypeStatic(); if (type == Texture3D::GetTypeStatic()) return cache->GetResource(name); else if (type == Texture2DArray::GetTypeStatic()) return cache->GetResource(name); else #endif return cache->GetResource(name); } else return cache->GetResource(name); } return nullptr; } }