public void ApplyDrawParameters(RenderDrawContext context, ParameterCollection parameters, FastListStruct <LightDynamicEntry> currentLights, ref BoundingBoxExt boundingBox)
            {
                var boundingBoxCasted = (BoundingBox)boundingBox;
                int lightIndex        = 0;

                for (int i = 0; i < currentLights.Count; ++i)
                {
                    LightDynamicEntry lightEntry     = currentLights[i];
                    LightComponent    lightComponent = lightEntry.Light;

                    if (lightComponent.BoundingBox.Intersects(ref boundingBoxCasted))
                    {
                        var spotLight = (LightSpot)lightComponent.Type;

                        /*
                         * // TODO: Just save the shaderdata struct directly within "LightDynamicEntry"?
                         * var singleLightData = (LightSpotTextureProjectionShaderData)lightEntry.ShadowMapTexture.ShaderData;   // TODO: This must not depend on the shadow map texture!
                         *
                         * worldToTextureUV[lightIndex] = singleLightData.WorldToTextureUV;
                         * projectionTextureMipMapLevels[lightIndex] = singleLightData.ProjectiveTextureMipMapLevel;
                         * projectiveTexture = singleLightData.ProjectiveTexture;
                         */

                        // TODO: Move this to "Collect()" and use "LightSpotTextureProjectionShaderData", but IDK how!
                        worldToTextureUV[lightIndex]       = ComputeWorldToTextureUVMatrix(lightComponent);
                        projectorPlaneMatrices[lightIndex] = ComputeProjectorPlaneMatrix(lightComponent);

                        // We use the maximum number of mips instead of the actual number,
                        // so things like video textures behave more consistently when changing the number of mip maps to generate.
                        int   maxMipMapCount = Texture.CountMips(lightParameters.ProjectionTexture.Width, lightParameters.ProjectionTexture.Height);
                        float projectiveTextureMipMapLevel = (float)(maxMipMapCount - 1) * spotLight.MipMapScale; // "- 1" because the lowest mip level is 0, not 1.
                        projectionTextureMipMapLevels[lightIndex] = projectiveTextureMipMapLevel;
                        transitionAreas[lightIndex] = Math.Max(spotLight.TransitionArea, 0.001f);                 // Keep the value just above zero. This is to prevent some issues with the "smoothstep()" function on OpenGL and OpenGL ES.

                        ++lightIndex;
                    }
                }

                // TODO: Why is this set if it's already in the collection?
                // TODO: Does this get set once per group or something?
                parameters.Set(projectiveTextureKey, lightParameters.ProjectionTexture);
                parameters.Set(uvScale, lightParameters.UVScale);
                parameters.Set(uvOffset, lightParameters.UVOffset);
                parameters.Set(worldToProjectiveTextureUVsKey, worldToTextureUV);
                parameters.Set(projectorPlaneMatricesKey, projectorPlaneMatrices);
                parameters.Set(projectionTextureMipMapLevelsKey, projectionTextureMipMapLevels);
                parameters.Set(transitionAreasKey, transitionAreas);
            }
        public override void ProcessLights(ProcessLightsParameters parameters)
        {
            if (parameters.LightCollection.Count == 0)
            {
                return;
            }

            // Check if we have a fallback renderer next in the chain, in case we don't need shadows
            bool hasNextRenderer = parameters.RendererIndex < (parameters.Renderers.Length - 1);

            var currentGroupParameters = SpotLightGroupParameters.Null;

            // Start by filtering/sorting what can be processed
            shadowComparer.ShadowMapTexturesPerLight = parameters.ShadowMapTexturesPerLight;
            shadowComparer.Lights = parameters.LightCollection;
            parameters.LightIndices.Sort(0, parameters.LightIndices.Count, shadowComparer);

            // Loop over the number of lights + 1 where the last iteration will always flush the last batch of lights
            for (int j = 0; j < parameters.LightIndices.Count + 1;)
            {
                // TODO: Eventually move this loop to a separate function that returns a structure.

                // These variables will contain the relevant parameters of the next usable light:
                var nextGroupParameters = SpotLightGroupParameters.Null;
                LightShadowMapTexture nextShadowTexture = null;
                RenderLight           nextLight         = null;

                // Find the next light whose attributes aren't null:
                if (j < parameters.LightIndices.Count)
                {
                    nextLight = parameters.LightCollection[parameters.LightIndices[j]];

                    if (nextLight.Type is LightSpot spotLight)
                    {
                        if (spotLight.ProjectiveTexture != null) // TODO: Remove this branch?!
                        {
                            nextGroupParameters.SpotParameters.ProjectionTexture = spotLight.ProjectiveTexture;
                            nextGroupParameters.SpotParameters.FlipMode          = spotLight.FlipMode;
                            nextGroupParameters.SpotParameters.UVScale           = spotLight.UVScale;
                            nextGroupParameters.SpotParameters.UVOffset          = spotLight.UVOffset;
                        }
                    }

                    if (parameters.ShadowMapRenderer != null &&
                        parameters.ShadowMapTexturesPerLight.TryGetValue(nextLight, out nextShadowTexture) &&
                        nextShadowTexture.Atlas != null)    // atlas could not be allocated? treat it as a non-shadowed texture
                    {
                        nextGroupParameters.ShadowType     = nextShadowTexture.ShadowType;
                        nextGroupParameters.ShadowRenderer = nextShadowTexture.Renderer;
                    }
                }

                // Flush current group
                // If we detect that the previous light's attributes don't match the next one's, create a new group (or add to an existing one that has the same attributes):
                if (j == parameters.LightIndices.Count || !currentGroupParameters.Equals(ref nextGroupParameters))
                {
                    if (processedLights.Count > 0)
                    {
                        ITextureProjectionRenderer currentTextureProjectionRenderer = FindOrCreateTextureProjectionRenderer(currentGroupParameters);

                        var lightGroupKey = new LightGroupKey(currentGroupParameters.ShadowRenderer, currentGroupParameters.ShadowType, currentTextureProjectionRenderer);
                        LightShaderGroupDynamic lightShaderGroup = FindOrCreateLightShaderGroup(lightGroupKey, parameters);

                        // Add view and lights to the current group:
                        var allowedLightCount = lightShaderGroup.AddView(parameters.ViewIndex, parameters.View, processedLights.Count);
                        for (int i = 0; i < allowedLightCount; ++i)
                        {
                            LightDynamicEntry light = processedLights[i];
                            lightShaderGroup.AddLight(light.Light, light.ShadowMapTexture);
                        }

                        // TODO: assign extra lights to non-shadow rendering if possible
                        //for (int i = lightCount; i < processedLights.Count; ++i)
                        //    XXX.AddLight(processedLights[i], null);

                        // Add the current light shader group to the collection if it hasn't already been added:
                        var lightShaderGroupEntry = new LightShaderGroupEntry <LightGroupKey>(lightGroupKey, lightShaderGroup);
                        if (!lightShaderGroups.Contains(lightShaderGroupEntry))
                        {
                            lightShaderGroups.Add(lightShaderGroupEntry);
                        }

                        processedLights.Clear();
                    }

                    // Start next group
                    currentGroupParameters = nextGroupParameters;
                }

                if (j < parameters.LightIndices.Count)
                {
                    // Do we need to process non shadowing lights or defer it to something else?
                    if (nextShadowTexture == null && hasNextRenderer)
                    {
                        // Break out so the remaining lights can be handled by the next renderer
                        break;
                    }

                    parameters.LightIndices.RemoveAt(j);
                    processedLights.Add(new LightDynamicEntry(nextLight, nextShadowTexture));
                }
                else
                {
                    j++;
                }
            }

            processedLights.Clear();
        }