예제 #1
0
        static void InitNullTexture()
        {
            // Depth textures use HDR values
            var texture = TextureArrayHelpers.CreateTexture2D(s_nullColor, UnityEngine.TextureFormat.RGB9e5Float);

            s_nullTexture2DArray      = TextureArrayHelpers.CreateTexture2DArray(texture);
            s_nullTexture2DArray.name = "Sea Floor Depth Null Texture";
        }
예제 #2
0
        protected override void InitData()
        {
            base.InitData();

            int resolution = OceanRenderer.Instance.LodDataResolution;
            var desc       = new RenderTextureDescriptor(resolution, resolution, TextureFormat, 0);

            _sources = CreateLodDataTextures(desc, SimName + "_1", NeedToReadWriteTextureData);

            TextureArrayHelpers.ClearToBlack(_sources);
            TextureArrayHelpers.ClearToBlack(_targets);
        }
예제 #3
0
        static void InitNullTexture()
        {
            var texture = Instantiate <Texture2D>(Texture2D.whiteTexture);
            // Null texture needs to be white (uses R channel) with a 1000 intensity.
            var color = new Color(1000, 1000, 1000, 1);

            Color[] pixels = Enumerable.Repeat(color, texture.height * texture.width).ToArray();
            texture.SetPixels(pixels);
            texture.Apply();
            s_nullTexture2DArray      = TextureArrayHelpers.CreateTexture2DArray(texture);
            s_nullTexture2DArray.name = "Sea Floor Depth Null Texture";
        }
 /// <summary>
 /// May happen if scenes change etc
 /// </summary>
 void ClearBufferIfLightChanged()
 {
     if (_mainLight != OceanRenderer.Instance._primaryLight)
     {
         if (_mainLight)
         {
             BufCopyShadowMap = null;
             TextureArrayHelpers.ClearToBlack(_sources);
             TextureArrayHelpers.ClearToBlack(_targets);
         }
         _mainLight = null;
     }
 }
        static void InitNullTexture()
        {
            var texture = Instantiate <Texture2D>(Texture2D.whiteTexture);
            // Null texture needs to be white (uses R channel) with a 1000 intensity.
            var color = new Color(1000, 1000, 1000, 1);

            Color[] pixels = new Color[texture.height * texture.width];
            for (int i = 0; i < pixels.Length; i++)
            {
                pixels[i] = color;
            }
            texture.SetPixels(pixels);
            texture.Apply();
            s_nullTexture2DArray      = TextureArrayHelpers.CreateTexture2DArray(texture);
            s_nullTexture2DArray.name = "Sea Floor Depth Null Texture";
        }
