/// <inheritdoc/>
        public override void Render(IList <SceneNode> nodes, RenderContext context, RenderOrder order)
        {
            if (nodes == null)
            {
                throw new ArgumentNullException("nodes");
            }
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if (order != RenderOrder.UserDefined)
            {
                throw new NotImplementedException("Render order must be 'UserDefined'.");
            }
            if (context.CameraNode == null)
            {
                throw new GraphicsException("Camera node needs to be set in render context.");
            }
            if (context.GBuffer0 == null)
            {
                throw new GraphicsException("GBuffer0 needs to be set in render context.");
            }

            int numberOfNodes = nodes.Count;

            if (numberOfNodes == 0)
            {
                return;
            }

            var   graphicsService  = context.GraphicsService;
            var   graphicsDevice   = graphicsService.GraphicsDevice;
            var   viewport         = context.Viewport;
            int   width            = viewport.Width;
            int   height           = viewport.Height;
            var   renderTargetPool = graphicsService.RenderTargetPool;
            var   cameraNode       = context.CameraNode;
            var   projection       = cameraNode.Camera.Projection;
            Pose  view             = cameraNode.PoseWorld.Inverse;
            Pose  cameraPose       = cameraNode.PoseWorld;
            float near             = projection.Near;
            float far = projection.Far;

            int frame = context.Frame;

            cameraNode.LastFrame = frame;

            // Save render state.
            var originalRasterizerState   = graphicsDevice.RasterizerState;
            var originalDepthStencilState = graphicsDevice.DepthStencilState;
            var originalBlendState        = graphicsDevice.BlendState;

            graphicsDevice.RasterizerState   = RasterizerState.CullNone;
            graphicsDevice.DepthStencilState = DepthStencilState.None;

            RenderTarget2D offscreenBuffer = null;
            Texture        depthBufferHalf = null;

            if (!EnableOffscreenRendering || context.RenderTarget == null)
            {
                graphicsDevice.BlendState = BlendState.AlphaBlend;
                _parameterGBuffer0.SetValue(context.GBuffer0);
            }
            else
            {
                // Render at half resolution into off-screen buffer.
                width  = Math.Max(1, width / 2);
                height = Math.Max(1, height / 2);

                graphicsDevice.BlendState = BlendStateOffscreen;

                offscreenBuffer = renderTargetPool.Obtain2D(
                    new RenderTargetFormat(width, height, false, context.RenderTarget.Format, DepthFormat.None));
                graphicsDevice.SetRenderTarget(offscreenBuffer);
                graphicsDevice.Clear(Color.Black);

                // Get half-res depth buffer.
                object obj;
                if (context.Data.TryGetValue(RenderContextKeys.DepthBufferHalf, out obj) &&
                    obj is Texture2D)
                {
                    depthBufferHalf = (Texture2D)obj;
                    _parameterGBuffer0.SetValue(depthBufferHalf);
                }
                else
                {
                    string message = "Downsampled depth buffer is not set in render context. (The downsampled "
                                     + "depth buffer (half width and height) is required by the VolumetricLightRenderer "
                                     + "to use half-res off-screen rendering. It needs to be stored in "
                                     + "RenderContext.Data[RenderContextKeys.DepthBufferHalf].)";
                    throw new GraphicsException(message);
                }
            }

            // Set global effect parameters.
            _parameterViewportSize.SetValue(new Vector2(width, height));

            var isHdrEnabled = context.RenderTarget != null && context.RenderTarget.Format == SurfaceFormat.HdrBlendable;

            for (int i = 0; i < numberOfNodes; i++)
            {
                var node = nodes[i] as VolumetricLightNode;
                if (node == null)
                {
                    continue;
                }

                // VolumetricLightNode is visible in current frame.
                node.LastFrame = frame;

                // Effect parameters for volumetric light properties.
                _parameterColor.SetValue((Vector3)node.Color / node.NumberOfSamples);
                _parameterNumberOfSamples.SetValue(node.NumberOfSamples);
                _parameterLightTextureMipMap.SetValue((float)node.MipMapBias);

                // The volumetric light effect is created for the parent light node.
                var lightNode = node.Parent as LightNode;
                if (lightNode == null)
                {
                    continue;
                }

                Pose lightPose = lightNode.PoseWorld;

                // Get start and end depth values of light AABB in view space.
                var lightAabbView = lightNode.Shape.GetAabb(lightNode.ScaleWorld, view * lightPose);
                var startZ        = Math.Max(-lightAabbView.Maximum.Z, near) / far;
                var endZ          = Math.Min(-lightAabbView.Minimum.Z / far, 1);
                _parameterDepthInterval.SetValue(new Vector2(startZ, endZ));

                // Get a rectangle that covers the light in screen space.
                var rectangle           = GraphicsHelper.GetScissorRectangle(cameraNode, new Viewport(0, 0, width, height), lightNode);
                var texCoordTopLeft     = new Vector2F(rectangle.Left / (float)width, rectangle.Top / (float)height);
                var texCoordBottomRight = new Vector2F(rectangle.Right / (float)width, rectangle.Bottom / (float)height);

                GraphicsHelper.GetFrustumFarCorners(cameraNode.Camera.Projection, texCoordTopLeft, texCoordBottomRight, _frustumFarCorners);

                // Convert frustum far corners from view space to world space.
                for (int j = 0; j < _frustumFarCorners.Length; j++)
                {
                    _frustumFarCorners[j] = (Vector3)cameraPose.ToWorldDirection((Vector3F)_frustumFarCorners[j]);
                }

                _parameterFrustumCorners.SetValue(_frustumFarCorners);

                Vector2 randomSeed = AnimateNoise ? new Vector2((float)MathHelper.Frac(context.Time.TotalSeconds))
                                          : new Vector2(0);
                _parameterRandomSeed.SetValue(randomSeed);

                // Set light parameters and apply effect pass.
                if (lightNode.Light is PointLight)
                {
                    var light = (PointLight)lightNode.Light;

                    float hdrScale = isHdrEnabled ? light.HdrScale : 1;
                    _parameterLightDiffuse.SetValue((Vector3)light.Color * light.DiffuseIntensity * hdrScale);
                    _parameterLightPosition.SetValue((Vector3)(lightPose.Position - cameraPose.Position));
                    _parameterLightRange.SetValue(light.Range);
                    _parameterLightAttenuation.SetValue(light.Attenuation);

                    bool hasTexture = (light.Texture != null);
                    if (hasTexture)
                    {
                        _parameterLightTexture.SetValue(light.Texture);

                        // Cube maps are left handed --> Sample with inverted z. (Otherwise, the
                        // cube map and objects or texts in it are mirrored.)
                        var mirrorZ = Matrix44F.CreateScale(1, 1, -1);
                        _parameterLightTextureMatrix.SetValue((Matrix)(mirrorZ * lightPose.Inverse));
                    }

                    if (hasTexture)
                    {
                        if (light.Texture.Format == SurfaceFormat.Alpha8)
                        {
                            _passPointLightTextureAlpha.Apply();
                        }
                        else
                        {
                            _passPointLightTextureRgb.Apply();
                        }
                    }
                    else
                    {
                        _passPointLight.Apply();
                    }
                }
                else if (lightNode.Light is Spotlight)
                {
                    var light = (Spotlight)lightNode.Light;

                    float hdrScale = isHdrEnabled ? light.HdrScale : 1;
                    _parameterLightDiffuse.SetValue((Vector3)light.Color * light.DiffuseIntensity * hdrScale);
                    _parameterLightPosition.SetValue((Vector3)(lightPose.Position - cameraPose.Position));
                    _parameterLightRange.SetValue(light.Range);
                    _parameterLightAttenuation.SetValue(light.Attenuation);
                    _parameterLightDirection.SetValue((Vector3)lightPose.ToWorldDirection(Vector3F.Forward));
                    _parameterLightAngles.SetValue(new Vector2(light.FalloffAngle, light.CutoffAngle));

                    bool hasTexture = (light.Texture != null);
                    if (hasTexture)
                    {
                        _parameterLightTexture.SetValue(light.Texture);

                        var proj = Matrix44F.CreatePerspectiveFieldOfView(light.CutoffAngle * 2, 1, 0.1f, 100);
                        _parameterLightTextureMatrix.SetValue((Matrix)(GraphicsHelper.ProjectorBiasMatrix * proj * (lightPose.Inverse * new Pose(cameraPose.Position))));
                    }

                    if (hasTexture)
                    {
                        if (light.Texture.Format == SurfaceFormat.Alpha8)
                        {
                            _passSpotlightTextureAlpha.Apply();
                        }
                        else
                        {
                            _passSpotlightTextureRgb.Apply();
                        }
                    }
                    else
                    {
                        _passSpotlight.Apply();
                    }
                }
                else if (lightNode.Light is ProjectorLight)
                {
                    var light = (ProjectorLight)lightNode.Light;

                    float hdrScale = isHdrEnabled ? light.HdrScale : 1;
                    _parameterLightDiffuse.SetValue((Vector3)light.Color * light.DiffuseIntensity * hdrScale);
                    _parameterLightPosition.SetValue((Vector3)(lightPose.Position - cameraPose.Position));
                    _parameterLightRange.SetValue(light.Projection.Far);
                    _parameterLightAttenuation.SetValue(light.Attenuation);

                    _parameterLightTexture.SetValue(light.Texture);

                    _parameterLightTextureMatrix.SetValue((Matrix)(GraphicsHelper.ProjectorBiasMatrix * light.Projection * (lightPose.Inverse * new Pose(cameraPose.Position))));

                    if (light.Texture.Format == SurfaceFormat.Alpha8)
                    {
                        _passProjectorLightTextureAlpha.Apply();
                    }
                    else
                    {
                        _passProjectorLightTextureRgb.Apply();
                    }
                }
                else
                {
                    continue;
                }

                // Draw a screen space quad covering the light.
                graphicsDevice.DrawQuad(rectangle);
            }

            _parameterGBuffer0.SetValue((Texture)null);
            _parameterLightTexture.SetValue((Texture)null);

            if (offscreenBuffer != null)
            {
                // ----- Combine off-screen buffer with scene.
                graphicsDevice.BlendState = BlendState.Opaque;

                // The previous scene render target is bound as texture.
                // --> Switch scene render targets!
                var sceneRenderTarget = context.RenderTarget;
                var renderTarget      = renderTargetPool.Obtain2D(new RenderTargetFormat(sceneRenderTarget));
                context.SourceTexture = offscreenBuffer;
                context.RenderTarget  = renderTarget;

                // Use the UpsampleFilter, which supports "nearest-depth upsampling".
                // (Nearest-depth upsampling is an "edge-aware" method that tries to
                // maintain the original geometry and prevent blurred edges.)
                if (_upsampleFilter == null)
                {
                    _upsampleFilter                = new UpsampleFilter(graphicsService);
                    _upsampleFilter.Mode           = UpsamplingMode.NearestDepth;
                    _upsampleFilter.RebuildZBuffer = true;
                }

                _upsampleFilter.DepthThreshold = DepthThreshold;
                context.SceneTexture           = sceneRenderTarget;

                _upsampleFilter.Process(context);

                context.SceneTexture  = null;
                context.SourceTexture = null;
                renderTargetPool.Recycle(offscreenBuffer);
                renderTargetPool.Recycle(sceneRenderTarget);
            }

            // Restore render states.
            graphicsDevice.RasterizerState   = originalRasterizerState;
            graphicsDevice.DepthStencilState = originalDepthStencilState;
            graphicsDevice.BlendState        = originalBlendState;
        }
        public override void Render(IList <SceneNode> nodes, RenderContext context, RenderOrder order)
        {
            if (nodes == null)
            {
                throw new ArgumentNullException("nodes");
            }
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }

            int numberOfNodes = nodes.Count;

            if (numberOfNodes == 0)
            {
                return;
            }

            context.Validate(_effect);
            context.ThrowIfCameraMissing();

            var graphicsDevice   = _effect.GraphicsDevice;
            var savedRenderState = new RenderStateSnapshot(graphicsDevice);

            graphicsDevice.DepthStencilState = DepthStencilState.None;
            graphicsDevice.RasterizerState   = RasterizerState.CullNone;
            graphicsDevice.BlendState        = GraphicsHelper.BlendStateAdd;

            var viewport = graphicsDevice.Viewport;

            _parameterViewportSize.SetValue(new Vector2(viewport.Width, viewport.Height));
            _parameterGBuffer0.SetValue(context.GBuffer0);
            _parameterGBuffer1.SetValue(context.GBuffer1);

            var    cameraNode     = context.CameraNode;
            var    cameraPose     = cameraNode.PoseWorld;
            Matrix viewProjection = (Matrix)cameraNode.View * cameraNode.Camera.Projection;

            // Update SceneNode.LastFrame for all visible nodes.
            int frame = context.Frame;

            cameraNode.LastFrame = frame;

            var isHdrEnabled = context.IsHdrEnabled();

            for (int i = 0; i < numberOfNodes; i++)
            {
                var lightNode = nodes[i] as LightNode;
                if (lightNode == null)
                {
                    continue;
                }

                var light = lightNode.Light as Spotlight;
                if (light == null)
                {
                    continue;
                }

                // LightNode is visible in current frame.
                lightNode.LastFrame = frame;

                float hdrScale = isHdrEnabled ? light.HdrScale : 1;
                _parameterDiffuseColor.SetValue((Vector3)light.Color * light.DiffuseIntensity * hdrScale);
                _parameterSpecularColor.SetValue((Vector3)light.Color * light.SpecularIntensity * hdrScale);

                var lightPose = lightNode.PoseWorld;
                _parameterPosition.SetValue((Vector3)(lightPose.Position - cameraPose.Position));

                var lightDirectionWorld = lightPose.ToWorldDirection(Vector3F.Forward);
                _parameterLightDirection.SetValue((Vector3)lightDirectionWorld);

                _parameterRange.SetValue(light.Range);
                _parameterAttenuation.SetValue(light.Attenuation);
                _parameterCutoffAngle.SetValue(light.CutoffAngle);
                _parameterFalloffAngle.SetValue(Math.Min(light.FalloffAngle, light.CutoffAngle));

                bool hasShadow = (lightNode.Shadow != null && lightNode.Shadow.ShadowMask != null);
                if (hasShadow)
                {
                    switch (lightNode.Shadow.ShadowMaskChannel)
                    {
                    case 0: _parameterShadowMaskChannel.SetValue(new Vector4(1, 0, 0, 0)); break;

                    case 1: _parameterShadowMaskChannel.SetValue(new Vector4(0, 1, 0, 0)); break;

                    case 2: _parameterShadowMaskChannel.SetValue(new Vector4(0, 0, 1, 0)); break;

                    default: _parameterShadowMaskChannel.SetValue(new Vector4(0, 0, 0, 1)); break;
                    }

                    _parameterShadowMask.SetValue(lightNode.Shadow.ShadowMask);
                }

                bool hasTexture = (light.Texture != null);
                if (hasTexture)
                {
                    var projection = Matrix44F.CreatePerspectiveFieldOfView(light.CutoffAngle * 2, 1, 0.1f, 100);
                    _parameterTextureMatrix.SetValue((Matrix)(GraphicsHelper.ProjectorBiasMatrix * projection * (lightPose.Inverse * new Pose(cameraPose.Position))));
                    _parameterTexture.SetValue(light.Texture);
                }

                var rectangle           = GraphicsHelper.GetViewportRectangle(cameraNode, viewport, lightNode);
                var texCoordTopLeft     = new Vector2F(rectangle.Left / (float)viewport.Width, rectangle.Top / (float)viewport.Height);
                var texCoordBottomRight = new Vector2F(rectangle.Right / (float)viewport.Width, rectangle.Bottom / (float)viewport.Height);
                GraphicsHelper.GetFrustumFarCorners(cameraNode.Camera.Projection, texCoordTopLeft, texCoordBottomRight, _frustumFarCorners);

                // Convert frustum far corners from view space to world space.
                for (int j = 0; j < _frustumFarCorners.Length; j++)
                {
                    _frustumFarCorners[j] = (Vector3)cameraPose.ToWorldDirection((Vector3F)_frustumFarCorners[j]);
                }
                _parameterFrustumCorners.SetValue(_frustumFarCorners);

                if (lightNode.Clip != null)
                {
                    var data = lightNode.RenderData as LightRenderData;
                    if (data == null)
                    {
                        data = new LightRenderData();
                        lightNode.RenderData = data;
                    }

                    data.UpdateClipSubmesh(context.GraphicsService, lightNode);

                    graphicsDevice.DepthStencilState = GraphicsHelper.DepthStencilStateOnePassStencilFail;
                    graphicsDevice.BlendState        = GraphicsHelper.BlendStateNoColorWrite;

                    _parameterWorldViewProjection.SetValue((Matrix)data.ClipMatrix * viewProjection);
                    _passClip.Apply();
                    data.ClipSubmesh.Draw();

                    graphicsDevice.DepthStencilState = lightNode.InvertClip
            ? GraphicsHelper.DepthStencilStateStencilEqual0
            : GraphicsHelper.DepthStencilStateStencilNotEqual0;
                    graphicsDevice.BlendState = GraphicsHelper.BlendStateAdd;
                }
                else
                {
                    graphicsDevice.DepthStencilState = DepthStencilState.None;
                }

                if (hasShadow)
                {
                    if (hasTexture)
                    {
                        if (light.Texture.Format == SurfaceFormat.Alpha8)
                        {
                            _passShadowedTexturedAlpha.Apply();
                        }
                        else
                        {
                            _passShadowedTexturedRgb.Apply();
                        }
                    }
                    else
                    {
                        _passShadowed.Apply();
                    }
                }
                else
                {
                    if (hasTexture)
                    {
                        if (light.Texture.Format == SurfaceFormat.Alpha8)
                        {
                            _passTexturedAlpha.Apply();
                        }
                        else
                        {
                            _passTexturedRgb.Apply();
                        }
                    }
                    else
                    {
                        _passDefault.Apply();
                    }
                }

                graphicsDevice.DrawQuad(rectangle);
            }

            savedRenderState.Restore();
        }