﻿/**************************************/
/*         LIQUID VOLUME PRO 2        */
/*         Created by Kronnect        */
/**************************************/

// Enable floating point render textures
#define LIQUID_VOLUME_FP_RENDER_TEXTURES

// Displays sliced liquid volume
//#define DEBUG_SLICE

using UnityEngine;
using UnityEngine.Rendering;
using System;
using System.Collections.Generic;
using LiquidVolumeFX.MIConvexHull;
using System.Linq;
using Random = UnityEngine.Random;
using UnityEngine.XR;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace LiquidVolumeFX {
    public enum TOPOLOGY {
        Sphere = 0,
        Cylinder = 1,
        Cube = 2,
        Irregular = 10
    }

    public enum DETAIL {
        Simple = 0,
        SimpleNoFlask = 1,
        Default = 10,
        DefaultNoFlask = 11,
        Multiple = 50,
        MultipleNoFlask = 51
    }

    public enum LEVEL_COMPENSATION {
        None = 0,
        Fast = 10,
        Accurate = 20
    }

    public enum BuoyancyEffect {
        Simple = 0,
        PositionOnly = 1,
        PositionAndRotation = 2
    }

    public static class DetailExtensions {

        public static bool isMultiple(this DETAIL detail) {
            return detail == DETAIL.Multiple || detail == DETAIL.MultipleNoFlask;
        }

        public static bool allowsRefraction(this DETAIL detail) {
            return detail != DETAIL.DefaultNoFlask && detail != DETAIL.MultipleNoFlask;
        }

        public static bool usesFlask(this DETAIL detail) {
            return detail == DETAIL.Simple || detail == DETAIL.Default || detail == DETAIL.Multiple;
        }

    }


    [ExecuteInEditMode]
    [HelpURL("https://kronnect.com/support")]
    [AddComponentMenu("Effects/Liquid Volume")]
    [DisallowMultipleComponent]
    public partial class LiquidVolume : MonoBehaviour {

        public static bool FORCE_GLES_COMPATIBILITY = false;

        /// <summary>
        /// Max number of layers for multiple style
        /// </summary>
        public const int MAX_LAYERS = 16;

        [Serializable]
        public struct LiquidLayer {
            [HideInInspector] public string layerName;
            public float amount;
            [Tooltip("Density defines the order in which layers stack up. Two or more liquids with same density and miscible set to true will mix.")]
            public float density;
            public Color color;
            [Tooltip("If miscible is set to true, this liquid will mix with any other with same density.")]
            public bool miscible;
            [Range(0, 1)]
            public float murkiness;
            public Color murkColor;
            [Range(0.001f, 0.48f)]
            public float scale;

            [Range(0, 1), Tooltip("The viscosity determines the influence of global turbulence to this liquid. A value of 0 means sticky while a value of 1 means a very fluid substance.")]
            public float viscosity;

            [Range(0.001f, 10)]
            public float adjustmentSpeed;

            [Range(0, 1), Tooltip("Bubbles opacity.")]
            public float bubblesOpacity;

            [NonSerialized]
            public float mixedAmount, mixedScale, mixedMurkiness;
            [NonSerialized]
            public Color mixedColor;
            [NonSerialized]
            public Color mixedColor2;
            [NonSerialized]
            public float mixedBubblesOpacity;
            [HideInInspector]
            public float baseLevel;
            [HideInInspector]
            public float currentBaseLevel;
            [HideInInspector]
            public float currentAmount;
            [HideInInspector]
            public Color currentColor;
            [HideInInspector]
            public float currentMurkiness;
            [HideInInspector]
            public Color currentColor2;
            [HideInInspector]
            public float currentBubblesOpacity;

            public void SetDefaults(int layerIndex) {
                amount = 0.1f;
                density = 1f;
                color = new Color(Random.value, Random.value, Random.value, 0.5f);
                murkColor = new Color(0, 0, 0, 1f);
                murkiness = 0.5f;
                scale = 0.3f;
                adjustmentSpeed = 1f;
                viscosity = 1f;
                bubblesOpacity = 1f;
                layerName = "Layer " + layerIndex;
            }

            public void ResetCurrentStates() {
                currentColor = color;
                currentColor2 = murkColor;
                currentMurkiness = murkiness;
                currentAmount = amount;
                currentBaseLevel = baseLevel;
                currentBubblesOpacity = bubblesOpacity;
            }

        }


        [SerializeField]
        TOPOLOGY _topology = TOPOLOGY.Sphere;

        public TOPOLOGY topology {
            get { return _topology; }
            set {
                if (_topology != value) {
                    _topology = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        DETAIL _detail = DETAIL.Default;

        public DETAIL detail {
            get { return _detail; }
            set {
                if (_detail != value) {
                    _detail = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 1)]
        float _level = 0.5f;

        public float level {
            get { return _level; }
            set {
                if (_level != value) {
                    if (_detail.isMultiple()) {
                        Debug.LogWarning("In multiple detail level, fill level is automatically determined by the amount/density values in each layer.");
                        return;
                    }
                    _level = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        [Range(0, 1)]
        float _levelMultiplier = 1f;

        public float levelMultiplier {
            get { return _levelMultiplier; }
            set {
                if (_levelMultiplier != value) {
                    _levelMultiplier = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        LiquidLayer[] _liquidLayers;

        public LiquidLayer[] liquidLayers {
            get { return _liquidLayers; }
            set {
                if (_liquidLayers != value) {
                    _liquidLayers = value;
                    lastLayerCount = 0;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        [Range(0, 16)]
        int _smoothContactSurface = 3;

        public int smoothContactSurface {
            get { return _smoothContactSurface; }
            set {
                if (_smoothContactSurface != value) {
                    _smoothContactSurface = value;
                    requireLayersUpdate = true;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        [Range(0, 1)]
        float _ditherStrength = 0.5f;

        public float ditherStrength {
            get { return _ditherStrength; }
            set {
                if (_ditherStrength != value) {
                    _ditherStrength = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0.001f, 10)]
        float _layersAdjustmentSpeed = 1f;

        public float layersAdjustmentSpeed {
            get { return _layersAdjustmentSpeed; }
            set {
                if (_layersAdjustmentSpeed != value) {
                    _layersAdjustmentSpeed = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        bool _layersAdjustmentCompact = true;

        public bool layersAdjustmentCompact {
            get { return _layersAdjustmentCompact; }
            set {
                if (_layersAdjustmentCompact != value) {
                    _layersAdjustmentCompact = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        [Tooltip("Uses directional light color")]
        bool _useLightColor;

        public bool useLightColor {
            get { return _useLightColor; }
            set {
                if (_useLightColor != value) {
                    _useLightColor = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        Light _directionalLight;

        public Light directionalLight {
            get { return _directionalLight; }
            set {
                if (_directionalLight != value) {
                    _directionalLight = value;
                    UpdateMaterialProperties();
                }
            }
        }



        [SerializeField]
        [ColorUsage(true)]
        Color _liquidColor1 = new Color(0, 1, 0, 0.1f);

        public Color liquidColor1 {
            get { return _liquidColor1; }
            set {
                if (_liquidColor1 != value) {
                    _liquidColor1 = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0.1f, 4.85f)]
        float _liquidScale1 = 1f;

        public float liquidScale1 {
            get { return _liquidScale1; }
            set {
                if (_liquidScale1 != value) {
                    _liquidScale1 = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [ColorUsage(true)]
        Color _liquidColor2 = new Color(1, 0, 0, 0.3f);

        public Color liquidColor2 {
            get { return _liquidColor2; }
            set {
                if (_liquidColor2 != value) {
                    _liquidColor2 = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(2f, 4.85f)]
        float _liquidScale2 = 5f;

        public float liquidScale2 {
            get { return _liquidScale2; }
            set {
                if (_liquidScale2 != value) {
                    _liquidScale2 = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 1)]
        float _alpha = 1f;

        public float alpha {
            get { return _alpha; }
            set {
                if (_alpha != Mathf.Clamp01(value)) {
                    _alpha = Mathf.Clamp01(value);
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [ColorUsage(true)]
        Color _emissionColor = new Color(0, 0, 0);

        public Color emissionColor {
            get { return _emissionColor; }
            set {
                if (_emissionColor != value) {
                    _emissionColor = value;
                    UpdateMaterialProperties();
                }
            }
        }
        [SerializeField]
        bool _ditherShadows = true;

        public bool ditherShadows {
            get { return _ditherShadows; }
            set {
                if (_ditherShadows != value) {
                    _ditherShadows = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 1)]
        float _murkiness = 1.0f;

        public float murkiness {
            get { return _murkiness; }
            set {
                if (_murkiness != value) {
                    _murkiness = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 1f)]
        float _turbulence1 = 0.5f;

        public float turbulence1 {
            get { return _turbulence1; }
            set {
                if (_turbulence1 != value) {
                    _turbulence1 = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 1f)]
        float _turbulence2 = 0.2f;

        public float turbulence2 {
            get { return _turbulence2; }
            set {
                if (_turbulence2 != value) {
                    _turbulence2 = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        float _frecuency = 1f;

        public float frecuency {
            get { return _frecuency; }
            set {
                if (_frecuency != value) {
                    _frecuency = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0f, 2f)]
        float _speed = 1f;

        public float speed {
            get { return _speed; }
            set {
                if (_speed != value) {
                    _speed = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 5f)]
        float _sparklingIntensity = 0.1f;

        public float sparklingIntensity {
            get { return _sparklingIntensity; }
            set {
                if (_sparklingIntensity != value) {
                    _sparklingIntensity = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 1)]
        float _sparklingAmount = 0.2f;

        public float sparklingAmount {
            get { return _sparklingAmount; }
            set {
                if (_sparklingAmount != value) {
                    _sparklingAmount = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 10)]
        float _deepObscurance = 2.0f;

        public float deepObscurance {
            get { return _deepObscurance; }
            set {
                if (_deepObscurance != value) {
                    _deepObscurance = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [ColorUsage(true)]
        Color _foamColor = new Color(1, 1, 1, 0.65f);

        public Color foamColor {
            get { return _foamColor; }
            set {
                if (_foamColor != value) {
                    _foamColor = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0.01f, 1f)]
        float _foamScale = 0.2f;

        public float foamScale {
            get { return _foamScale; }
            set {
                if (_foamScale != value) {
                    _foamScale = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 0.1f)]
        float _foamThickness = 0.04f;

        public float foamThickness {
            get { return _foamThickness; }
            set {
                if (_foamThickness != value) {
                    _foamThickness = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(-1, 1)]
        float _foamDensity = 0.5f;

        public float foamDensity {
            get { return _foamDensity; }
            set {
                if (_foamDensity != value) {
                    _foamDensity = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(4, 100)]
        float _foamWeight = 10f;

        public float foamWeight {
            get { return _foamWeight; }
            set {
                if (_foamWeight != value) {
                    _foamWeight = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 1)]
        float _foamTurbulence = 1f;

        public float foamTurbulence {
            get { return _foamTurbulence; }
            set {
                if (_foamTurbulence != value) {
                    _foamTurbulence = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        bool _foamVisibleFromBottom = true;

        public bool foamVisibleFromBottom {
            get { return _foamVisibleFromBottom; }
            set {
                if (_foamVisibleFromBottom != value) {
                    _foamVisibleFromBottom = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        bool _smokeEnabled = true;

        public bool smokeEnabled {
            get { return _smokeEnabled; }
            set {
                if (_smokeEnabled != value) {
                    _smokeEnabled = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [ColorUsage(true)]
        [SerializeField]
        Color _smokeColor = new Color(0.7f, 0.7f, 0.7f, 0.25f);

        public Color smokeColor {
            get { return _smokeColor; }
            set {
                if (_smokeColor != value) {
                    _smokeColor = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0.01f, 1f)]
        float _smokeScale = 0.25f;

        public float smokeScale {
            get { return _smokeScale; }
            set {
                if (_smokeScale != value) {
                    _smokeScale = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 10f)]
        float _smokeBaseObscurance = 2.0f;

        public float smokeBaseObscurance {
            get { return _smokeBaseObscurance; }
            set {
                if (_smokeBaseObscurance != value) {
                    _smokeBaseObscurance = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        [Range(0, 10f)]
        float _smokeHeightAtten;

        public float smokeHeightAtten {
            get { return _smokeHeightAtten; }
            set {
                if (_smokeHeightAtten != value) {
                    _smokeHeightAtten = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        [Range(0, 20f)]
        float _smokeSpeed = 5.0f;

        public float smokeSpeed {
            get { return _smokeSpeed; }
            set {
                if (_smokeSpeed != value) {
                    _smokeSpeed = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        bool _fixMesh;

        public bool fixMesh {
            get { return _fixMesh; }
            set {
                if (_fixMesh != value) {
                    _fixMesh = value;
                    UpdateMaterialProperties();
                }
            }
        }

        public Mesh originalMesh;
        public Vector3 originalPivotOffset;

        [SerializeField]
        Vector3 _pivotOffset;

        public Vector3 pivotOffset {
            get { return _pivotOffset; }
            set {
                if (_pivotOffset != value) {
                    _pivotOffset = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        bool _autoCloseMesh;

        public bool autoCloseMesh {
            get { return _autoCloseMesh; }
            set {
                if (_autoCloseMesh != value) {
                    _autoCloseMesh = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        [Range(0, 1.5f)]
        float _upperLimit = 1.5f;

        public float upperLimit {
            get { return _upperLimit; }
            set {
                if (_upperLimit != value) {
                    _upperLimit = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(-1.5f, 1.5f)]
        float _lowerLimit = -1.5f;

        public float lowerLimit {
            get { return _lowerLimit; }
            set {
                if (_lowerLimit != value) {
                    _lowerLimit = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        int _subMeshIndex = -1;

        public int subMeshIndex {
            get { return _subMeshIndex; }
            set {
                if (_subMeshIndex != value) {
                    _subMeshIndex = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        Material _flaskMaterial;

        public Material flaskMaterial {
            get { return _flaskMaterial; }
            set {
                if (_flaskMaterial != value) {
                    _flaskMaterial = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 1)]
        float _flaskThickness = 0.03f;

        public float flaskThickness {
            get { return _flaskThickness; }
            set {
                if (_flaskThickness != value) {
                    _flaskThickness = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 1)]
        float _glossinessInternal = 0.3f;

        public float glossinessInternal {
            get { return _glossinessInternal; }
            set {
                if (_glossinessInternal != value) {
                    _glossinessInternal = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        bool _scatteringEnabled = false;

        public bool scatteringEnabled {
            get { return _scatteringEnabled; }
            set {
                if (_scatteringEnabled != value) {
                    _scatteringEnabled = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(1, 16)]
        int _scatteringPower = 5;

        public int scatteringPower {
            get { return _scatteringPower; }
            set {
                if (_scatteringPower != value) {
                    _scatteringPower = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 10)]
        float _scatteringAmount = 0.3f;

        public float scatteringAmount {
            get { return _scatteringAmount; }
            set {
                if (_scatteringAmount != value) {
                    _scatteringAmount = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        bool _refractionBlur = true;

        public bool refractionBlur {
            get { return _refractionBlur; }
            set {
                if (_refractionBlur != value) {
                    _refractionBlur = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 1)]
        float _blurIntensity = 0.75f;

        public float blurIntensity {
            get { return _blurIntensity; }
            set {
                if (_blurIntensity != Mathf.Clamp01(value)) {
                    _blurIntensity = Mathf.Clamp01(value);
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        int _liquidRaySteps = 10;

        public int liquidRaySteps {
            get { return _liquidRaySteps; }
            set {
                if (_liquidRaySteps != value) {
                    _liquidRaySteps = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        int _foamRaySteps = 7;

        public int foamRaySteps {
            get { return _foamRaySteps; }
            set {
                if (_foamRaySteps != value) {
                    _foamRaySteps = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        int _smokeRaySteps = 5;

        public int smokeRaySteps {
            get { return _smokeRaySteps; }
            set {
                if (_smokeRaySteps != value) {
                    _smokeRaySteps = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        Texture2D _bumpMap;

        public Texture2D bumpMap {
            get { return _bumpMap; }
            set {
                if (_bumpMap != value) {
                    _bumpMap = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 1f)]
        float _bumpStrength = 1f;

        public float bumpStrength {
            get { return _bumpStrength; }
            set {
                if (_bumpStrength != value) {
                    _bumpStrength = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 10f)]
        float _bumpDistortionScale = 1f;

        public float bumpDistortionScale {
            get { return _bumpDistortionScale; }
            set {
                if (_bumpDistortionScale != value) {
                    _bumpDistortionScale = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        Vector2 _bumpDistortionOffset;

        public Vector2 bumpDistortionOffset {
            get { return _bumpDistortionOffset; }
            set {
                if (_bumpDistortionOffset != value) {
                    _bumpDistortionOffset = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        Texture2D _distortionMap;

        public Texture2D distortionMap {
            get { return _distortionMap; }
            set {
                if (_distortionMap != value) {
                    _distortionMap = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        Texture2D _texture;

        public Texture2D texture {
            get { return _texture; }
            set {
                if (_texture != value) {
                    _texture = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        Vector2
            _textureScale = Vector2.one;

        public Vector2 textureScale {
            get { return _textureScale; }
            set {
                if (_textureScale != value) {
                    _textureScale = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        Vector2
            _textureOffset;

        public Vector2 textureOffset {
            get { return _textureOffset; }
            set {
                if (_textureOffset != value) {
                    _textureOffset = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0, 10f)]
        float
            _distortionAmount = 1f;

        public float distortionAmount {
            get { return _distortionAmount; }
            set {
                if (_distortionAmount != value) {
                    _distortionAmount = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        bool _depthAware;

        public bool depthAware {
            get { return _depthAware; }
            set {
                if (_depthAware != value) {
                    _depthAware = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        float _depthAwareOffset;

        public float depthAwareOffset {
            get { return _depthAwareOffset; }
            set {
                if (_depthAwareOffset != value) {
                    _depthAwareOffset = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        bool _irregularDepthDebug = false;

        public bool irregularDepthDebug {
            get { return _irregularDepthDebug; }
            set {
                if (_irregularDepthDebug != value) {
                    _irregularDepthDebug = value;
                    UpdateMaterialProperties();
                }
            }
        }



        [SerializeField]
        bool _depthAwareCustomPass;

        public bool depthAwareCustomPass {
            get { return _depthAwareCustomPass; }
            set {
                if (_depthAwareCustomPass != value) {
                    _depthAwareCustomPass = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        bool _depthAwareCustomPassDebug = false;

        public bool depthAwareCustomPassDebug {
            get { return _depthAwareCustomPassDebug; }
            set {
                if (_depthAwareCustomPassDebug != value) {
                    _depthAwareCustomPassDebug = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        [Range(0, 5f)]
        float _doubleSidedBias = 0.001f;

        public float doubleSidedBias {
            get { return _doubleSidedBias; }
            set {
                if (_doubleSidedBias != value) {
                    _doubleSidedBias = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        float _backDepthBias;

        public float backDepthBias {
            get { return _backDepthBias; }
            set {
                if (value < 0) value = 0;
                if (_backDepthBias != value) {
                    _backDepthBias = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        LEVEL_COMPENSATION _rotationLevelCompensation = LEVEL_COMPENSATION.None;

        public LEVEL_COMPENSATION rotationLevelCompensation {
            get { return _rotationLevelCompensation; }
            set {
                if (_rotationLevelCompensation != value) {
                    _rotationLevelCompensation = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        bool _ignoreGravity;

        public bool ignoreGravity {
            get { return _ignoreGravity; }
            set {
                if (_ignoreGravity != value) {
                    _ignoreGravity = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        bool _reactToForces;

        public bool reactToForces {
            get { return _reactToForces; }
            set {
                if (_reactToForces != value) {
                    _reactToForces = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        Vector3 _extentsScale = Vector3.one;

        public Vector3 extentsScale {
            get { return _extentsScale; }
            set {
                if (_extentsScale != value) {
                    _extentsScale = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(1, 3)]
        int _noiseVariation = 1;

        public int noiseVariation {
            get { return _noiseVariation; }
            set {
                if (_noiseVariation != value) {
                    _noiseVariation = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        bool _allowViewFromInside = false;

        public bool allowViewFromInside {
            get { return _allowViewFromInside; }
            set {
                if (_allowViewFromInside != value) {
                    _allowViewFromInside = value;
                    lastDistanceToCam = -1;
                    CheckInsideOut();
                }
            }
        }


        [SerializeField]
        bool
            _debugSpillPoint = false;

        public bool debugSpillPoint {
            get { return _debugSpillPoint; }
            set {
                if (_debugSpillPoint != value) {
                    _debugSpillPoint = value;
                }
            }
        }

        [SerializeField]
        int
            _renderQueue = 3001;

        public int renderQueue {
            get { return _renderQueue; }
            set {
                if (_renderQueue != value) {
                    _renderQueue = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        Cubemap _reflectionTexture;

        public Cubemap reflectionTexture {
            get { return _reflectionTexture; }
            set {
                if (_reflectionTexture != value) {
                    _reflectionTexture = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0.1f, 5f)]
        float _physicsMass = 1f;

        public float physicsMass {
            get { return _physicsMass; }
            set {
                if (_physicsMass != value) {
                    _physicsMass = value;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        [Range(0.0f, 0.2f)]
        float _physicsAngularDamp = 0.02f;

        public float physicsAngularDamp {
            get { return _physicsAngularDamp; }
            set {
                if (_physicsAngularDamp != value) {
                    _physicsAngularDamp = value;
                    UpdateMaterialProperties();
                }
            }
        }

        #region Bubbles properties

        [SerializeField]
        int _bubblesAmount = 32;

        public int bubblesAmount {
            get { return _bubblesAmount; }
            set {
                if (_bubblesAmount != value) {
                    _bubblesAmount = value;
                    requireBubblesUpdate = true;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField, Range(1, 16)]
        int _bubblesSizeMin = 2;

        public int bubblesSizeMin {
            get { return _bubblesSizeMin; }
            set {
                if (_bubblesSizeMin != value) {
                    _bubblesSizeMin = value;
                    requireBubblesUpdate = true;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField, Range(1, 16)]
        int _bubblesSizeMax = 4;

        public int bubblesSizeMax {
            get { return _bubblesSizeMax; }
            set {
                if (_bubblesSizeMax != value) {
                    _bubblesSizeMax = value;
                    requireBubblesUpdate = true;
                    UpdateMaterialProperties();
                }
            }
        }

        [SerializeField]
        float _bubblesScale = 3f;

        public float bubblesScale {
            get { return _bubblesScale; }
            set {
                if (_bubblesScale != value) {
                    _bubblesScale = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        float _bubblesBrightness = 1;

        public float bubblesBrightness {
            get { return _bubblesBrightness; }
            set {
                if (_bubblesBrightness != value) {
                    _bubblesBrightness = value;
                    UpdateMaterialProperties();
                }
            }
        }


        [SerializeField]
        float _bubblesVerticalSpeed = 1;

        public float bubblesVerticalSpeed {
            get { return _bubblesVerticalSpeed; }
            set {
                if (_bubblesVerticalSpeed != value) {
                    _bubblesVerticalSpeed = value;
                    UpdateMaterialProperties();
                }
            }
        }



        [SerializeField]
        int _bubblesSeed = 1;

        public int bubblesSeed {
            get { return _bubblesSeed; }
            set {
                if (_bubblesSeed != value) {
                    _bubblesSeed = value;
                    requireBubblesUpdate = true;
                    UpdateMaterialProperties();
                }
            }
        }



        #endregion


        #region Point Light support

        [Serializable]
        public struct PointLightParams {
            [HideInInspector]
            public string name;
            [NonSerialized]
            public Vector3 localPosition;
            [NonSerialized]
            public Vector3 accel;
            [NonSerialized]
            public float nextTargetTime;

            public Vector3 targetPos;
            public bool randomTarget;
            public Vector3 randomTargetCenter;
            public float randomTargetRadius;
            public float nextTargetInterval;
            public float moveSpeed;
            public float lightRange;
            public float intensityMin, intensityMax;
            public float intensitySpeed;
            public Color color1, color2;
            public float colorSpeed;

        }


        [SerializeField]
        bool _pointLightsEnabled;

        public bool pointLightsEnabled {
            get { return _pointLightsEnabled; }
            set {
                if (_pointLightsEnabled != value) {
                    _pointLightsEnabled = value;
                    UpdateMaterialProperties();
                }
            }
        }

        public PointLightParams[] pointLightParams;

        public float pointLightsScatteringAmount = 0.8f;
        public float pointLightsIntensity = 1f;
        public float pointLightInsideAtten;


        #endregion
#if LIQUID_VOLUME_FP_RENDER_TEXTURES
        public static bool useFPRenderTextures => true;
#else
        public static bool useFPRenderTextures => false;
#endif

        // ---- INTERNAL CODE ----
        const int SHADER_KEYWORD_DEPTH_AWARE_INDEX = 0;
        const int SHADER_KEYWORD_DEPTH_AWARE_CUSTOM_PASS_INDEX = 1;
        const int SHADER_KEYWORD_IGNORE_GRAVITY_INDEX = 2;
        const int SHADER_KEYWORD_NON_AABB_INDEX = 3;
        const int SHADER_KEYWORD_TOPOLOGY_INDEX = 4;
        const int SHADER_KEYWORD_REFRACTION_INDEX = 5;

        const string SHADER_KEYWORD_DEPTH_AWARE = "LIQUID_VOLUME_DEPTH_AWARE";
        const string SHADER_KEYWORD_DEPTH_AWARE_CUSTOM_PASS = "LIQUID_VOLUME_DEPTH_AWARE_PASS";
        const string SHADER_KEYWORD_NON_AABB = "LIQUID_VOLUME_NON_AABB";
        const string SHADER_KEYWORD_IGNORE_GRAVITY = "LIQUID_VOLUME_IGNORE_GRAVITY";
        const string SHADER_KEYWORD_SPHERE = "LIQUID_VOLUME_SPHERE";
        const string SHADER_KEYWORD_CUBE = "LIQUID_VOLUME_CUBE";
        const string SHADER_KEYWORD_CYLINDER = "LIQUID_VOLUME_CYLINDER";
        const string SHADER_KEYWORD_IRREGULAR = "LIQUID_VOLUME_IRREGULAR";
        const string SHADER_KEYWORD_FP_RENDER_TEXTURE = "LIQUID_VOLUME_FP_RENDER_TEXTURES";
        const string SHADER_KEYWORD_USE_REFRACTION = "LIQUID_VOLUME_USE_REFRACTION";

        const string SPILL_POINT_GIZMO = "SpillPointGizmo";

        [NonSerialized]
        public Material liqMat;
        Material liqMatSimple, liqMatDefaultNoFlask, liqMatMultipleNoFlask;
        Mesh mesh;
        [NonSerialized]
        public Renderer mr;
        readonly static List<Material> mrSharedMaterials = new List<Material>();
        Vector3 lastPosition, lastScale;
        Quaternion lastRotation;
        string[] shaderKeywords;
        bool camInside;
        float lastDistanceToCam;
        DETAIL currentDetail;
        Vector4 turb, shaderTurb;
        float turbulenceSpeed, murkinessSpeed, bubblesVerticalSpeedAcum;
        float liquidLevelPos;
        bool shouldUpdateMaterialProperties;
        int currentNoiseVariation;
        float levelMultipled;

        // Multiple layers
        Color[] layersTextureColors, layersTextureColors2, layersTextureColorsTemp;
        Vector4[] layersProperties;
        Texture2D layersPropertiesTex, layersColorsTex, layersColors2Tex;
        Texture2D noise3DUnwrapped;
        Texture3D[] noise3DTex;
        Color[][] colors3D, colors3DWithBubbles;
        Color[] layersColorsTexContents, layersColors2TexContents;
        Color[] layersPropertiesTexContents;

        float[] layersSum;
        [NonSerialized]
        public bool requireLayersUpdate;
        [NonSerialized]
        public bool requireBubblesUpdate;
        int[] sortedLayers;
        int sortedLayersCount;
        int lastLayerCount;
        bool immediate;

        // Mesh info
        Vector3[] verticesUnsorted, verticesSorted;
        static Vector3[] rotatedVertices;
        int[] verticesIndices;
        float volumeRef, lastLevelVolumeRef;

        // Physics
        Vector3 prevVelocity, prev2Velocity, inertia, lastAvgVelocity;
        float angularVelocity, angularInertia;
        float turbulenceDueForces;
        Quaternion liquidRot;

        float prevThickness;

        // Spill point debug
        GameObject spillPointGizmo;
        static string[] defaultContainerNames = {
            "GLASS",
            "CONTAINER",
            "BOTTLE",
            "POTION",
            "FLASK",
            "LIQUID"
        };

        // Point light effects
        Color[] pointLightColorBuffer;
        Vector4[] pointLightPositionBuffer;
        int lastPointLightCount;


        #region Gameloop events


        void OnEnable() {
            if (!gameObject.activeInHierarchy)
                return;
            levelMultipled = _level * _levelMultiplier;
            turb.z = 1f;
            turbulenceDueForces = 0f;
            turbulenceSpeed = 1f;
            liquidRot = transform.rotation;
            currentDetail = _detail;
            currentNoiseVariation = -1;
            lastPosition = transform.position;
            lastRotation = transform.rotation;
            lastScale = transform.localScale;
            prevThickness = _flaskThickness;
            if (_depthAwareCustomPass && transform.parent == null) {
                _depthAwareCustomPass = false;
            }
            InitLayers();
            UpdateMaterialPropertiesNow();
            PointLightsInitialize();
            if (!Application.isPlaying) {
                if (_detail.isMultiple()) {
                    requireLayersUpdate = true;
                }
                shouldUpdateMaterialProperties = true;
            }
        }

        void Reset() {
            // Try to assign propert topology based on mesh
            if (mesh == null)
                return;

            if (mesh.vertexCount == 24) {
                topology = TOPOLOGY.Cube;
            } else {
                Renderer renderer = GetComponent<Renderer>();
                if (renderer == null) {
                    if (mesh.bounds.extents.y > mesh.bounds.extents.x) {
                        topology = TOPOLOGY.Cylinder;
                    }
                } else if (renderer.bounds.extents.y > renderer.bounds.extents.x) {
                    topology = TOPOLOGY.Cylinder;
                    if (!Application.isPlaying) {
                        if (transform.rotation.eulerAngles != Vector3.zero && (mesh.bounds.extents.y <= mesh.bounds.extents.x || mesh.bounds.extents.y <= mesh.bounds.extents.z)) {
                            Debug.LogWarning("Intrinsic model rotation detected. Consider using the Bake Transform and/or Center Pivot options in Advanced section.");
                        }
                    }

                }
            }
        }


        void OnDestroy() {

            RestoreOriginalMesh();

            liqMat = null;
            if (liqMatSimple != null) {
                DestroyImmediate(liqMatSimple);
                liqMatSimple = null;
            }
            if (liqMatDefaultNoFlask != null) {
                DestroyImmediate(liqMatDefaultNoFlask);
                liqMatDefaultNoFlask = null;
            }
            if (liqMatMultipleNoFlask != null) {
                DestroyImmediate(liqMatMultipleNoFlask);
                liqMatMultipleNoFlask = null;
            }

            if (noise3DTex != null) {
                for (int k = 0; k < noise3DTex.Length; k++) {
                    Texture3D tex = noise3DTex[k];
                    if (tex != null && tex.name.Contains("Clone")) {
                        DestroyImmediate(tex);
                        noise3DTex[k] = null;
                    }
                }
            }

            LiquidVolumeDepthPrePassRenderFeature.RemoveLiquidFromBackRenderers(this);
            LiquidVolumeDepthPrePassRenderFeature.RemoveLiquidFromFrontRenderers(this);

        }


        void RenderObject() {
            var act = gameObject.activeInHierarchy && enabled;

            if (shouldUpdateMaterialProperties || !Application.isPlaying) {
                shouldUpdateMaterialProperties = false;
                UpdateMaterialPropertiesNow();
            }

            if (act && _allowViewFromInside) {
                CheckInsideOut();
            }

            if (_detail.isMultiple()) {
                AnimateLayers();
            }

            UpdateAnimations();

            if (_pointLightsEnabled) {
                UpdateLightEffects();
            }

            if (!act || _topology != TOPOLOGY.Irregular) {
                LiquidVolumeDepthPrePassRenderFeature.RemoveLiquidFromBackRenderers(this);
            } else if (_topology == TOPOLOGY.Irregular) {
                LiquidVolumeDepthPrePassRenderFeature.AddLiquidToBackRenderers(this);
            }

            Transform parent = transform.parent;
            if (parent != null) {
                Renderer parentRenderer = GetComponentInParent<Renderer>();
                if (!act || !_depthAwareCustomPass) {
                    LiquidVolumeDepthPrePassRenderFeature.RemoveLiquidFromFrontRenderers(this);
                } else if (_depthAwareCustomPass) {
                    LiquidVolumeDepthPrePassRenderFeature.AddLiquidToFrontRenderers(this);
                }
            }
            if (_debugSpillPoint) {
                UpdateSpillPointGizmo();
            }

#if UNITY_EDITOR
            // Since Unity materials do not support color/vector arrays we need to submit them everytime to avoid losing the uniform values inside Unity Editor
            if (_detail.isMultiple()) {
                UploadMaterialArrays();
            }
#endif
        }

        public void OnWillRenderObject() {
            RenderObject();
        }

        void FixedUpdate() {
            turbulenceSpeed += Time.deltaTime * 3f * _speed;
            liqMat.SetFloat(ShaderParams.TurbulenceSpeed, turbulenceSpeed * 4f);
            murkinessSpeed += Time.deltaTime * 0.05f * (shaderTurb.x + shaderTurb.y);
            liqMat.SetFloat(ShaderParams.MurkinessSpeed, murkinessSpeed);
            bubblesVerticalSpeedAcum += Time.deltaTime * _bubblesVerticalSpeed;
            if (_detail.isMultiple()) {
                UpdateBubblesProperties();
            }
        }

        void UpdateBubblesProperties() {
            if (_bubblesBrightness < 0)
                _bubblesBrightness = 0;
            liqMat.SetVector(ShaderParams.BubblesData, new Vector4(100f / (1f + _bubblesScale), bubblesVerticalSpeedAcum, _bubblesBrightness, 0));
        }


        void OnDidApplyAnimationProperties() {  // support for animating property based fields
            shouldUpdateMaterialProperties = true;
        }

        #endregion

        #region Internal stuff

        struct MeshCache {
            public Vector3[] verticesSorted;
            public Vector3[] verticesUnsorted;
            public int[] indices;
        }

        static readonly Dictionary<Mesh, MeshCache> meshCache = new Dictionary<Mesh, MeshCache>();

        public void ClearMeshCache() {
            meshCache.Clear();
        }

        void ReadVertices() {
            if (mesh == null)
                return;

            if (!meshCache.TryGetValue(mesh, out MeshCache meshData)) {
                if (!mesh.isReadable) {
                    Debug.LogError("Mesh " + mesh.name + " is not readable. Please select your mesh and enable the Read/Write Enabled option.");
                }

                verticesUnsorted = mesh.vertices;
                verticesIndices = mesh.triangles;

                int vertexCount = verticesUnsorted.Length;

                if (verticesSorted == null || verticesSorted.Length != vertexCount) {
                    verticesSorted = new Vector3[vertexCount];
                }
                Array.Copy(verticesUnsorted, verticesSorted, vertexCount);
                Array.Sort(verticesSorted, vertexComparer);

                meshData.verticesUnsorted = verticesUnsorted;
                meshData.indices = verticesIndices;
                meshData.verticesSorted = verticesSorted;

                if (meshCache.Count > 64) {
                    // Clear cache to avoid memory overrun
                    ClearMeshCache();
                }
                meshCache[mesh] = meshData;
            } else {
                verticesUnsorted = meshData.verticesUnsorted;
                verticesIndices = meshData.indices;
                verticesSorted = meshData.verticesSorted;
            }
        }

        int vertexComparer(Vector3 v0, Vector3 v1) {
            if (v1.y < v0.y) return -1;
            if (v1.y > v0.y) return 1;
            return 0;
        }


        void UpdateAnimations() {
            // Check proper scale
            switch (topology) {
                case TOPOLOGY.Sphere:
                    if (transform.localScale.y != transform.localScale.x || transform.localScale.z != transform.localScale.x)
                        transform.localScale = new Vector3(transform.localScale.x, transform.localScale.x, transform.localScale.x);
                    break;
                case TOPOLOGY.Cylinder:
                    if (transform.localScale.z != transform.localScale.x)
                        transform.localScale = new Vector3(transform.localScale.x, transform.localScale.y, transform.localScale.x);
                    break;
            }


            if (liqMat != null) {
                Vector3 turbDir = Vector3.right;
                Quaternion rot = transform.rotation;
                if (_reactToForces) {
                    Quaternion instantRot = transform.rotation;
                    float dt = Time.smoothDeltaTime;
                    if (Application.isPlaying && dt > 0) {
                        Vector3 instantVelocity = (transform.position - lastPosition) / dt;
                        Vector3 avgVelocity = (prev2Velocity + prevVelocity + instantVelocity) / 3f;
                        prev2Velocity = prevVelocity;
                        prevVelocity = instantVelocity;

                        Vector3 instantAccel = (avgVelocity - lastAvgVelocity);
                        lastAvgVelocity = avgVelocity;
                        inertia += avgVelocity;

                        float accelMag = instantAccel.magnitude;
                        float force = Mathf.Max(accelMag / _physicsMass - _physicsAngularDamp, 0f);
                        angularInertia += force;
                        angularVelocity += angularInertia;
                        if (angularVelocity > 0) {
                            angularInertia -= Mathf.Abs(angularVelocity) * _physicsMass / 100f;
                        } else if (angularVelocity < 0) {
                            angularInertia += Mathf.Abs(angularVelocity) * _physicsMass / 100f;
                        }
                        float damp = 1f - _physicsAngularDamp;
                        angularInertia *= damp;
                        inertia *= damp;

                        float mag = Mathf.Clamp(angularVelocity, -90f, 90f);
                        if (inertia.x != 0 || inertia.z != 0) {
                            turbDir = inertia.normalized;
                        }
                        Vector3 axis = Vector3.Cross(turbDir, Vector3.down);
                        instantRot = Quaternion.AngleAxis(mag, axis);

                        float cinematic = Mathf.Abs(angularInertia) + Mathf.Abs(angularVelocity);
                        turbulenceDueForces = Mathf.Min(0.5f / _physicsMass, turbulenceDueForces + cinematic / 1000f);
                        turbulenceDueForces *= damp;
                    } else {
                        turbulenceDueForces = 0;
                    }

                    if (_topology == TOPOLOGY.Sphere) {
                        liquidRot = Quaternion.Lerp(liquidRot, instantRot, 0.1f);
                        rot = liquidRot;
                    }
                } else if (turbulenceDueForces > 0) {
                    turbulenceDueForces *= 0.1f;
                }
                Matrix4x4 m = Matrix4x4.TRS(Vector3.zero, rot, Vector3.one);
                liqMat.SetMatrix(ShaderParams.RotationMatrix, m.inverse);
                if (_topology != TOPOLOGY.Sphere) {
                    float tx = turbDir.x;
                    turbDir.x += (turbDir.z - turbDir.x) * 0.25f;
                    turbDir.z += (tx - turbDir.z) * 0.25f;
                }
                turb.z = turbDir.x;
                turb.w = turbDir.z;
            }

            bool hasRotated = transform.rotation != lastRotation;
            if (_reactToForces || hasRotated || transform.position != lastPosition || transform.localScale != lastScale) {
                UpdateLevels(hasRotated);
            }
        }


        public void PointLightsRandomize() {
            PointLightsInitialize(true);
        }

        void PointLightsInitialize(bool randomize = false) {
            if (pointLightParams == null) {
                pointLightParams = new PointLightParams[6];
                randomize = true;
            } else if (pointLightParams.Length > 6) {
                Array.Resize<PointLightParams>(ref pointLightParams, 6);
            }

            if (!_pointLightsEnabled) {
                UpdateLightEffects();
                return;
            }
            if (randomize) {
                for (int k = 0; k < pointLightParams.Length; k++) {
                    pointLightParams[k].localPosition = Vector3.zero;
                    pointLightParams[k].randomTargetCenter = new Vector3(0, -0.5f + _level * 0.5f, 0);
                    pointLightParams[k].color1 = new Color(Random.value, Random.value, Random.value);
                    pointLightParams[k].color2 = new Color(Random.value, Random.value, Random.value);
                    pointLightParams[k].colorSpeed = Random.Range(0.5f, 2f);
                    pointLightParams[k].intensityMin = Random.Range(0.5f, 1.5f);
                    pointLightParams[k].intensityMax = pointLightParams[k].intensityMin + Random.value * 1.5f;
                    pointLightParams[k].intensitySpeed = Random.Range(0.5f, 2f);
                    pointLightParams[k].lightRange = Random.Range(0.01f, 0.1f);
                    pointLightParams[k].moveSpeed = Random.value * 0.1f;
                    pointLightParams[k].accel = Vector3.zero;
                    pointLightParams[k].nextTargetInterval = 1f;
                    pointLightParams[k].randomTarget = true;
                    if (pointLightParams[k].randomTargetRadius == 0)
                        pointLightParams[k].randomTargetRadius = 0.4f;
                    SetNewLightTargetPos(k);
                }
            }
        }

        void SetNewLightTargetPos(int k) {
            pointLightParams[k].targetPos = pointLightParams[k].randomTargetCenter + Random.insideUnitSphere * pointLightParams[k].randomTargetRadius;
            pointLightParams[k].targetPos.x = Mathf.Clamp(pointLightParams[k].targetPos.x, -0.4f, 0.4f);
            pointLightParams[k].targetPos.y = Mathf.Clamp(pointLightParams[k].targetPos.y, -0.4f, 0.4f);
            pointLightParams[k].targetPos.z = Mathf.Clamp(pointLightParams[k].targetPos.z, -0.4f, 0.4f);
            pointLightParams[k].nextTargetTime = Time.time + Random.value + pointLightParams[k].nextTargetInterval;
        }

        void UpdateLightEffects() {
#if UNITY_EDITOR
            if (lastPointLightCount != pointLightParams.Length) {
                lastPointLightCount = pointLightParams.Length;
            }
            for (int k = 0; k < pointLightParams.Length; k++) {
                pointLightParams[k].name = "Light " + (k + 1);
            }
#endif
            // Animate lights
            for (int k = 0; k < pointLightParams.Length; k++) {
                Vector3 curPos = pointLightParams[k].localPosition;
                if (pointLightParams[k].randomTarget && Time.time > pointLightParams[k].nextTargetTime) {
                    SetNewLightTargetPos(k);
                }
                Vector3 targetPos = pointLightParams[k].targetPos;
                Vector3 accel = pointLightParams[k].accel;
                float speed = pointLightParams[k].moveSpeed * Time.deltaTime;
                accel.x += curPos.x < targetPos.x ? speed : -speed;
                const float maxAccel = 5f;
                if (accel.x > speed * maxAccel)
                    accel.x = speed * maxAccel;
                else if (accel.x < speed * -maxAccel) {
                    accel.x = speed * -maxAccel;
                }
                accel.y += curPos.y < targetPos.y ? speed : -speed;
                if (accel.y > speed * maxAccel)
                    accel.y = speed * maxAccel;
                else if (accel.y < speed * -maxAccel) {
                    accel.y = speed * -maxAccel;
                }
                accel.z += curPos.z < targetPos.z ? speed : -speed;
                if (accel.z > speed * maxAccel)
                    accel.z = speed * maxAccel;
                else if (accel.z < speed * -maxAccel) {
                    accel.z = speed * -maxAccel;
                }
                pointLightParams[k].accel = accel;
                curPos += pointLightParams[k].accel;
                if (curPos.x < -0.5) {
                    curPos.x = -0.5f;
                    pointLightParams[k].accel.x = 0;
                } else if (curPos.x > 0.5f) {
                    curPos.x = 0.5f;
                    pointLightParams[k].accel.x = 0;
                }
                if (curPos.y < -0.5) {
                    curPos.y = -0.5f;
                    pointLightParams[k].accel.y = 0;
                } else if (curPos.y > 0.5f) {
                    curPos.y = 0.5f;
                    pointLightParams[k].accel.y = 0;
                }
                if (curPos.z < -0.5) {
                    curPos.z = -0.5f;
                    pointLightParams[k].accel.z = 0;
                } else if (curPos.z > 0.5f) {
                    curPos.z = 0.5f;
                    pointLightParams[k].accel.z = 0;
                }
                pointLightParams[k].localPosition = curPos;
            }

            // Pass data to shader
            if (pointLightColorBuffer == null || pointLightColorBuffer.Length != 6) {
                pointLightColorBuffer = new Color[6];
            }
            if (pointLightPositionBuffer == null || pointLightPositionBuffer.Length != 6) {
                pointLightPositionBuffer = new Vector4[6];
            }
            float range, multiplier;
            for (int k = 0; k < 6; k++) {
                range = multiplier = 0;
                if (_pointLightsEnabled && k < pointLightParams.Length) {
                    Vector3 pos = transform.TransformPoint(pointLightParams[k].localPosition);
                    pointLightPositionBuffer[k] = pos;
                    range = pointLightParams[k].lightRange * pointLightsScatteringAmount / 25f; // note: 25 comes from Unity point light attenuation equation
                    float intensity = pointLightParams[k].intensityMin + Mathf.PingPong(Time.time * pointLightParams[k].intensitySpeed, pointLightParams[k].intensityMax - pointLightParams[k].intensityMin);
                    multiplier = pointLightsIntensity * intensity;
                }
                if (range > 0 && multiplier > 0) {
                    // Apply attenuation if light is affected by fog distance & falloff
                    Color pointLightColor = Color.Lerp(pointLightParams[k].color1, pointLightParams[k].color2, Mathf.PingPong(Time.time * pointLightParams[k].colorSpeed, 1f));
                    pointLightColorBuffer[k] = new Vector4(pointLightColor.r * multiplier, pointLightColor.g * multiplier, pointLightColor.b * multiplier, range);
                } else {
                    pointLightColorBuffer[k] = Color.black;
                }
            }

            if (liqMat != null) {
                liqMat.SetFloat(ShaderParams.PointLightInsideAtten, pointLightInsideAtten);
                liqMat.SetColorArray(ShaderParams.PointLightColorArray, pointLightColorBuffer);
                liqMat.SetVectorArray(ShaderParams.PointLightPositionArray, pointLightPositionBuffer);
            }

        }

        public void UpdateMaterialProperties() {
            if (Application.isPlaying) {
                shouldUpdateMaterialProperties = true;
            } else {
                UpdateMaterialPropertiesNow();
            }

        }

        void UpdateMaterialPropertiesNow() {
            if (!gameObject.activeInHierarchy)
                return;

            switch (_detail) {
                case DETAIL.Simple:
                case DETAIL.SimpleNoFlask:
                    if (liqMatSimple == null) {
                        liqMatSimple = Instantiate(Resources.Load<Material>("Materials/LiquidVolumeSimple")) as Material;
                    }
                    liqMat = liqMatSimple;
                    break;
                case DETAIL.Multiple:
                case DETAIL.MultipleNoFlask:
                    if (liqMatMultipleNoFlask == null) {
                        liqMatMultipleNoFlask = Instantiate(Resources.Load<Material>("Materials/LiquidVolumeMultipleNoFlask")) as Material;
                    }
                    liqMat = liqMatMultipleNoFlask;
                    break;
                default:
                    if (liqMatDefaultNoFlask == null) {
                        liqMatDefaultNoFlask = Instantiate(Resources.Load<Material>("Materials/LiquidVolumeDefaultNoFlask")) as Material;
                    }
                    liqMat = liqMatDefaultNoFlask;
                    break;
            }

            if (_flaskMaterial == null) {
                _flaskMaterial = Instantiate(Resources.Load<Material>("Materials/Flask"));
            }

            if (liqMat == null)
                return;

            CheckMeshDisplacement();

            if (currentDetail != _detail) {
                currentDetail = _detail;
                requireLayersUpdate = true;
            }

            if (requireLayersUpdate && _detail.isMultiple()) {
                UpdateLayersNow();
            }

            UpdateLevels();
            if (mr == null) 
                return;

            // Try to compute submesh index heuristically if this is the first time the liquid has been added to a multi-material mesh
            mr.GetSharedMaterials(mrSharedMaterials);
            int sharedMaterialsCount = mrSharedMaterials.Count;
            if (_subMeshIndex < 0) {
                for (int w = 0; w < defaultContainerNames.Length; w++) {
                    if (_subMeshIndex >= 0)
                        break;
                    for (int k = 0; k < sharedMaterialsCount; k++) {
                        if (mrSharedMaterials[k] != null && mrSharedMaterials[k] != _flaskMaterial && mrSharedMaterials[k].name.ToUpper().Contains(defaultContainerNames[w])) {
                            _subMeshIndex = k;
                            break;
                        }
                    }
                }
            }
            if (_subMeshIndex < 0)
                _subMeshIndex = 0;

            if (sharedMaterialsCount > 1 && _subMeshIndex >= 0 && _subMeshIndex < sharedMaterialsCount) {
                mrSharedMaterials[_subMeshIndex] = liqMat;
            } else {
                mrSharedMaterials.Clear();
                mrSharedMaterials.Add(liqMat);
            }

            if (_flaskMaterial != null) {
                bool shouldUseFlaskMaterial = _detail.usesFlask();
                if (shouldUseFlaskMaterial && !mrSharedMaterials.Contains(_flaskMaterial)) {
                    // empty slot?
                    for (int k=0;k<mrSharedMaterials.Count;k++) {
                        if (mrSharedMaterials[k] == null) {
                            mrSharedMaterials[k] = _flaskMaterial;
                            shouldUseFlaskMaterial = false;
                        }
                    }
                    if (shouldUseFlaskMaterial) {
                        mrSharedMaterials.Add(_flaskMaterial);
                    }
                } else if (!shouldUseFlaskMaterial && mrSharedMaterials.Contains(_flaskMaterial)) {
                    mrSharedMaterials.Remove(_flaskMaterial);
                }
                _flaskMaterial.renderQueue = _renderQueue + 1;
            }
            mr.sharedMaterials = mrSharedMaterials.ToArray();

            liqMat.SetFloat(ShaderParams.DitherStrength, _ditherStrength);
            liqMat.SetColor(ShaderParams.Color1, ApplyGlobalAlpha(_liquidColor1));
            liqMat.SetColor(ShaderParams.Color2, ApplyGlobalAlpha(_liquidColor2));
            liqMat.SetColor(ShaderParams.EmissionColor, _emissionColor);

            if (_useLightColor && _directionalLight != null) {
                Color lightColor = _directionalLight.color;
                liqMat.SetColor(ShaderParams.LightColor, lightColor);
            } else {
                liqMat.SetColor(ShaderParams.LightColor, Color.white);
            }

            int scatteringPower = _scatteringPower;
            float scatteringAmount = _scatteringAmount;
            if (!_scatteringEnabled) {
                scatteringPower = 0;
                scatteringAmount = 0;
            }
            liqMat.SetVector(ShaderParams.GlossinessInt, new Vector4((1f - _glossinessInternal) * 96f + 1f, Mathf.Pow(2, scatteringPower), scatteringAmount, _glossinessInternal));
            liqMat.SetFloat(ShaderParams.DoubleSidedBias, _doubleSidedBias);
            liqMat.SetFloat(ShaderParams.BackDepthBias, -_backDepthBias);

            liqMat.SetFloat(ShaderParams.Muddy, _murkiness);
            liqMat.SetFloat(ShaderParams.Alpha, _alpha);

            float alphaCombined = _alpha * Mathf.Clamp01((_liquidColor1.a + _liquidColor2.a) * 4f);
            if (_ditherShadows) {
                liqMat.SetFloat(ShaderParams.AlphaCombined, alphaCombined);
            } else {
                liqMat.SetFloat(ShaderParams.AlphaCombined, alphaCombined > 0 ? 1000f : 0f);
            }

            liqMat.SetFloat(ShaderParams.SparklingIntensity, _sparklingIntensity * 250.0f);
            liqMat.SetFloat(ShaderParams.SparklingThreshold, 1.0f - _sparklingAmount);
            liqMat.SetFloat(ShaderParams.DepthAtten, _deepObscurance);
            Color smokeColor = ApplyGlobalAlpha(_smokeColor);
            int smokeRaySteps = _smokeRaySteps;
            if (!_smokeEnabled) {
                smokeColor.a = 0;
                smokeRaySteps = 1;
            }
            liqMat.SetColor(ShaderParams.SmokeColor, smokeColor);
            liqMat.SetFloat(ShaderParams.SmokeAtten, _smokeBaseObscurance);
            liqMat.SetFloat(ShaderParams.SmokeHeightAtten, _smokeHeightAtten);
            liqMat.SetFloat(ShaderParams.SmokeSpeed, _smokeSpeed);
            liqMat.SetFloat(ShaderParams.SmokeRaySteps, smokeRaySteps);

            liqMat.SetFloat(ShaderParams.LiquidRaySteps, _liquidRaySteps);

            liqMat.SetColor(ShaderParams.FoamColor, ApplyGlobalAlpha(_foamColor));
            liqMat.SetFloat(ShaderParams.FoamRaySteps, _foamThickness > 0 ? _foamRaySteps : 1);
            liqMat.SetFloat(ShaderParams.FoamDensity, _foamThickness > 0 ? _foamDensity : -1f);
            liqMat.SetFloat(ShaderParams.FoamWeight, _foamWeight);
            liqMat.SetFloat(ShaderParams.FoamBottom, _foamVisibleFromBottom ? 1f : 0f);
            liqMat.SetFloat(ShaderParams.FoamTurbulence, _foamTurbulence);

            switch (_detail) {
                case DETAIL.Multiple:
                case DETAIL.MultipleNoFlask:
                    UpdateBubblesProperties();
                    break;
            }

            if (_bubblesAmount < 0) {
                _bubblesAmount = 0;
            }

            if (_noiseVariation != currentNoiseVariation || requireBubblesUpdate) {
                requireBubblesUpdate = false;
                currentNoiseVariation = _noiseVariation;
                if (noise3DTex == null || noise3DTex.Length != 4) {
                    noise3DTex = new Texture3D[4];
                }
                if (noise3DTex[currentNoiseVariation] == null) {
                    noise3DTex[currentNoiseVariation] = Resources.Load<Texture3D>("Textures/Noise3D" + currentNoiseVariation.ToString());
                }
                Texture3D tex3d = noise3DTex[currentNoiseVariation];
                if (tex3d != null) {
                    bool needsBubbleTextureUpdate = _bubblesAmount > 0 || (_bubblesAmount == 0 && tex3d != null && tex3d.name.Contains("Clone"));
                    if (needsBubbleTextureUpdate && _detail.isMultiple()) {
                        if (!tex3d.name.Contains("Clone")) {
                            tex3d = Instantiate(tex3d);
                            tex3d.hideFlags = HideFlags.DontSave;
                            noise3DTex[currentNoiseVariation] = tex3d;
                        }

                        if (colors3D == null || colors3D.Length != 4) {
                            colors3D = new Color[4][];
                        }
                        if (colors3D[currentNoiseVariation] == null) {
                            colors3D[currentNoiseVariation] = tex3d.GetPixels();
                            int length = colors3D[currentNoiseVariation].Length;
                            if (colors3DWithBubbles == null || colors3DWithBubbles.Length != 4) {
                                colors3DWithBubbles = new Color[4][];
                            }
                            if (colors3DWithBubbles[currentNoiseVariation] == null || colors3DWithBubbles[currentNoiseVariation].Length != length) {
                                colors3DWithBubbles[currentNoiseVariation] = new Color[length];
                            }
                        }
                        if (_bubblesAmount > 0 && _detail.isMultiple()) {
                            AddBubbles(tex3d);
                        } else {
                            ClearBubbles(tex3d);
                        }
                    }
                    liqMat.SetTexture(ShaderParams.NoiseTex, tex3d);
                }
            }

            liqMat.renderQueue = _renderQueue;

            UpdateInsideOut();

            if (_topology == TOPOLOGY.Irregular) {
                if (prevThickness != _flaskThickness) {
                    prevThickness = _flaskThickness;
                }
            }
        }

        Color ApplyGlobalAlpha(Color originalColor) {
            return new Color(originalColor.r, originalColor.g, originalColor.b, originalColor.a * _alpha);
        }


        void GetRenderer() {
            MeshFilter mf = GetComponent<MeshFilter>();
            if (mf != null) {
                mesh = mf.sharedMesh;
                mr = GetComponent<MeshRenderer>();
            } else {
                SkinnedMeshRenderer smr = GetComponent<SkinnedMeshRenderer>();
                if (smr != null) {
                    mesh = smr.sharedMesh;
                    mr = smr;
                }
            }
        }

        void UpdateLevels(bool updateShaderKeywords = true) {

            _level = Mathf.Clamp01(_level);
            levelMultipled = _level * _levelMultiplier;

            if (liqMat == null)
                return;

            if (mesh == null) {
                GetRenderer();
                ReadVertices();
            } else if (mr == null) {
                GetRenderer();
            }
            if (mesh == null || mr == null) {
                return;
            }

            Vector4 size = new Vector4(mesh.bounds.extents.x * 2f * transform.lossyScale.x, mesh.bounds.extents.y * 2f * transform.lossyScale.y, mesh.bounds.extents.z * 2f * transform.lossyScale.z, 0);
            size.x *= _extentsScale.x;
            size.y *= _extentsScale.y;
            size.z *= _extentsScale.z;
            float maxWidth = Mathf.Max(size.x, size.z);

            Vector3 extents = _ignoreGravity ? new Vector3(size.x * 0.5f, size.y * 0.5f, size.z * 0.5f) : mr.bounds.extents;
            extents *= (1f - _flaskThickness);
            extents.x *= _extentsScale.x;
            extents.y *= _extentsScale.y;
            extents.z *= _extentsScale.z;

            // Compensate levelpos with upperlimit
            float rotationAdjustment;
            if (_upperLimit < 1f && !_ignoreGravity) {
                float y1 = transform.TransformPoint(Vector3.up * extents.y).y;
                float y0 = transform.TransformPoint(Vector3.up * (extents.y * _upperLimit)).y;
                rotationAdjustment = Mathf.Max(y0 - y1, 0);
            } else {
                rotationAdjustment = 0;
            }

#if DEBUG_SLICE
            float approxVolume = 0;
#endif


            // Compensate rotation in cylindrical shapes where mesh height is on another scale than width
            float thisLevel = levelMultipled;
            if (_rotationLevelCompensation != LEVEL_COMPENSATION.None && !_ignoreGravity && thisLevel > 0) {

                MeshVolumeCalcFunction volumeFunction;
                int exitIterations;

                if (_rotationLevelCompensation == LEVEL_COMPENSATION.Fast) {
                    volumeFunction = GetMeshVolumeUnderLevelFast;
                    exitIterations = 8;
                } else {
                    volumeFunction = GetMeshVolumeUnderLevel;
                    exitIterations = 10;
                }

                Vector3 mrCenter = mr.bounds.center;
                if (lastLevelVolumeRef != thisLevel) {
                    lastLevelVolumeRef = thisLevel;

                    if (_topology == TOPOLOGY.Cylinder) {
                        float r = size.x * 0.5f;
                        float h = size.y * thisLevel;
                        volumeRef = Mathf.PI * r * r * h; // perfect cylinder volume (optimization for performance reasons, ideally should go through normal mesh volume computation - the else part)
                    } else {
                        Quaternion q = transform.rotation;
                        transform.rotation = Quaternion.identity;

                        float tempExtentY = _ignoreGravity ? size.y * 0.5f : mr.bounds.extents.y;
                        tempExtentY *= (1f - _flaskThickness);
                        tempExtentY *= _extentsScale.y;

                        RotateVertices();
                        volumeRef = volumeFunction(thisLevel, mrCenter, tempExtentY);
                        transform.rotation = q;
                    }
                }
                RotateVertices();

                float nearestLevel = thisLevel;
                float minVolumeDiff = float.MaxValue;

                float maxLevel = Mathf.Clamp01(thisLevel + 0.5f);
                float minLevel = Mathf.Clamp01(thisLevel - 0.5f);
                for (int i = 0; i < 12; i++) {
                    thisLevel = (minLevel + maxLevel) * 0.5f;
                    float volume = volumeFunction(thisLevel, mrCenter, extents.y);
                    float volumeDiff = Mathf.Abs(volumeRef - volume);
                    if (volumeDiff < minVolumeDiff) {
                        #if DEBUG_SLICE
                            approxVolume = volume;
                        #endif
                        minVolumeDiff = volumeDiff;
                        nearestLevel = thisLevel;
                    }
                    if (volume < volumeRef) {
                        minLevel = thisLevel;
                    } else {
                        if (i >= exitIterations)
                            break;
                        maxLevel = thisLevel;
                    }
                }

                thisLevel = nearestLevel * _levelMultiplier;

            } else {
                if (levelMultipled <= 0)
                    thisLevel = -0.001f; // ensure it's below the flask thickness
            }

            liquidLevelPos = mr.bounds.center.y - extents.y;
            liquidLevelPos += extents.y * 2f * thisLevel + rotationAdjustment;

#if DEBUG_SLICE
            Debug.Log("Ref: " + volumeRef + " Vol: " + approxVolume + " Level: " + thisLevel);
#endif

            liqMat.SetFloat(ShaderParams.LevelPos, liquidLevelPos);
            float upperLimit = mesh.bounds.extents.y * _extentsScale.y * _upperLimit;
            liqMat.SetFloat(ShaderParams.UpperLimit, upperLimit);
            float lowerLimit = mesh.bounds.extents.y * _extentsScale.y * _lowerLimit;
            liqMat.SetFloat(ShaderParams.LowerLimit, lowerLimit);
            float visibleLevel = (levelMultipled <= 0 || levelMultipled >= 1f) ? 0f : 1f;
            UpdateTurbulence();
            float foamPos = mr.bounds.center.y - extents.y + (rotationAdjustment + extents.y * 2.0f * (thisLevel + _foamThickness)) * visibleLevel;
            liqMat.SetFloat(ShaderParams.FoamMaxPos, foamPos);
            Vector4 thickness = new Vector4(1.0f - _flaskThickness, (1.0f - _flaskThickness * maxWidth / size.z), (1.0f - _flaskThickness * maxWidth / size.z), 0);
            liqMat.SetVector(ShaderParams.FlaskThickness, thickness);
            size.w = size.x * 0.5f * thickness.x;
            size.x = Vector3.Distance(mr.bounds.max, mr.bounds.min);
            liqMat.SetVector(ShaderParams.Size, size);
            float scaleFactor = size.y * 0.5f * (1.0f - _flaskThickness * maxWidth / size.y);
            liqMat.SetVector(ShaderParams.Scale, new Vector4(_smokeScale / scaleFactor, _foamScale / scaleFactor, _liquidScale1 / scaleFactor, _liquidScale2 / scaleFactor));
            liqMat.SetVector(ShaderParams.Center, transform.position);

            if (shaderKeywords == null || shaderKeywords.Length != 6) {
                shaderKeywords = new string[6];
            }
            for (int k = 0; k < shaderKeywords.Length; k++) {
                shaderKeywords[k] = null;
            }

            if (_detail.isMultiple()) {
                liqMat.SetFloat(ShaderParams.SizeWorld, mr.bounds.size.y);
            }

            if (_depthAware) {
                shaderKeywords[SHADER_KEYWORD_DEPTH_AWARE_INDEX] = SHADER_KEYWORD_DEPTH_AWARE;
                liqMat.SetFloat(ShaderParams.DepthAwareOffset, _depthAwareOffset);
            }

            if (_depthAwareCustomPass) {
                shaderKeywords[SHADER_KEYWORD_DEPTH_AWARE_CUSTOM_PASS_INDEX] = SHADER_KEYWORD_DEPTH_AWARE_CUSTOM_PASS;
            }

            if (_reactToForces && _topology == TOPOLOGY.Sphere) {
                shaderKeywords[SHADER_KEYWORD_IGNORE_GRAVITY_INDEX] = SHADER_KEYWORD_IGNORE_GRAVITY;
            } else if (_ignoreGravity) {
                shaderKeywords[SHADER_KEYWORD_IGNORE_GRAVITY_INDEX] = SHADER_KEYWORD_IGNORE_GRAVITY;
            } else if (transform.rotation.eulerAngles != Vector3.zero) {
                shaderKeywords[SHADER_KEYWORD_NON_AABB_INDEX] = SHADER_KEYWORD_NON_AABB;
            }
            switch (_topology) {
                case TOPOLOGY.Sphere:
                    shaderKeywords[SHADER_KEYWORD_TOPOLOGY_INDEX] = SHADER_KEYWORD_SPHERE;
                    break;
                case TOPOLOGY.Cube:
                    shaderKeywords[SHADER_KEYWORD_TOPOLOGY_INDEX] = SHADER_KEYWORD_CUBE;
                    break;
                case TOPOLOGY.Cylinder:
                    shaderKeywords[SHADER_KEYWORD_TOPOLOGY_INDEX] = SHADER_KEYWORD_CYLINDER;
                    break;
                default:
                    shaderKeywords[SHADER_KEYWORD_TOPOLOGY_INDEX] = SHADER_KEYWORD_IRREGULAR;
                    break;
            }
            if (_refractionBlur && _detail.allowsRefraction()) {
                liqMat.SetFloat(ShaderParams.FlaskBlurIntensity, _blurIntensity * (_refractionBlur ? 1f : 0f));
                shaderKeywords[SHADER_KEYWORD_REFRACTION_INDEX] = SHADER_KEYWORD_USE_REFRACTION;
            }
            if (updateShaderKeywords) {
                liqMat.shaderKeywords = shaderKeywords;
            }

            lastPosition = transform.position;
            lastScale = transform.localScale;
            lastRotation = transform.rotation;
        }


        void RotateVertices() {
            int vertexCount = verticesUnsorted.Length;
            if (rotatedVertices == null || rotatedVertices.Length != vertexCount) {
                rotatedVertices = new Vector3[vertexCount];
            }
            for (int k = 0; k < vertexCount; k++) {
                rotatedVertices[k] = transform.TransformPoint(verticesUnsorted[k]);
            }
        }


        float SignedVolumeOfTriangle(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 zeroPoint) {
            // zero point can be useful to avoid floating point issues if mesh is at very distant positions but in general this can be omitted
            p1.x -= zeroPoint.x; p1.y -= zeroPoint.y; p1.z -= zeroPoint.z;
            p2.x -= zeroPoint.x; p2.y -= zeroPoint.y; p2.z -= zeroPoint.z;
            p3.x -= zeroPoint.x; p3.y -= zeroPoint.y; p3.z -= zeroPoint.z;
            float v321 = p3.x * p2.y * p1.z;
            float v231 = p2.x * p3.y * p1.z;
            float v312 = p3.x * p1.y * p2.z;
            float v132 = p1.x * p3.y * p2.z;
            float v213 = p2.x * p1.y * p3.z;
            float v123 = p1.x * p2.y * p3.z;
            float e = (1.0f / 6.0f) * (-v321 + v231 + v312 - v132 - v213 + v123);
            return e;
        }


        delegate float MeshVolumeCalcFunction(float level01, Vector3 zeroPoint, float yExtent);

        float GetMeshVolumeUnderLevelFast(float level01, Vector3 zeroPoint, float yExtent) {

            float level = mr.bounds.center.y - yExtent;
            level += yExtent * 2f * level01;

            float vol = 0;
            for (int k = 0; k < verticesIndices.Length; k += 3) {
                Vector3 p1 = rotatedVertices[verticesIndices[k]];
                Vector3 p2 = rotatedVertices[verticesIndices[k + 1]];
                Vector3 p3 = rotatedVertices[verticesIndices[k + 2]];
                if (p1.y > level) p1.y = level;
                if (p2.y > level) p2.y = level;
                if (p3.y > level) p3.y = level;
                vol += SignedVolumeOfTriangle(p1, p2, p3, zeroPoint);
            }
            return Mathf.Abs(vol);
        }

        Vector3 ClampVertexToSlicePlane(Vector3 p, Vector3 q, float level) {
            Vector3 qp = (q - p).normalized;
            float h = p.y - level;
            return p + qp * h / -qp.y;
        }


#if DEBUG_SLICE
        GameObject o;
#endif
        readonly List<Vector3> verts = new List<Vector3>();
        readonly List<Vector3> cutPoints = new List<Vector3>();
        Vector3 cutPlaneCenter;

        float GetMeshVolumeUnderLevel(float level01, Vector3 zeroPoint, float yExtent) {

            float level = mr.bounds.center.y - yExtent;
            level += yExtent * 2f * level01;

            cutPlaneCenter = Vector3.zero;
            cutPoints.Clear();
            verts.Clear();

            // Slice mesh
            int indicesCount = verticesIndices.Length;
            for (int k = 0; k < indicesCount; k += 3) {
                Vector3 p1 = rotatedVertices[verticesIndices[k]];
                Vector3 p2 = rotatedVertices[verticesIndices[k + 1]];
                Vector3 p3 = rotatedVertices[verticesIndices[k + 2]];

                if (p1.y > level && p2.y > level && p3.y > level) continue;

                if (p1.y < level && p2.y > level && p3.y > level) {
                    p2 = ClampVertexToSlicePlane(p2, p1, level);
                    p3 = ClampVertexToSlicePlane(p3, p1, level);
                    cutPoints.Add(p2);
                    cutPoints.Add(p3);
                    cutPlaneCenter += p2;
                    cutPlaneCenter += p3;
                } else if (p2.y < level && p1.y > level && p3.y > level) {
                    p1 = ClampVertexToSlicePlane(p1, p2, level);
                    p3 = ClampVertexToSlicePlane(p3, p2, level);
                    cutPoints.Add(p1);
                    cutPoints.Add(p3);
                    cutPlaneCenter += p1;
                    cutPlaneCenter += p3;
                } else if (p3.y < level && p1.y > level && p2.y > level) {
                    p1 = ClampVertexToSlicePlane(p1, p3, level);
                    p2 = ClampVertexToSlicePlane(p2, p3, level);
                    cutPoints.Add(p1);
                    cutPoints.Add(p2);
                    cutPlaneCenter += p1;
                    cutPlaneCenter += p2;
                } else if (p1.y > level && p2.y < level && p3.y < level) {
                    Vector3 p12 = ClampVertexToSlicePlane(p1, p2, level);
                    Vector3 p13 = ClampVertexToSlicePlane(p1, p3, level);
                    verts.Add(p12);
                    verts.Add(p2);
                    verts.Add(p3);
                    verts.Add(p13);
                    verts.Add(p12);
                    verts.Add(p3);
                    cutPoints.Add(p12);
                    cutPoints.Add(p13);
                    cutPlaneCenter += p12;
                    cutPlaneCenter += p13;
                    continue;
                } else if (p2.y > level && p1.y < level && p3.y < level) {
                    Vector3 p21 = ClampVertexToSlicePlane(p2, p1, level);
                    Vector3 p23 = ClampVertexToSlicePlane(p2, p3, level);
                    verts.Add(p1);
                    verts.Add(p21);
                    verts.Add(p3);
                    verts.Add(p21);
                    verts.Add(p23);
                    verts.Add(p3);
                    cutPoints.Add(p21);
                    cutPoints.Add(p23);
                    cutPlaneCenter += p21;
                    cutPlaneCenter += p23;
                    continue;
                } else if (p3.y > level && p1.y < level && p2.y < level) {
                    Vector3 p31 = ClampVertexToSlicePlane(p3, p1, level);
                    Vector3 p32 = ClampVertexToSlicePlane(p3, p2, level);
                    verts.Add(p31);
                    verts.Add(p1);
                    verts.Add(p2);
                    verts.Add(p32);
                    verts.Add(p31);
                    verts.Add(p2);
                    cutPoints.Add(p31);
                    cutPoints.Add(p32);
                    cutPlaneCenter += p31;
                    cutPlaneCenter += p32;
                    continue;
                }
                verts.Add(p1);
                verts.Add(p2);
                verts.Add(p3);
            }

            // close sliced mesh
            int cutPointsCount = cutPoints.Count;
            if (cutPoints.Count >= 3) {
                cutPlaneCenter /= cutPointsCount;
                cutPoints.Sort(PolygonSortOnPlane);

                for (int k = 0; k < cutPointsCount; k++) {
                    Vector3 p1 = cutPoints[k];
                    Vector3 p2;
                    if (k == cutPointsCount - 1) {
                        p2 = cutPoints[0];
                    } else {
                        p2 = cutPoints[k + 1];
                    }
                    verts.Add(cutPlaneCenter);
                    verts.Add(p1);
                    verts.Add(p2);
                }
            }

            // compute mesh volume
            int vertCount = verts.Count;
            float vol = 0;
            for (int k = 0; k < vertCount; k += 3) {
                vol += SignedVolumeOfTriangle(verts[k], verts[k + 1], verts[k + 2], zeroPoint);
            }

#if DEBUG_SLICE
            if (o == null) {
                o = new GameObject("Sliced", typeof(MeshFilter), typeof(MeshRenderer));
            }

            Mesh mesh = new Mesh();
            mesh.vertices = verts.ToArray();
            int len = mesh.vertices.Length;
            int[] inds = new int[len];
            for (int k = 0; k < inds.Length; k++) {
                inds[k] = k;
            }
            mesh.triangles = inds;
            MeshFilter mf = o.GetComponent<MeshFilter>();
            mf.mesh = mesh;
#endif

            return Mathf.Abs(vol);
        }

        int PolygonSortOnPlane(Vector3 p1, Vector3 p2) {
            float r1 = Mathf.Atan2(p1.x - cutPlaneCenter.x, p1.z - cutPlaneCenter.z);
            float r2 = Mathf.Atan2(p2.x - cutPlaneCenter.x, p2.z - cutPlaneCenter.z);
            if (r1 < r2) return -1;
            if (r1 > r2) return 1;
            return 0;
        }

        void UpdateTurbulence() {
            if (liqMat == null)
                return;
            float visibleLevel = levelMultipled > 0 ? 1f: 0f; // (_level<=0 || _level>=1f) ? 0.1f: 1f;	// commented out to allow animation even level is 0 or full
            float isInsideContainer = (camInside && _allowViewFromInside) ? 0f : 1f;
            turb.x = _turbulence1 * visibleLevel * isInsideContainer;
            turb.y = Mathf.Max(_turbulence2, turbulenceDueForces) * visibleLevel * isInsideContainer;
            shaderTurb = turb;
            shaderTurb.z *= 3.1415927f * _frecuency * 4f;
            shaderTurb.w *= 3.1415927f * _frecuency * 4f;
            liqMat.SetVector(ShaderParams.Turbulence, shaderTurb);
        }

        void CheckInsideOut() {
            Camera cam = Camera.current;
            if (cam == null || mr == null) {
                if (!_allowViewFromInside)
                    UpdateInsideOut();
                return;
            }

            Vector3 currentCamPos = cam.transform.position + cam.transform.forward * cam.nearClipPlane;
            float currentDistanceToCam = (currentCamPos - transform.position).sqrMagnitude;
            if (currentDistanceToCam == lastDistanceToCam)
                return;
            lastDistanceToCam = currentDistanceToCam;

            // Check if position is inside container
            bool nowInside = false;
            switch (_topology) {
                case TOPOLOGY.Cube:
                    nowInside = PointInAABB(currentCamPos);
                    break;
                case TOPOLOGY.Cylinder:
                    nowInside = PointInCylinder(currentCamPos);
                    break;
                default:
                    float diam = mesh.bounds.extents.x * 2f;
                    nowInside = (currentCamPos - transform.position).sqrMagnitude < (diam * diam);
                    break;
            }

            if (nowInside != camInside) {
                camInside = nowInside;
                UpdateInsideOut();
            }
        }


        bool PointInAABB(Vector3 point) {
            point = transform.InverseTransformPoint(point);
            Vector3 ext = mesh.bounds.extents;
            if (point.x < ext.x && point.x > -ext.x &&
                point.y < ext.y && point.y > -ext.y &&
                point.z < ext.z && point.z > -ext.z) {
                return true;
            } else {
                return false;
            }
        }

        bool PointInCylinder(Vector3 point) {
            point = transform.InverseTransformPoint(point);
            Vector3 ext = mesh.bounds.extents;
            if (point.x < ext.x && point.x > -ext.x &&
                point.y < ext.y && point.y > -ext.y &&
                point.z < ext.z && point.z > -ext.z) {

                point.y = 0;
                Vector3 currentPos = transform.position;
                currentPos.y = 0;
                return (point - currentPos).sqrMagnitude < ext.x * ext.x;
            }
            return false;
        }


        void UpdateInsideOut() {
            if (liqMat == null)
                return;
            if (_allowViewFromInside && camInside) {
                liqMat.SetInt(ShaderParams.CullMode, (int)CullMode.Front);
                liqMat.SetInt(ShaderParams.ZTestMode, (int)CompareFunction.Always);
                if (_flaskMaterial != null) {
                    _flaskMaterial.SetInt(ShaderParams.CullMode, (int)CullMode.Front);
                    _flaskMaterial.SetInt(ShaderParams.ZTestMode, (int)CompareFunction.Always);
                }
            } else {
                liqMat.SetInt(ShaderParams.CullMode, (int)CullMode.Back);
                liqMat.SetInt(ShaderParams.ZTestMode, (int)CompareFunction.LessEqual);
                if (_flaskMaterial != null) {
                    _flaskMaterial.SetInt(ShaderParams.CullMode, (int)CullMode.Back);
                    _flaskMaterial.SetInt(ShaderParams.ZTestMode, (int)CompareFunction.LessEqual);
                }
            }
            UpdateTurbulence();
        }

        #endregion


        #region Public API

        /// <summary>
        /// Returns the vertical position in world space coordinates of the liquid surface
        /// </summary>
        /// <value>The get liquid surface Y position.</value>
        public float liquidSurfaceYPosition {
            get {
                return liquidLevelPos;
            }
        }

        /// <summary>
        /// Changes the transform position and/or rotation of a floating object.
        /// </summary>
        public void MoveToLiquidSurface(Transform transform, BuoyancyEffect effect, Transform root = null, float dampen = 0.8f) {
            Vector3 position = transform.position;
            if (effect == BuoyancyEffect.Simple) {
                position.y = liquidLevelPos;
                transform.position = position;
                return;
            }
            if (effect == BuoyancyEffect.PositionOnly) {
                Vector4 shaderTurb = liqMat.GetVector(ShaderParams.Turbulence);
                float sizeY = liqMat.GetVector(ShaderParams.Size).y;
                float turbFactor = shaderTurb.y * 0.05f * sizeY * _foamTurbulence;
                float turbulence1 = Mathf.Sin(transform.position.x * shaderTurb.z + transform.position.z * shaderTurb.w + turbulenceSpeed * 4f) * turbFactor;
                float y1 = liquidLevelPos + turbulence1;
                position.y = y1;
                transform.position = position;
                return;
            }

            // Position and rotation
            if (effect == BuoyancyEffect.PositionAndRotation) {
                if (root == null) {
                    Debug.LogError("No root specified when calling MoveToLiquidSurface(..). Usually the root is a parent object that receives the buoyancy position and rotation changes.");
                }
                Vector4 shaderTurb = liqMat.GetVector(ShaderParams.Turbulence);
                Collider collider = transform.GetComponent<Collider>();
                if (collider == null) {
                    Debug.LogError("No collider found in object used when calling MoveToLiquidSurface(..) of Liquid Volume.");
                    return;
                }
                float sizeY = liqMat.GetVector(ShaderParams.Size).y;
                float turbFactor = shaderTurb.y * 0.05f * sizeY * _foamTurbulence;
                Vector3 min = collider.bounds.min;
                Vector3 max = collider.bounds.max;
                float turbulence1 = Mathf.Sin(min.x * shaderTurb.z + min.z * shaderTurb.w + turbulenceSpeed * 4f) * turbFactor;
                float turbulence2 = Mathf.Sin(max.x * shaderTurb.z + max.z * shaderTurb.w + turbulenceSpeed * 4f) * turbFactor;
                float y1 = liquidLevelPos + turbulence1;
                float y2 = liquidLevelPos + turbulence2;
                position.y = (y1 + y2) * 0.5f;
                root.transform.position = position;
                Vector3 boundsCenter = collider.bounds.center;
                Vector3 p1 = new Vector3(min.x - boundsCenter.x, y1, min.z - boundsCenter.z);
                Vector3 p2 = new Vector3(max.x - boundsCenter.x, y2, max.z - boundsCenter.z);
                Vector3 n = Vector3.Cross((p2 - p1).normalized, Vector3.back);
                n.y = n.y < 0 ? -n.y : n.y;
                Debug.DrawRay(transform.position, transform.position + n);
                root.transform.up = Vector3.Lerp(root.transform.up, n, dampen);
            }
        }

        /// <summary>
        /// Computes approximate point where liquid starts pouring over the flask when it's rotated
        /// </summary>
        /// <returns><c>true</c>, if spill point is detected, <c>false</c> otherwise.</returns>
        /// <param name="spillPosition">Returned spill position in world space coordinates.</param>
        /// <param name="apertureStart">A value that determines where the aperture of the flask starts (0-1 where 0 is flask center and 1 is the very top).</param>
        public bool GetSpillPoint(out Vector3 spillPosition, float apertureStart = 1f) {
            float spillAmount;
            return GetSpillPoint(out spillPosition, out spillAmount, apertureStart);
        }



        /// <summary>
        /// Computes approximate point where liquid starts pouring over the flask when it's rotated
        /// </summary>
        /// <returns><c>true</c>, if spill point is detected, <c>false</c> otherwise.</returns>
        /// <param name="spillPosition">Returned spill position in world space coordinates.</param>
        /// <param name="spillAmount">A returned value that represent the amount of liquid spilt.</param>
        /// <param name="apertureStart">A value that determines where the aperture of the flask starts (0-1 where 0 is flask center and 1 is the very top).</param>
        public bool GetSpillPoint(out Vector3 spillPosition, out float spillAmount, float apertureStart = 1f) {
            spillPosition = Vector3.zero;
            spillAmount = 0;
            if (mesh == null || verticesSorted == null || levelMultipled <= 0)
                return false;

            float maxy = float.MinValue;
            for (int k = 0; k < verticesSorted.Length; k++) {
                Vector3 vertex = verticesSorted[k];
                if (vertex.y > maxy) {
                    maxy = vertex.y;
                }
            }
            float clampy = maxy * apertureStart * 0.99f;
            Vector3 vt = transform.position;
            bool crossed = false;
            float miny = float.MaxValue;
            for (int k = 0; k < verticesSorted.Length; k++) {
                Vector3 vertex = verticesSorted[k];
                if (vertex.y < clampy)
                    break;
                vertex = transform.TransformPoint(vertex);
                if (vertex.y < liquidLevelPos && vertex.y < miny) {
                    miny = vertex.y;
                    vt = vertex;
                    crossed = true;
                }
            }
            if (!crossed)
                return false;

            spillPosition = vt;
            spillAmount = (liquidLevelPos - vt.y) / (mesh.bounds.extents.y * 2f * transform.localScale.y);
            return true;
        }


        void UpdateSpillPointGizmo() {
            if (!_debugSpillPoint) {
                if (spillPointGizmo != null) {
                    DestroyImmediate(spillPointGizmo.gameObject);
                    spillPointGizmo = null;
                }
                return;
            }

            if (spillPointGizmo == null) {
                Transform t = transform.Find(SPILL_POINT_GIZMO);
                if (t != null) {
                    DestroyImmediate(t.gameObject);
                }
                spillPointGizmo = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                spillPointGizmo.name = SPILL_POINT_GIZMO;
                spillPointGizmo.transform.SetParent(transform, true);
                Collider collider = spillPointGizmo.GetComponent<Collider>();
                if (collider != null)
                    DestroyImmediate(collider);
                MeshRenderer mr = spillPointGizmo.GetComponent<MeshRenderer>();
                if (mr != null) {
                    mr.sharedMaterial = Instantiate(mr.sharedMaterial); // to avoid Editor (non playing) warning
                    mr.sharedMaterial.hideFlags = HideFlags.DontSave;
                    mr.sharedMaterial.color = Color.yellow;
                }
            }

            Vector3 spillPoint;
            if (GetSpillPoint(out spillPoint, 1f)) {
                spillPointGizmo.transform.position = spillPoint;
                if (mesh != null) {
                    Vector3 size = mesh.bounds.extents * 0.2f;
                    float s = size.x > size.y ? size.x : size.z;
                    s = s > size.z ? s : size.z;
                    spillPointGizmo.transform.localScale = new Vector3(s, s, s);
                } else {
                    spillPointGizmo.transform.localScale = new Vector3(0.05f, 0.05f, 0.05f);
                }
                spillPointGizmo.SetActive(true);
            } else {
                spillPointGizmo.SetActive(false);
            }

        }

        /// <summary>
        /// Applies current transform rotation and scale to the vertices and resets the transform rotation and scale to default values.
        /// This operation makes the game object transform point upright as normal game objects and is required for Liquid Volume to work on imported models that comes with a rotation
        /// </summary>
        public void BakeRotation() {

            if (transform.localRotation == transform.rotation) {
                // nothing to do!
                return;
            }
            MeshFilter mf = GetComponent<MeshFilter>();
            Mesh mesh = mf.sharedMesh;
            if (mesh == null) return;
            mesh = Instantiate(mesh);
            Vector3[] vertices = mesh.vertices;
            Vector3 scale = transform.localScale;
            Vector3 localPos = transform.localPosition;

            transform.localScale = Vector3.one;

            Transform parent = transform.parent;
            if (parent != null) {
                transform.SetParent(null, false);
            }

            for (int k = 0; k < vertices.Length; k++) {
                vertices[k] = transform.TransformVector(vertices[k]);
            }
            mesh.vertices = vertices;
            mesh.RecalculateBounds();
            mesh.RecalculateNormals();
            mf.sharedMesh = mesh;

            // Ensure parent has no different rotation
            if (parent != null) {
                transform.SetParent(parent, false);
                transform.localPosition = localPos;
            }

            transform.localRotation = Quaternion.Euler(0, 0, 0);
            transform.localScale = scale;

            RefreshMeshAndCollider();
        }

        /// <summary>
        /// This operation computes the geometric center of all vertices and displaces them so the pivot is centered in the model
        /// </summary>
        public void CenterPivot() {
            CenterPivot(Vector3.zero, false);
        }

        readonly List<Vertex> hullVertices = new List<Vertex>();
        [SerializeField]
        Mesh fixedMesh, closedMesh;

        /// <summary>
        /// This operation computes the geometric center of all vertices and displaces them so the pivot is centered in the model
        /// </summary>
        public void CenterPivot(Vector3 offset, bool closeMesh) {

            MeshFilter mf = GetComponent<MeshFilter>();
            Mesh mesh = mf.sharedMesh;
            if (mesh == null) return;

            mesh = Instantiate(mesh);
            mesh.name = mf.sharedMesh.name; // keep original name to detect if user assigns a different mesh to meshfilter and discard originalMesh reference

            Vector3[] vertices = mesh.vertices;

            if (closeMesh) {
                hullVertices.Clear();
                for (int k = 0; k < vertices.Length; k++) {
                    hullVertices.Add(new Vertex(vertices[k]));
                }

                // Create hull
                var result = ConvexHull.Create(hullVertices);
                mesh.Clear();
                mesh.vertices = result.Points.Select(x => x.ToVec()).ToArray();
                var xxx = result.Points.ToList();

                List<int> triangles = new List<int>();
                foreach (var face in result.Faces) {
                    triangles.Add(xxx.IndexOf(face.Vertices[0]));
                    triangles.Add(xxx.IndexOf(face.Vertices[1]));
                    triangles.Add(xxx.IndexOf(face.Vertices[2]));
                }
                mesh.SetTriangles(triangles, 0);
                mesh.RecalculateBounds();

                vertices = mesh.vertices;
                closedMesh = mesh;
            }

            Vector3 midPoint = Vector3.zero;
            for (int k = 0; k < vertices.Length; k++) {
                midPoint += vertices[k];
            }
            midPoint /= vertices.Length;
            midPoint += offset;
            for (int k = 0; k < vertices.Length; k++) {
                vertices[k] -= midPoint;
            }
            mesh.vertices = vertices;
            mesh.RecalculateBounds();
            if (closeMesh) {
                mesh.RecalculateNormals();
            }
            mf.sharedMesh = mesh;

            fixedMesh = mesh;

            Vector3 localScale = transform.localScale;
            midPoint.x *= localScale.x;
            midPoint.y *= localScale.y;
            midPoint.z *= localScale.z;
            transform.localPosition += midPoint;

            RefreshMeshAndCollider();
        }

        public void RefreshMeshAndCollider() {
            ClearMeshCache();
            MeshCollider mc = GetComponent<MeshCollider>();
            if (mc != null) {
                Mesh oldMesh = mc.sharedMesh;
                mc.sharedMesh = null;
                mc.sharedMesh = oldMesh;
            }
        }

        /// <summary>
        /// Assigns a new layer density and optionally keep the same volume
        /// </summary>
        /// <param name="newDensity">New density.</param>
        /// <param name="keepVolume">If set to <c>true</c> keep volume.</param>
        public void SetLayerDensity(int layerIndex, float newDensity, bool keepVolume) {
            if (layerIndex < 0 || _liquidLayers == null || layerIndex >= _liquidLayers.Length)
                return;
            float amount = _liquidLayers[layerIndex].amount;
            if (keepVolume && _liquidLayers[layerIndex].density > 0) {
                amount *= newDensity / _liquidLayers[layerIndex].density;
            }
            _liquidLayers[layerIndex].amount = amount;
            _liquidLayers[layerIndex].density = newDensity;
        }


        #endregion

        #region Multiple layers

        void InitLayers() {
            requireLayersUpdate = true;
            lastLayerCount = -1;

            if (_liquidLayers == null)
                return;
            for (int k = 0; k < _liquidLayers.Length; k++) {
                _liquidLayers[k].ResetCurrentStates();
            }
        }

        public void UpdateLayers(bool immediate = false) {
            shouldUpdateMaterialProperties = true;
            requireLayersUpdate = true;
            lastLayerCount = _liquidLayers.Length;
            if (immediate) {
                this.immediate = true;
                RenderObject();
            }
        }

        void UpdateLayersNow() {

            requireLayersUpdate = false;

            if (_liquidLayers == null) {
                _liquidLayers = new LiquidLayer[0];
            } else if (_liquidLayers.Length > MAX_LAYERS) {
                Array.Resize(ref _liquidLayers, MAX_LAYERS);
                Debug.Log("Liquid layers max count exceeded. Capping to " + MAX_LAYERS + ".");
            }

            if (lastLayerCount >= 0 && lastLayerCount < _liquidLayers.Length) {
                for (int k = lastLayerCount; k < _liquidLayers.Length; k++) {
                    _liquidLayers[k].SetDefaults(k);
                }
            }
            lastLayerCount = _liquidLayers.Length;

            if (sortedLayers == null || sortedLayers.Length < MAX_LAYERS) {
                sortedLayers = new int[MAX_LAYERS];
            }

            // ensure proper values
            for (int k = 0; k < _liquidLayers.Length; k++) {
                _liquidLayers[k].amount = Mathf.Max(0, _liquidLayers[k].amount);
                _liquidLayers[k].scale = Mathf.Clamp(_liquidLayers[k].scale, 0.001f, 0.48f);
                _liquidLayers[k].murkiness = Mathf.Clamp01(_liquidLayers[k].murkiness);
                if (_liquidLayers[k].adjustmentSpeed <= 0)
                    _liquidLayers[k].adjustmentSpeed = 1f;
                if (_liquidLayers[k].density < 0.001f)
                    _liquidLayers[k].density = 0.001f;
            }

            // Sort & stack by density
            sortedLayersCount = 0;
            float currentDensity = float.MaxValue;
            float baseLevel = 0;
            for (int k = 0; k < _liquidLayers.Length; k++) {
                float maxDensity = float.MinValue;
                // Pick greater density first
                for (int j = 0; j < _liquidLayers.Length; j++) {
                    float density = _liquidLayers[j].density;
                    if (density > maxDensity && density < currentDensity) {
                        maxDensity = density;
                    }
                }
                if (maxDensity == float.MinValue)
                    break;
                // Place layers of the mix
                for (int j = 0; j < _liquidLayers.Length; j++) {
                    if (_liquidLayers[j].density == maxDensity) {
                        _liquidLayers[j].baseLevel = baseLevel;
                        sortedLayers[sortedLayersCount++] = j;
                        baseLevel += _liquidLayers[j].amount / _liquidLayers[j].density;
                    }
                }
                currentDensity = maxDensity;
            }
            // Handle miscible layers
            for (int k = 0; k < sortedLayersCount; k++) {
                int i = sortedLayers[k];
                if (_liquidLayers[i].miscible) {
                    LiquidLayer temp = new LiquidLayer();
                    int j = k;
                    float groupBaseLevel = _liquidLayers[i].baseLevel;
                    float groupDensity = _liquidLayers[i].density;
                    while (_liquidLayers[i].miscible && _liquidLayers[i].density == groupDensity) {
                        float amount = Mathf.Max(_liquidLayers[i].amount, 0.0001f);
                        temp.color += _liquidLayers[i].color * amount;
                        temp.murkColor += _liquidLayers[i].murkColor * amount;
                        temp.murkiness += _liquidLayers[i].murkiness * amount;
                        temp.scale += _liquidLayers[i].scale * amount;
                        temp.amount += amount;
                        if (++j >= sortedLayersCount)
                            break;
                        i = sortedLayers[j];
                    }
                    // Combine
                    for (int l = k; l < j; l++) {
                        i = sortedLayers[l];
                        _liquidLayers[i].mixedColor = temp.color / temp.amount;
                        _liquidLayers[i].mixedColor2 = temp.murkColor / temp.amount;
                        _liquidLayers[i].mixedMurkiness = temp.murkiness / temp.amount;
                        _liquidLayers[i].mixedScale = temp.scale / temp.amount;
                        _liquidLayers[i].mixedBubblesOpacity = temp.bubblesOpacity / temp.amount;
                        _liquidLayers[i].mixedAmount = temp.amount / groupDensity;
                        _liquidLayers[i].baseLevel = groupBaseLevel;
                    }
                    k = j - 1;
                } else {
                    _liquidLayers[i].mixedColor = _liquidLayers[i].color;
                    _liquidLayers[i].mixedColor2 = _liquidLayers[i].murkColor;
                    _liquidLayers[i].mixedMurkiness = _liquidLayers[i].murkiness;
                    _liquidLayers[i].mixedScale = _liquidLayers[i].scale;
                    _liquidLayers[i].mixedBubblesOpacity = _liquidLayers[i].bubblesOpacity;
                    _liquidLayers[i].mixedAmount = Mathf.Clamp01(_liquidLayers[i].amount / _liquidLayers[i].density);
                }
            }

            // Compact
            for (int k = 0; k < sortedLayersCount - 1; k++) {
                int i = sortedLayers[k];
                int j = sortedLayers[k + 1];
                float expectedBaseLevel = _liquidLayers[i].baseLevel + _liquidLayers[i].mixedAmount;
                if (_liquidLayers[j].baseLevel > expectedBaseLevel) {
                    _liquidLayers[j].baseLevel = expectedBaseLevel;
                }
            }

            AnimateLayers(true);
        }

        int layerComparer(LiquidLayer layer1, LiquidLayer layer2) {
            return layer1.density.CompareTo(layer2.density);
        }

        void AnimateLayers(bool changes = false) {
            if (Application.isPlaying) {
                for (int j = 0; j < sortedLayersCount; j++) {
                    int k = sortedLayers[j];
                    if (_liquidLayers[k].currentBaseLevel != _liquidLayers[k].baseLevel || _liquidLayers[k].currentAmount != _liquidLayers[k].mixedAmount || _liquidLayers[k].currentMurkiness != _liquidLayers[k].mixedMurkiness || _liquidLayers[k].currentBubblesOpacity != _liquidLayers[k].mixedBubblesOpacity || _liquidLayers[k].currentColor != _liquidLayers[k].mixedColor || _liquidLayers[k].currentColor2 != _liquidLayers[k].mixedColor2) {
                        float t = Time.deltaTime * _layersAdjustmentSpeed * _liquidLayers[k].adjustmentSpeed;
                        if (t > 1f || immediate) {
                            t = 1f;
                        }
                        if (Similar(_liquidLayers[k].currentBaseLevel, _liquidLayers[k].baseLevel)) {
                            _liquidLayers[k].currentBaseLevel = _liquidLayers[k].baseLevel;
                        } else {
                            _liquidLayers[k].currentBaseLevel = Mathf.Lerp(_liquidLayers[k].currentBaseLevel, _liquidLayers[k].baseLevel, t);
                        }
                        if (Similar(_liquidLayers[k].currentAmount, _liquidLayers[k].mixedAmount)) {
                            _liquidLayers[k].currentAmount = _liquidLayers[k].mixedAmount;
                        } else {
                            _liquidLayers[k].currentAmount = Mathf.Lerp(_liquidLayers[k].currentAmount, _liquidLayers[k].mixedAmount, t);
                        }
                        if (Similar(_liquidLayers[k].currentMurkiness, _liquidLayers[k].mixedMurkiness)) {
                            _liquidLayers[k].currentMurkiness = _liquidLayers[k].mixedMurkiness;
                        } else {
                            _liquidLayers[k].currentMurkiness = Mathf.Lerp(_liquidLayers[k].currentMurkiness, _liquidLayers[k].mixedMurkiness, t);
                        }
                        if (Similar(ref _liquidLayers[k].currentColor, ref _liquidLayers[k].mixedColor)) {
                            _liquidLayers[k].currentColor = _liquidLayers[k].mixedColor;
                        } else {
                            _liquidLayers[k].currentColor = Color.Lerp(_liquidLayers[k].currentColor, _liquidLayers[k].mixedColor, t);
                        }
                        if (Similar(ref _liquidLayers[k].currentColor2, ref _liquidLayers[k].mixedColor2)) {
                            _liquidLayers[k].currentColor2 = _liquidLayers[k].mixedColor2;
                        } else {
                            _liquidLayers[k].currentColor2 = Color.Lerp(_liquidLayers[k].currentColor2, _liquidLayers[k].mixedColor2, t);
                        }
                        if (Similar(_liquidLayers[k].currentBubblesOpacity, _liquidLayers[k].mixedBubblesOpacity)) {
                            _liquidLayers[k].currentBubblesOpacity = _liquidLayers[k].mixedBubblesOpacity;
                        } else {
                            _liquidLayers[k].currentBubblesOpacity = Mathf.Lerp(_liquidLayers[k].currentBubblesOpacity, _liquidLayers[k].mixedBubblesOpacity, t);
                        }
                        changes = true;
                    }
                }
                if (changes) {
                    // Compact
                    if (_layersAdjustmentCompact) {
                        float currentBaseLevel = float.MinValue;
                        float expectedBaseLevel = 0;
                        for (int k = 0; k < _liquidLayers.Length; k++) {
                            // Get the minimum
                            float minCurrentBaseLevel = float.MaxValue;
                            int min = -1;
                            for (int j = 0; j < _liquidLayers.Length; j++) {
                                if (_liquidLayers[j].currentBaseLevel < minCurrentBaseLevel && _liquidLayers[j].currentBaseLevel > currentBaseLevel) {
                                    minCurrentBaseLevel = _liquidLayers[j].currentBaseLevel;
                                    min = j;
                                }
                            }
                            if (min < 0)
                                break;
                            if (k > 0) {
                                if (_liquidLayers[min].currentBaseLevel > expectedBaseLevel) {
                                    _liquidLayers[min].currentBaseLevel = expectedBaseLevel;
                                }
                            }
                            expectedBaseLevel = _liquidLayers[min].currentBaseLevel + _liquidLayers[min].currentAmount;
                            currentBaseLevel = _liquidLayers[min].currentBaseLevel;
                        }
                    }
                }
            } else {
                for (int j = 0; j < sortedLayersCount; j++) {
                    int k = sortedLayers[j];
                    if (_liquidLayers[k].currentBaseLevel != _liquidLayers[k].baseLevel || _liquidLayers[k].currentAmount != _liquidLayers[k].mixedAmount || _liquidLayers[k].currentMurkiness != _liquidLayers[k].mixedMurkiness || _liquidLayers[k].currentColor != _liquidLayers[k].mixedColor || _liquidLayers[k].currentColor2 != _liquidLayers[k].mixedColor2) {
                        _liquidLayers[k].currentBaseLevel = _liquidLayers[k].baseLevel;
                        _liquidLayers[k].currentAmount = _liquidLayers[k].mixedAmount;
                        _liquidLayers[k].currentColor = _liquidLayers[k].mixedColor;
                        _liquidLayers[k].currentColor2 = _liquidLayers[k].mixedColor2;
                        _liquidLayers[k].currentMurkiness = _liquidLayers[k].mixedMurkiness;
                        _liquidLayers[k].currentBubblesOpacity = _liquidLayers[k].mixedBubblesOpacity;
                        changes = true;
                    }
                }
            }
            immediate = false;

            if (changes) {
                UpdateLayersProperties();
            }
        }

        bool Similar(float a, float b) {
            if (a < b) {
                return (b - a) < 0.01f;
            } else {
                return (a - b) < 0.01f;
            }
        }

        bool Similar(ref Color a, ref Color b) {
            float dr, dg, db, da;
            dr = a.r < b.r ? b.r - a.r : a.r - b.r;
            if (dr > 0.01f)
                return false;
            dg = a.g < b.g ? b.g - a.g : a.g - b.g;
            if (dg > 0.01f)
                return false;
            db = a.b < b.b ? b.b - a.b : a.b - b.b;
            if (db > 0.01f)
                return false;
            da = a.a < b.a ? b.a - a.a : a.a - b.a;
            return da < 0.01f;
        }


        void UpdateLayersProperties() {

            if (_liquidLayers == null || sortedLayers == null || liqMat == null)
                return;

            const int LAYERS_RESOLUTION = 256;

            if (layersTextureColors == null || layersTextureColors.Length != LAYERS_RESOLUTION) {
                layersTextureColors = new Color[LAYERS_RESOLUTION];
            }
            if (layersTextureColors2 == null || layersTextureColors2.Length != LAYERS_RESOLUTION) {
                layersTextureColors2 = new Color[LAYERS_RESOLUTION];
            }
            if (layersTextureColorsTemp == null || layersTextureColorsTemp.Length != LAYERS_RESOLUTION) {
                layersTextureColorsTemp = new Color[LAYERS_RESOLUTION];
            }
            if (layersProperties == null || layersProperties.Length != LAYERS_RESOLUTION) {
                layersProperties = new Vector4[LAYERS_RESOLUTION];
            }
            if (layersSum == null || layersSum.Length != LAYERS_RESOLUTION) {
                layersSum = new float[LAYERS_RESOLUTION];
            }

            Color colorNull = new Color(0, 0, 0, 0);
            Vector4 zero = Vector4.zero;
            for (int j = 0; j < LAYERS_RESOLUTION; j++) {
                layersTextureColors[j] = colorNull;
                layersTextureColors2[j] = colorNull;
                layersTextureColorsTemp[j] = colorNull;
                layersProperties[j] = zero;
                layersSum[j] = 0;
            }

            float level = 0;
            for (int k = 0; k < sortedLayersCount; k++) {
                float layerBaseLevel = _liquidLayers[k].currentBaseLevel;
                float layerAmount = _liquidLayers[k].currentAmount;
                if (layerAmount + layerBaseLevel > 1f) {
                    layerAmount = 1f - layerBaseLevel;
                    if (layerAmount < 0)
                        layerAmount = 0;
                }
                if (level < layerBaseLevel + layerAmount)
                    level = layerBaseLevel + layerAmount;
            }

            if (level != _level) {
                _level = level;
                UpdateLevels();
            }

            int y = 0, y_next = 0;
            for (int i = 0; i < sortedLayersCount; i++) {
                int k = sortedLayers[i];
                Color layerColor = ApplyGlobalAlpha(_liquidLayers[k].currentColor);
                Color layerColor2 = ApplyGlobalAlpha(_liquidLayers[k].currentColor2);
                float alpha = layerColor.a;
                layerColor *= alpha;
                layerColor2 *= alpha;
                float layerMurkiness = _liquidLayers[k].currentMurkiness * alpha;
                float layerScale = _liquidLayers[k].mixedScale * 10.0f;
                float layerBaseLevel = _liquidLayers[k].currentBaseLevel * _levelMultiplier;
                float layerAmount = _liquidLayers[k].currentAmount * _levelMultiplier;
                float layerTurbulence = _liquidLayers[k].viscosity * alpha;
                float layerBubblesOpacity = _liquidLayers[k].bubblesOpacity * alpha;
                y = (int)((levelMultipled - layerBaseLevel) * LAYERS_RESOLUTION);
                y_next = (int)((levelMultipled - layerBaseLevel - layerAmount) * LAYERS_RESOLUTION);
                for (int j = y_next; j < y; j++) {
                    if (j >= 0 && j < LAYERS_RESOLUTION) {
                        layersTextureColors[j] += layerColor;
                        layersTextureColors2[j] += layerColor2;
                        layersProperties[j].x += layerMurkiness;
                        layersProperties[j].y = layerScale > layersProperties[j].y ? layerScale : layersProperties[j].y;
                        layersProperties[j].z += layerTurbulence;
                        layersProperties[j].w += layerBubblesOpacity;
                        layersSum[j] += alpha;
                    }
                }
            }

            int lastLayer = 0;
            for (int j = 0; j < LAYERS_RESOLUTION; j++) {
                float m = layersSum[j];
                if (m > 0) {
                    layersTextureColors[j] /= m;
                    layersTextureColors2[j] /= m;
                    layersProperties[j].x /= m;
                    layersProperties[j].z /= m;
                    layersProperties[j].w /= m;
                    lastLayer = j;
                }
            }

            // Copy last layer to fill gap when tilting liquid
            if (!_ignoreGravity) {
                for (int j = lastLayer + 1; j < LAYERS_RESOLUTION; j++) {
                    layersTextureColors[j] = layersTextureColors[lastLayer];
                    layersTextureColors2[j] = layersTextureColors2[lastLayer];
                    layersProperties[j] = layersProperties[lastLayer];
                }
            }

            // Smooth contact surfaces
            if (_smoothContactSurface > 0) {
                for (int j = 0; j < LAYERS_RESOLUTION; j++) {
                    Color sum = colorNull;
                    int count = 0;
                    for (int k = j - _smoothContactSurface; k <= j + _smoothContactSurface; k++) {
                        if (k >= 0 && k < LAYERS_RESOLUTION) {
                            sum += layersTextureColors[k];
                            count++;
                        }
                    }
                    layersTextureColorsTemp[j] = sum / count;
                }
                // Blit colors back
                for (int j = 0; j < LAYERS_RESOLUTION; j++) {
                    layersTextureColors[j] = layersTextureColorsTemp[j];
                }
            }


            UploadMaterialArrays();

#if UNITY_WEBGL
            SetLayersTextures();
#else
            if (FORCE_GLES_COMPATIBILITY) {
                SetLayersTextures();
            }
#endif
        }

        void UploadMaterialArrays() {
            liqMat.SetColorArray(ShaderParams.LayersColorArray, layersTextureColors);
            liqMat.SetColorArray(ShaderParams.LayersColor2Array, layersTextureColors2);
            liqMat.SetVectorArray(ShaderParams.LayersProperties, layersProperties);
        }

        void SetLayersTextures() {
            if (layersPropertiesTex == null) {
                layersPropertiesTex = new Texture2D(1, 256, TextureFormat.RGBAFloat, false, true);
                layersPropertiesTex.filterMode = FilterMode.Point;
                layersPropertiesTex.wrapMode = TextureWrapMode.Clamp;
            }
            if (layersColorsTex == null) {
                layersColorsTex = new Texture2D(1, 256, TextureFormat.ARGB32, false);
                layersColorsTex.filterMode = FilterMode.Point;
                layersColorsTex.wrapMode = TextureWrapMode.Clamp;
            }
            if (layersPropertiesTexContents == null || layersPropertiesTexContents.Length < 256) {
                layersPropertiesTexContents = new Color[256];
            }
            if (layersColorsTexContents == null || layersColorsTexContents.Length < 256) {
                layersColorsTexContents = new Color[256];
            }
            if (layersColors2Tex == null) {
                layersColors2Tex = new Texture2D(1, 256, TextureFormat.ARGB32, false);
                layersColors2Tex.filterMode = FilterMode.Point;
                layersColors2Tex.wrapMode = TextureWrapMode.Clamp;
            }
            if (layersColors2TexContents == null || layersColors2TexContents.Length < 256) {
                layersColors2TexContents = new Color[256];
            }
            for (int k = 0; k < 256; k++) {
                layersPropertiesTexContents[k].r = layersProperties[k].x;
                layersPropertiesTexContents[k].g = layersProperties[k].y;
                layersPropertiesTexContents[k].b = layersProperties[k].z;
                layersPropertiesTexContents[k].a = layersProperties[k].w;
                layersColorsTexContents[k] = layersTextureColors[k];
                layersColors2TexContents[k] = layersTextureColors2[k];
            }
            layersPropertiesTex.SetPixels(layersPropertiesTexContents);
            layersPropertiesTex.Apply();
            liqMat.SetTexture(ShaderParams.LayersPropertiesTex, layersPropertiesTex);
            layersColorsTex.SetPixels(layersColorsTexContents);
            layersColorsTex.Apply();
            liqMat.SetTexture(ShaderParams.LayersColorsTex, layersColorsTex);
            layersColors2Tex.SetPixels(layersColors2TexContents);
            layersColors2Tex.Apply();
            liqMat.SetTexture(ShaderParams.LayersColors2Tex, layersColors2Tex);
        }


        public void Redraw() {
            if (_liquidLayers != null) {
                for (int k = 0; k < _liquidLayers.Length; k++) {
                    _liquidLayers[k].baseLevel = 0;
                    _liquidLayers[k].currentAmount = 0;
                    _liquidLayers[k].currentBaseLevel = 0;
                }
                requireLayersUpdate = true;
            }
            UpdateMaterialProperties();
        }

        #endregion


        #region Mesh Displacement

        void CheckMeshDisplacement() {
            MeshFilter meshFilter = GetComponent<MeshFilter>();

            if (meshFilter == null) {
                originalMesh = null;
                return;
            }

            Mesh currentMesh = meshFilter.sharedMesh;
            if (currentMesh == null) {
                if (_fixMesh) {
                    if (_autoCloseMesh && closedMesh != null) {
                        meshFilter.sharedMesh = closedMesh;
                        return;
                    } else if (!_autoCloseMesh && fixedMesh != null) {
                        meshFilter.sharedMesh = fixedMesh;
                        return;
                    } else if (originalMesh != null) {
                        meshFilter.sharedMesh = originalMesh;
                    }
                } else {
                    originalMesh = null;
                    return;
                }
                currentMesh = meshFilter.sharedMesh;
            }

            if (!_fixMesh) {
                RestoreOriginalMesh();
                originalMesh = null;
                return;
            }

            if (_autoCloseMesh && currentMesh == closedMesh) return;
            if (!_autoCloseMesh && currentMesh == fixedMesh) return;

            // Backup original mesh
            if (originalMesh == null || !(originalMesh.name.Equals(currentMesh.name))) {
                originalMesh = meshFilter.sharedMesh;
            }

            if (currentMesh != originalMesh) {
                RestoreOriginalMesh();
            }

            Vector3 pos = transform.localPosition;
            CenterPivot(_pivotOffset, _autoCloseMesh);
            originalPivotOffset = transform.localPosition - pos;
        }

        void RestoreOriginalMesh() {
            fixedMesh = closedMesh = null;
            if (originalMesh == null) return;

            MeshFilter meshFilter = GetComponent<MeshFilter>();
            if (meshFilter == null) return;

            meshFilter.sharedMesh = originalMesh;
            transform.localPosition -= originalPivotOffset;
            RefreshMeshAndCollider();
        }

        #endregion

        #region Bubbles support

        void AddBubbles(Texture3D tex) {

            if (tex == null)
                return;

            if (_bubblesSizeMax < _bubblesSizeMin) {
                _bubblesSizeMax = _bubblesSizeMin;
            }
            int size = tex.width;

            Array.Copy(colors3D[currentNoiseVariation], colors3DWithBubbles[currentNoiseVariation], colors3DWithBubbles[currentNoiseVariation].Length);
            Color[] colors = colors3DWithBubbles[currentNoiseVariation];

            // Clear bubble buffer
            for (int k = 0; k < colors.Length; k++) {
                colors[k].g = 0.5f;
                colors[k].b = 0.5f;
                colors[k].a = 0.5f;
            }

            // Save current random state
            Random.InitState(_bubblesSeed);

            // Mark bubble positions
            Vector3 nrm = Vector3.zero;
            for (int k = 0; k < _bubblesAmount; k++) {
                int bubbleSize = Random.Range(_bubblesSizeMin, _bubblesSizeMax + 1);
                float bubbleSizeSqr = (float)(bubbleSize * bubbleSize);
                int y = Random.Range(bubbleSize, size - bubbleSize);
                int z = Random.Range(bubbleSize, size - bubbleSize);
                int x = Random.Range(bubbleSize, size - bubbleSize);
                for (int y2 = y - bubbleSize; y2 <= y + bubbleSize; y2++) {
                    float dy = y2 - y;
                    for (int z2 = z - bubbleSize; z2 <= z + bubbleSize; z2++) {
                        float dz = z2 - z;
                        for (int x2 = x - bubbleSize; x2 <= x + bubbleSize; x2++) {
                            float dx = x2 - x;
                            float dSqr = dy * dy + dz * dz + dx * dx;
                            float d = 1f - dSqr / bubbleSizeSqr;
                            if (d >= 0) {
                                float l = (float)System.Math.Sqrt(dSqr);
                                nrm.x = dx / l;
                                nrm.y = dy / l;
                                nrm.z = dz / l;
                                nrm.x = (nrm.x + 1f) * 0.5f;
                                nrm.y = (nrm.y + 1f) * 0.5f;
                                nrm.z = (nrm.z + 1f) * 0.5f;
                                int c = y2 * size * size + z2 * size + x2;
                                colors[c].g = nrm.x;
                                colors[c].b = nrm.y;
                                colors[c].a = nrm.z;
                            }
                        }
                    }
                }
            }

#if UNITY_WEBGL
			bool useUnwrappedTexture = true;
#else
            bool useUnwrappedTexture = FORCE_GLES_COMPATIBILITY;
#endif

            if (useUnwrappedTexture) {

                if (noise3DUnwrapped == null) {
                    noise3DUnwrapped = new Texture2D(size, size * size, TextureFormat.RGBA32, false, true);
                }
                noise3DUnwrapped.SetPixels(colors);
                noise3DUnwrapped.Apply();
                liqMat.SetTexture(ShaderParams.NoiseTexUnwrapped, noise3DUnwrapped);
            }

            // Push colors to texture
            tex.SetPixels(colors);
            tex.Apply();
        }


        void ClearBubbles(Texture3D tex) {

            if (tex == null)
                return;

            Array.Copy(colors3D[currentNoiseVariation], colors3DWithBubbles[currentNoiseVariation], colors3DWithBubbles[currentNoiseVariation].Length);
            Color[] colors = colors3DWithBubbles[currentNoiseVariation];

            // Clear bubble buffer
            for (int k = 0; k < colors.Length; k++) {
                colors[k].g = 0.5f;
                colors[k].b = 0.5f;
                colors[k].a = 0.5f;
            }

            // Push colors to texture
            tex.SetPixels(colors);
            tex.Apply();

            liqMat.SetTexture(ShaderParams.NoiseTexUnwrapped, null);
        }

#endregion

    }
}