예제 #6
0
        public override void UpdateLodData()
        {
            base.UpdateLodData();

            if (_mainLight != OceanRenderer.Instance._primaryLight)
            {
                if (_mainLight)
                {
                    _mainLight.RemoveCommandBuffer(LightEvent.BeforeScreenspaceMask, _bufCopyShadowMap);
                    _bufCopyShadowMap = null;
                    TextureArrayHelpers.ClearToBlack(_sources);
                    TextureArrayHelpers.ClearToBlack(_targets);
                }
                _mainLight = null;
            }

            if (!OceanRenderer.Instance._primaryLight)
            {
                if (!Settings._allowNullLight)
                {
                    Debug.LogWarning("Primary light must be specified on OceanRenderer script to enable shadows.", this);
                }
                return;
            }

            if (!_mainLight)
            {
                if (!StartInitLight())
                {
                    enabled = false;
                    return;
                }
            }

            if (_bufCopyShadowMap == null && s_processData)
            {
                _bufCopyShadowMap      = new CommandBuffer();
                _bufCopyShadowMap.name = "Shadow data";
                _mainLight.AddCommandBuffer(LightEvent.BeforeScreenspaceMask, _bufCopyShadowMap);
            }
            else if (!s_processData && _bufCopyShadowMap != null)
            {
                _mainLight.RemoveCommandBuffer(LightEvent.BeforeScreenspaceMask, _bufCopyShadowMap);
                _bufCopyShadowMap = null;
            }

            if (!s_processData)
            {
                return;
            }


            var lodCount = OceanRenderer.Instance.CurrentLodCount;

            SwapRTs(ref _sources, ref _targets);

            _bufCopyShadowMap.Clear();

            var lt = OceanRenderer.Instance._lodTransform;

            ValidateSourceData();
            // clear the shadow collection. it will be overwritten with shadow values IF the shadows render,
            // which only happens if there are (nontransparent) shadow receivers around
            TextureArrayHelpers.ClearToBlack(_targets);
            for (var lodIdx = OceanRenderer.Instance.CurrentLodCount - 1; lodIdx >= 0; lodIdx--)
            {
                _renderProperties.Initialise(_bufCopyShadowMap, _updateShadowShader, krnl_UpdateShadow);


                lt._renderData[lodIdx].Validate(0, this);
                _renderProperties.SetVector(sp_CenterPos, lt._renderData[lodIdx]._posSnapped);
                _renderProperties.SetVector(sp_Scale, lt.GetLodTransform(lodIdx).lossyScale);
                _renderProperties.SetVector(sp_CamPos, OceanRenderer.Instance.Viewpoint.position);
                _renderProperties.SetVector(sp_CamForward, OceanRenderer.Instance.Viewpoint.forward);
                _renderProperties.SetVector(sp_JitterDiameters_CurrentFrameWeights, new Vector4(Settings._jitterDiameterSoft, Settings._jitterDiameterHard, Settings._currentFrameWeightSoft, Settings._currentFrameWeightHard));
                _renderProperties.SetMatrix(sp_MainCameraProjectionMatrix, _cameraMain.projectionMatrix * _cameraMain.worldToCameraMatrix);
                _renderProperties.SetFloat(sp_SimDeltaTime, Time.deltaTime);

                // compute which lod data we are sampling previous frame shadows from. if a scale change has happened this can be any lod up or down the chain.
                var srcDataIdx = lodIdx + ScaleDifferencePow2;
                srcDataIdx = Mathf.Clamp(srcDataIdx, 0, lt.LodCount - 1);
                _renderProperties.SetFloat(OceanRenderer.sp_LD_SliceIndex, lodIdx);
                _renderProperties.SetFloat(sp_LD_SliceIndex_Source, srcDataIdx);
                BindSourceData(_renderProperties, false);
                _renderProperties.SetTexture(
                    sp_LD_TexArray_Target,
                    _targets
                    );
                _renderProperties.DispatchShader();
            }
        }
예제 #7
0
        public override void UpdateLodData()
        {
            if (!enabled)
            {
                return;
            }

            base.UpdateLodData();

            if (_mainLight != OceanRenderer.Instance._primaryLight)
            {
                if (_mainLight)
                {
                    _mainLight.RemoveCommandBuffer(LightEvent.BeforeScreenspaceMask, BufCopyShadowMap);
                    BufCopyShadowMap = null;
                    TextureArrayHelpers.ClearToBlack(_sources);
                    TextureArrayHelpers.ClearToBlack(_targets);
                }
                _mainLight = null;
            }

            if (!OceanRenderer.Instance._primaryLight)
            {
                if (!Settings._allowNullLight)
                {
                    Debug.LogWarning("Primary light must be specified on OceanRenderer script to enable shadows.", OceanRenderer.Instance);
                }
                return;
            }

            if (!_mainLight)
            {
                if (!StartInitLight())
                {
                    enabled = false;
                    return;
                }
            }

            if (BufCopyShadowMap == null && s_processData)
            {
                BufCopyShadowMap      = new CommandBuffer();
                BufCopyShadowMap.name = "Shadow data";
                _mainLight.AddCommandBuffer(LightEvent.BeforeScreenspaceMask, BufCopyShadowMap);
            }
            else if (!s_processData && BufCopyShadowMap != null)
            {
                _mainLight.RemoveCommandBuffer(LightEvent.BeforeScreenspaceMask, BufCopyShadowMap);
                BufCopyShadowMap = null;
            }

            if (!s_processData)
            {
                return;
            }

            // Update the camera if it has changed.
            if (_cameraMain.transform != OceanRenderer.Instance.Viewpoint)
            {
                UpdateCameraMain();
            }

            Swap(ref _sources, ref _targets);

            BufCopyShadowMap.Clear();

            ValidateSourceData();

            // clear the shadow collection. it will be overwritten with shadow values IF the shadows render,
            // which only happens if there are (nontransparent) shadow receivers around. this is only reliable
            // in play mode, so don't do it in edit mode.
#if UNITY_EDITOR
            if (UnityEditor.EditorApplication.isPlaying)
#endif
            {
                TextureArrayHelpers.ClearToBlack(_targets);
            }

            var lt = OceanRenderer.Instance._lodTransform;
            for (var lodIdx = lt.LodCount - 1; lodIdx >= 0; lodIdx--)
            {
                _renderProperties.Initialise(BufCopyShadowMap, _updateShadowShader, krnl_UpdateShadow);

                lt._renderData[lodIdx].Validate(0, SimName);
                _renderProperties.SetVector(sp_CenterPos, lt._renderData[lodIdx]._posSnapped);
                var scale = OceanRenderer.Instance.CalcLodScale(lodIdx);
                _renderProperties.SetVector(sp_Scale, new Vector3(scale, 1f, scale));

                if (OceanRenderer.Instance.Viewpoint != null)
                {
                    _renderProperties.SetVector(sp_CamPos, OceanRenderer.Instance.Viewpoint.position);
                    _renderProperties.SetVector(sp_CamForward, OceanRenderer.Instance.Viewpoint.forward);
                }

                _renderProperties.SetVector(sp_JitterDiameters_CurrentFrameWeights, new Vector4(Settings._jitterDiameterSoft, Settings._jitterDiameterHard, Settings._currentFrameWeightSoft, Settings._currentFrameWeightHard));
                _renderProperties.SetMatrix(sp_MainCameraProjectionMatrix, _cameraMain.projectionMatrix * _cameraMain.worldToCameraMatrix);
                _renderProperties.SetFloat(sp_SimDeltaTime, OceanRenderer.Instance.DeltaTimeDynamics);

                // compute which lod data we are sampling previous frame shadows from. if a scale change has happened this can be any lod up or down the chain.
                var srcDataIdx = lodIdx + ScaleDifferencePow2;
                srcDataIdx = Mathf.Clamp(srcDataIdx, 0, lt.LodCount - 1);
                _renderProperties.SetInt(sp_LD_SliceIndex, lodIdx);
                _renderProperties.SetInt(sp_LD_SliceIndex_Source, srcDataIdx);
                BindSourceData(_renderProperties, false);
                _renderProperties.SetTexture(sp_LD_TexArray_Target, _targets);

                BufCopyShadowMap.DispatchCompute(_updateShadowShader, krnl_UpdateShadow,
                                                 OceanRenderer.Instance.LodDataResolution / THREAD_GROUP_SIZE_X,
                                                 OceanRenderer.Instance.LodDataResolution / THREAD_GROUP_SIZE_Y,
                                                 1);
            }
        }
예제 #8
0
        public override void UpdateLodData()
        {
            if (!enabled)
            {
                return;
            }

            base.UpdateLodData();

            if (_mainLight != OceanRenderer.Instance._primaryLight)
            {
                if (_mainLight)
                {
                    _mainLight.RemoveCommandBuffer(LightEvent.BeforeScreenspaceMask, BufCopyShadowMap);
                    BufCopyShadowMap = null;
                    TextureArrayHelpers.ClearToBlack(_sources);
                    TextureArrayHelpers.ClearToBlack(_targets);
                }
                _mainLight = null;
            }

            if (!OceanRenderer.Instance._primaryLight)
            {
                if (!Settings._allowNullLight)
                {
                    Debug.LogWarning("Primary light must be specified on OceanRenderer script to enable shadows.", OceanRenderer.Instance);
                }
                return;
            }

            if (!_mainLight)
            {
                if (!StartInitLight())
                {
                    enabled = false;
                    return;
                }
            }

            if (BufCopyShadowMap == null && s_processData)
            {
                BufCopyShadowMap      = new CommandBuffer();
                BufCopyShadowMap.name = "Shadow data";
                _mainLight.AddCommandBuffer(LightEvent.BeforeScreenspaceMask, BufCopyShadowMap);
            }
            else if (!s_processData && BufCopyShadowMap != null)
            {
                _mainLight.RemoveCommandBuffer(LightEvent.BeforeScreenspaceMask, BufCopyShadowMap);
                BufCopyShadowMap = null;
            }

            if (!s_processData)
            {
                return;
            }

            Swap(ref _sources, ref _targets);

            BufCopyShadowMap.Clear();

            ValidateSourceData();

            // clear the shadow collection. it will be overwritten with shadow values IF the shadows render,
            // which only happens if there are (nontransparent) shadow receivers around. this is only reliable
            // in play mode, so don't do it in edit mode.
#if UNITY_EDITOR
            if (UnityEditor.EditorApplication.isPlaying)
#endif
            {
                TextureArrayHelpers.ClearToBlack(_targets);
            }

            // Cache the camera for further down.
            var camera = OceanRenderer.Instance.ViewCamera;
            if (camera == null)
            {
                // We want to return early after clear.
                return;
            }

            {
                // Run shadow update

                // It feels like quite a lot could be optimized out of the below. I think the same params are written repeatedly, and probably
                // a bunch of them are already available in existing ocean globals.

                _renderProperties.Initialise(BufCopyShadowMap, _updateShadowShader, krnl_UpdateShadow);

                _renderProperties.SetVector(sp_CamPos, camera.transform.position);
                _renderProperties.SetVector(sp_CamForward, camera.transform.forward);

                _renderProperties.SetVector(sp_JitterDiameters_CurrentFrameWeights, new Vector4(Settings._jitterDiameterSoft, Settings._jitterDiameterHard, Settings._currentFrameWeightSoft, Settings._currentFrameWeightHard));
                _renderProperties.SetMatrix(sp_MainCameraProjectionMatrix, camera.projectionMatrix * camera.worldToCameraMatrix);
                _renderProperties.SetFloat(sp_SimDeltaTime, OceanRenderer.Instance.DeltaTimeDynamics);

                _renderProperties.SetTexture(GetParamIdSampler(true), (Texture)_sources);

                _renderProperties.SetTexture(sp_LD_TexArray_Target, _targets);

                _renderProperties.SetBuffer(sp_cascadeDataSrc, OceanRenderer.Instance._bufCascadeDataSrc);

                var lt = OceanRenderer.Instance._lodTransform;
                for (var lodIdx = lt.LodCount - 1; lodIdx >= 0; lodIdx--)
                {
#if UNITY_EDITOR
                    lt._renderData[lodIdx].Validate(0, SimName);
#endif

                    _renderProperties.SetVector(sp_CenterPos, lt._renderData[lodIdx]._posSnapped);
                    var scale = OceanRenderer.Instance.CalcLodScale(lodIdx);
                    _renderProperties.SetVector(sp_Scale, new Vector3(scale, 1f, scale));

                    // compute which lod data we are sampling previous frame shadows from. if a scale change has happened this can be any lod up or down the chain.
                    var srcDataIdx = lodIdx + ScaleDifferencePow2;
                    srcDataIdx = Mathf.Clamp(srcDataIdx, 0, lt.LodCount - 1);
                    _renderProperties.SetInt(sp_LD_SliceIndex, lodIdx);
                    _renderProperties.SetInt(sp_LD_SliceIndex_Source, srcDataIdx);

                    BufCopyShadowMap.DispatchCompute(_updateShadowShader, krnl_UpdateShadow,
                                                     OceanRenderer.Instance.LodDataResolution / THREAD_GROUP_SIZE_X,
                                                     OceanRenderer.Instance.LodDataResolution / THREAD_GROUP_SIZE_Y,
                                                     1);
                }

#if ENABLE_VR && ENABLE_VR_MODULE
                // Disable for XR SPI otherwise input will not have correct world position.
                if (XRSettings.enabled && XRSettings.stereoRenderingMode == XRSettings.StereoRenderingMode.SinglePassInstanced)
                {
                    BufCopyShadowMap.DisableShaderKeyword("STEREO_INSTANCING_ON");
                }
#endif

                // Process registered inputs.
                for (var lodIdx = lt.LodCount - 1; lodIdx >= 0; lodIdx--)
                {
                    BufCopyShadowMap.SetRenderTarget(_targets, _targets.depthBuffer, 0, CubemapFace.Unknown, lodIdx);
                    SubmitDraws(lodIdx, BufCopyShadowMap);
                }

#if ENABLE_VR && ENABLE_VR_MODULE
                // Restore XR SPI as we cannot rely on remaining pipeline to do it for us.
                if (XRSettings.enabled && XRSettings.stereoRenderingMode == XRSettings.StereoRenderingMode.SinglePassInstanced)
                {
                    BufCopyShadowMap.EnableShaderKeyword("STEREO_INSTANCING_ON");
                }
#endif
            }

            // Set the target texture as to make sure we catch the 'pong' each frame
            Shader.SetGlobalTexture(GetParamIdSampler(), _targets);
        }
        public override void UpdateLodData()
        {
            if (!enabled)
            {
                return;
            }

            base.UpdateLodData();

            ClearBufferIfLightChanged();

            if (!StartInitLight())
            {
                enabled = false;
                return;
            }

            if (!s_processData)
            {
                return;
            }

            if (BufCopyShadowMap == null)
            {
                BufCopyShadowMap      = new CommandBuffer();
                BufCopyShadowMap.name = "Shadow data";
            }

            if (!s_processData)
            {
                return;
            }

            Swap(ref _sources, ref _targets);

            BufCopyShadowMap.Clear();

            ValidateSourceData();

            // clear the shadow collection. it will be overwritten with shadow values IF the shadows render,
            // which only happens if there are (nontransparent) shadow receivers around. this is only reliable
            // in play mode, so don't do it in edit mode.
#if UNITY_EDITOR
            if (UnityEditor.EditorApplication.isPlaying)
#endif
            {
                TextureArrayHelpers.ClearToBlack(_targets);
            }

            // Cache the camera for further down.
            var camera = OceanRenderer.Instance.ViewCamera;
            if (camera == null)
            {
                // We want to return early after clear.
                return;
            }

            // TODO - this is in SRP, so i can't ifdef it? what is a good plan here - wait for it to be removed completely?
#pragma warning disable 618
            using (new ProfilingSample(BufCopyShadowMap, "CrestSampleShadows"))
#pragma warning restore 618
            {
                var lt = OceanRenderer.Instance._lodTransform;
                for (var lodIdx = lt.LodCount - 1; lodIdx >= 0; lodIdx--)
                {
#if UNITY_EDITOR
                    lt._renderData[lodIdx].Validate(0, SimName);
#endif

                    _renderMaterial[lodIdx].SetVector(sp_CenterPos, lt._renderData[lodIdx]._posSnapped);
                    var scale = OceanRenderer.Instance.CalcLodScale(lodIdx);
                    _renderMaterial[lodIdx].SetVector(sp_Scale, new Vector3(scale, 1f, scale));
                    _renderMaterial[lodIdx].SetVector(sp_JitterDiameters_CurrentFrameWeights, new Vector4(Settings._jitterDiameterSoft, Settings._jitterDiameterHard, Settings._currentFrameWeightSoft, Settings._currentFrameWeightHard));
                    _renderMaterial[lodIdx].SetMatrix(sp_MainCameraProjectionMatrix, GL.GetGPUProjectionMatrix(camera.projectionMatrix, renderIntoTexture: true) * camera.worldToCameraMatrix);
                    _renderMaterial[lodIdx].SetFloat(sp_SimDeltaTime, Time.deltaTime);

                    // compute which lod data we are sampling previous frame shadows from. if a scale change has happened this can be any lod up or down the chain.
                    var srcDataIdx = lodIdx + ScaleDifferencePow2;
                    srcDataIdx = Mathf.Clamp(srcDataIdx, 0, lt.LodCount - 1);
                    _renderMaterial[lodIdx].SetFloat(sp_LD_SliceIndex, lodIdx);
                    _renderMaterial[lodIdx].SetFloat(sp_LD_SliceIndex_Source, srcDataIdx);
                    _renderMaterial[lodIdx].SetTexture(GetParamIdSampler(true), _sources);
                    _renderMaterial[lodIdx].SetBuffer(sp_cascadeDataSrc, OceanRenderer.Instance._bufCascadeDataSrc);

                    BufCopyShadowMap.Blit(Texture2D.blackTexture, _targets, _renderMaterial[lodIdx].material, -1, lodIdx);
                }

                // Disable single pass double-wide stereo rendering for these commands since we are rendering to
                // rendering texture. Otherwise, it will render double. Single pass instanced is broken here, but that
                // appears to be a Unity bug only for the legacy VR system.
                if (camera.stereoEnabled && XRSettings.stereoRenderingMode == XRSettings.StereoRenderingMode.SinglePass)
                {
                    BufCopyShadowMap.SetSinglePassStereo(SinglePassStereoMode.None);
                    BufCopyShadowMap.DisableShaderKeyword("UNITY_SINGLE_PASS_STEREO");
                }

                // Process registered inputs.
                for (var lodIdx = lt.LodCount - 1; lodIdx >= 0; lodIdx--)
                {
                    BufCopyShadowMap.SetRenderTarget(_targets, _targets.depthBuffer, 0, CubemapFace.Unknown, lodIdx);
                    SubmitDraws(lodIdx, BufCopyShadowMap);
                }

                // Restore single pass double-wide as we cannot rely on remaining pipeline to do it for us.
                if (camera.stereoEnabled && XRSettings.stereoRenderingMode == XRSettings.StereoRenderingMode.SinglePass)
                {
                    BufCopyShadowMap.SetSinglePassStereo(SinglePassStereoMode.SideBySide);
                    BufCopyShadowMap.EnableShaderKeyword("UNITY_SINGLE_PASS_STEREO");
                }

                // Set the target texture as to make sure we catch the 'pong' each frame
                Shader.SetGlobalTexture(GetParamIdSampler(), _targets);
            }
        }
예제 #10
0
        public override void UpdateLodData()
        {
            if (!enabled)
            {
                return;
            }

            base.UpdateLodData();

            ClearBufferIfLightChanged();

            if (!StartInitLight())
            {
                enabled = false;
                return;
            }

            if (!s_processData)
            {
                return;
            }

            if (BufCopyShadowMap == null)
            {
                BufCopyShadowMap      = new CommandBuffer();
                BufCopyShadowMap.name = "Shadow data";
            }

            if (!s_processData)
            {
                return;
            }

            Swap(ref _sources, ref _targets);

            BufCopyShadowMap.Clear();

            ValidateSourceData();

            // clear the shadow collection. it will be overwritten with shadow values IF the shadows render,
            // which only happens if there are (nontransparent) shadow receivers around. this is only reliable
            // in play mode, so don't do it in edit mode.
#if UNITY_EDITOR
            if (UnityEditor.EditorApplication.isPlaying)
#endif
            {
                TextureArrayHelpers.ClearToBlack(_targets);
            }

            // TODO - this is in SRP, so i can't ifdef it? what is a good plan here - wait for it to be removed completely?
#pragma warning disable 618
            using (new ProfilingSample(BufCopyShadowMap, "CrestSampleShadows"))
#pragma warning restore 618
            {
                var lt = OceanRenderer.Instance._lodTransform;
                for (var lodIdx = lt.LodCount - 1; lodIdx >= 0; lodIdx--)
                {
                    lt._renderData[lodIdx].Validate(0, SimName);
                    _renderMaterial[lodIdx].SetVector(sp_CenterPos, lt._renderData[lodIdx]._posSnapped);
                    var scale = OceanRenderer.Instance.CalcLodScale(lodIdx);
                    _renderMaterial[lodIdx].SetVector(sp_Scale, new Vector3(scale, 1f, scale));
                    _renderMaterial[lodIdx].SetVector(sp_JitterDiameters_CurrentFrameWeights, new Vector4(Settings._jitterDiameterSoft, Settings._jitterDiameterHard, Settings._currentFrameWeightSoft, Settings._currentFrameWeightHard));
                    _renderMaterial[lodIdx].SetMatrix(sp_MainCameraProjectionMatrix, _cameraMain.projectionMatrix * _cameraMain.worldToCameraMatrix);
                    _renderMaterial[lodIdx].SetFloat(sp_SimDeltaTime, Time.deltaTime);

                    // compute which lod data we are sampling previous frame shadows from. if a scale change has happened this can be any lod up or down the chain.
                    var srcDataIdx = lodIdx + ScaleDifferencePow2;
                    srcDataIdx = Mathf.Clamp(srcDataIdx, 0, lt.LodCount - 1);
                    _renderMaterial[lodIdx].SetInt(sp_LD_SliceIndex, lodIdx);
                    _renderMaterial[lodIdx].SetInt(sp_LD_SliceIndex_Source, srcDataIdx);
                    BindSourceData(_renderMaterial[lodIdx], false);
                    BufCopyShadowMap.Blit(Texture2D.blackTexture, _targets, _renderMaterial[lodIdx].material, -1, lodIdx);
                }
            }
        }