layers_environment_CloudsLayer.js

import * as THREE from 'three';
import { EnvironmentLayer } from './EnvironmentLayer.js';
import perlinWorley3D from "../../images/perlinWorley3D_128.bin";
import blueNoise from '../../images/blueNoise.png';
import {CloudsShader} from './shaders/CloudsShader'
import {CloudsBlurShader} from './shaders/CloudsBlurShader'

let cloudsTarget;
let cloudsBlurTarget1;
let cloudsBlurTarget2;
let cloudsMaterial;
let blurMaterial;
const previousCameraPositon = new THREE.Vector3();
const previousCameraQuaternion = new THREE.Quaternion();

let textureCallback;

let perlinWorley3DDataTexture;
let blueNoiseTexture;

const clock = new THREE.Clock();



/**
 * Base class for clouds layer. 
 * @class
 * @extends EnvironmentLayer
 */
class CloudsLayer extends EnvironmentLayer {
    /**
     * A volumetric clouds layer that allows specifying custom code to compute cloud opacity per sample.
     * See {@class NOAAGFSCloudsLayer} or {@class RandomCloudsLayer} for directly useable implementations.
     * 
     * The given shader glsl code {@param properties.sampleDensityFunction} will be inserted in the clouds shader and 
     * used to compute volume sample densities. It must implement a function "float sampleDensity(vec3 samplePosition, float lod)".
     * The lod shall be a value between 0 and 4 where 0 indicates samples closer to the camera and 4 indicates samples further away. 
     * 
     * Extra uniforms can be specified through the @param{properties.extraUniforms} and will be available in the shader by their key.
     * Only the following types are allowed: number, boolean, THREE.Vector2, THREE.Vector3, THREE.Vector4, THREE.Matrix3, THREE.Matrix4,
     * THREE.Data3DTexture, THREE.DataArrayTexture, THREE.DataTexture and THREE.Texture
     * 
     * Only one visible CloudsLayer (first in layer list) will be taken into account at a time.
     * 
     * @param {Object} properties 
     * @param {String|Number} properties.id layer id should be unique
     * @param {String} properties.name the name can be anything you want and is intended for labeling
     * @param {String} [properties.sampleDensityFunction] shader function that returns the cloud density given a point in cartesian coordinates and a level of detail.
     * @param {Object} [properties.extraUniforms] a key value pair of uniforms for the shader. The uniforms will be available with the given keys in the shader. 
     * @param {Number} [properties.quality = 0.5] a quality that affects the resolution and number of samples for volumetric clouds
     * @param {Number} [properties.minHeight = 500] clouds min height in meters above ellipsoid
     * @param {Number} [properties.maxHeight = 12000] clouds max height in meters above ellipsoid
     * @param {Number} [properties.density = 0.5] cloud density multiplier 
     * @param {Number} [properties.luminance = 1] sun intensity multiplier
     * @param {Number} [properties.windSpeed = 0.05] wind speed
     * @param {Number} [properties.windDirection = new THREE.Vector2(1.0,0.0)] wind direction
     * @param {Boolean} [properties.debug = false] wind direction
     * @param {THREE.Vector3} [properties.color = new THREE.Vector3(1.0,1.0,1.0)] base cloud color.
     * @param {Boolean} [properties.visible = true] layer will be rendered if true (true by default)
     * 
     */
    constructor(properties) {
        super(properties);
        this.transparency = properties.transparency ? properties.transparency : 0;
        this.shader = properties.shader;
        this.textures = properties.textures;
        this.isCloudsLayer = true;

        const isMobile = _isMobileDevice();
        this.quality = properties.quality;
        if(!this.quality) this.quality = isMobile ? 0.4 : 0.5;
        this.resolution = this.quality;
        this.numBlurPasses = 10/(this.quality*3);//Math.floor((1/this.quality)/5);//(1/this.quality)/5;
        this.proportionSamples = 0.3*this.quality;//this.quality;
        
        this.maxBlurOffset = 50.0;//1.5/this.resolution;
        this.startRadius = properties.minHeight ? properties.minHeight : 500;
        this.endRadius = Math.max(this.startRadius+1,properties.maxHeight ? properties.maxHeight : 12000);
        this.color = properties.color ? properties.color : new THREE.Vector3(1.0, 1.0, 1.0);

        this.windSpeed = properties.windSpeed? properties.windSpeed: 0.0;
        this.density = properties.density ? properties.density : 0.5;
        this.luminance = properties.luminance ? properties.luminance : 1;


        this.extraUniforms = properties.extraUniforms;
        this.sampleDensityFunction = properties.sampleDensityFunction;
        this.isCloudsLayer = true;

        if(properties.debug){
            this._createCloudsDebugPanel();
        }

    }

    getOutputTexture(){
        return cloudsBlurTarget2.texture;
    }
    getOutputDepthTexture(){
        return cloudsTarget.textures[1];
    }

    

    
    render(map) {
        

        cloudsMaterial.uniforms.proportionSamples.value = this.proportionSamples;
        


        map.renderer.setRenderTarget(cloudsTarget);
        map.postQuad.material = cloudsMaterial;
        map.renderer.render(map.postScene, map.postCamera);

        

        /// clouds blur
        
        let texelSizeVertical = 1 / cloudsBlurTarget1.height;
        let texelSizeHorizontal = 1 / cloudsBlurTarget1.width;
        let mul = 0.5;
        
        blurMaterial.uniforms.tDepth.value = map.target.depthTexture;
        blurMaterial.uniforms.cloudsDepth.value = cloudsTarget.textures[1];
        blurMaterial.uniforms.preserveMaxOpacity.value = 0.0;
        blurMaterial.uniforms.image.value = cloudsTarget.textures[0];
        blurMaterial.uniforms.offset.value.set(texelSizeHorizontal * mul, texelSizeVertical * mul);
        mul += 0.5;
        
        map.postQuad.material = blurMaterial;
        map.renderer.setRenderTarget(cloudsBlurTarget1);
        map.renderer.render(map.postScene, map.postCamera);

        blurMaterial.uniforms.image.value = cloudsBlurTarget1.texture;
        blurMaterial.uniforms.offset.value.set(texelSizeHorizontal * mul, texelSizeVertical * mul);
        mul += 0.5;
        
        map.renderer.setRenderTarget(cloudsBlurTarget2);
        map.renderer.render(map.postScene, map.postCamera);
        for (let p = 0; p < this.numBlurPasses; p++) {
            
            blurMaterial.uniforms.preserveMaxOpacity.value = 0.0;
            blurMaterial.uniforms.image.value = cloudsBlurTarget2.texture;
            blurMaterial.uniforms.offset.value.set(texelSizeHorizontal * Math.min(this.maxBlurOffset,mul), texelSizeVertical * Math.min(this.maxBlurOffset,mul));
            mul += 0.5;
            map.renderer.setRenderTarget(cloudsBlurTarget1);
            map.renderer.render(map.postScene, map.postCamera);

            blurMaterial.uniforms.image.value = cloudsBlurTarget1.texture;
            blurMaterial.uniforms.offset.value.set(texelSizeHorizontal * Math.min(this.maxBlurOffset,mul), texelSizeVertical * Math.min(this.maxBlurOffset,mul));
            mul += 0.5;
            
            map.renderer.setRenderTarget(cloudsBlurTarget2);
            map.renderer.render(map.postScene, map.postCamera);
        }
        
        previousCameraQuaternion.copy(map.camera.quaternion);
        previousCameraPositon.copy(map.camera.position);
    }
    updateUniforms(map) {
        const self = this;
        cloudsMaterial.uniforms.tDepth.value = map.target.depthTexture;
        cloudsMaterial.uniforms.cameraNear.value = map.camera.near;
        cloudsMaterial.uniforms.cameraFar.value = map.camera.far;
        cloudsMaterial.uniforms.radius.value = map.planet.radius;
        cloudsMaterial.uniforms.xfov.value = 2 * Math.atan(Math.tan(map.camera.fov * Math.PI / 180 / 2) * map.camera.aspect) * 180 / Math.PI;
        cloudsMaterial.uniforms.yfov.value = map.camera.fov;
        cloudsMaterial.uniforms.resolution.value = cloudsTarget.height,
        cloudsMaterial.uniforms.planetPosition.value = map.planet.position;
        cloudsMaterial.uniforms.nonPostCameraPosition.value = map.camera.position;
        cloudsMaterial.uniforms.ldf.value = map.logDepthBufFC;

        cloudsMaterial.uniforms.densityMultiplier.value = self.density;
        cloudsMaterial.uniforms.sunlight.value = self.luminance;
        cloudsMaterial.uniforms.color.value = self.color;
        
        cloudsMaterial.uniforms.startRadius.value = self.startRadius;
        cloudsMaterial.uniforms.endRadius.value = Math.max(self.startRadius, self.endRadius);
        cloudsMaterial.uniforms.windSpeed.value = self.windSpeed;
        cloudsMaterial.uniforms.windDirection.value = self.windDirection;

        map.camera.getWorldDirection(cloudsMaterial.uniforms.viewCenterFar.value).normalize();
        cloudsMaterial.uniforms.viewCenterNear.value.copy(cloudsMaterial.uniforms.viewCenterFar.value);
        cloudsMaterial.uniforms.up.value = map.camera.up.normalize();
        cloudsMaterial.uniforms.right.value.crossVectors(map.camera.up, cloudsMaterial.uniforms.viewCenterFar.value);
        cloudsMaterial.uniforms.viewCenterFar.value.multiplyScalar(map.camera.far).add(map.camera.position);
        cloudsMaterial.uniforms.viewCenterNear.value.multiplyScalar(map.camera.near).add(map.camera.position);
        if (map.shadows) {
            cloudsMaterial.uniforms.sunLocation.value.copy(map.sunPosition);
        }

        cloudsMaterial.uniforms.time.value = clock.getElapsedTime()*1000;

        blurMaterial.uniforms.cameraNear.value = map.camera.near;
        blurMaterial.uniforms.cameraFar.value = map.camera.far;
        blurMaterial.uniforms.ldf.value = map.logDepthBufFC;
        
    }

    changeSize(dom) {
        cloudsTarget.setSize(Math.floor(dom.offsetWidth * this.resolution), Math.floor(dom.offsetHeight * this.resolution));
        cloudsBlurTarget1.setSize(Math.floor(dom.offsetWidth), Math.floor(dom.offsetHeight));
        cloudsBlurTarget2.setSize(Math.floor(dom.offsetWidth), Math.floor(dom.offsetHeight));
    }

    _init(map) {
        const self = this;
        self.isInitialized = true;
        if (!cloudsTarget) {
            cloudsTarget = new THREE.WebGLRenderTarget(Math.floor(map.domContainer.offsetWidth*self.resolution), Math.floor(map.domContainer.offsetHeight*self.resolution), {count:2,  samples:8});
            cloudsTarget.stencilBuffer = false;
            cloudsTarget.depthBuffer = false;
            cloudsTarget.textures[0].format = THREE.RGBAFormat;
            cloudsTarget.textures[0].colorSpace = THREE.LinearSRGBColorSpace;
            cloudsTarget.textures[0].minFilter = THREE.LinearFilter;
            cloudsTarget.textures[0].magFilter = THREE.LinearFilter;
            cloudsTarget.textures[0].generateMipmaps = false;
            cloudsTarget.textures[0].premultiplyAlpha = false;

            cloudsTarget.textures[1].format = THREE.RedFormat;
            cloudsTarget.textures[1].type = THREE.HalfFloatType;
            cloudsTarget.textures[1].colorSpace = THREE.LinearSRGBColorSpace;
            cloudsTarget.textures[1].minFilter = THREE.NearestFilter;
            cloudsTarget.textures[1].magFilter = THREE.LinearFilter;
            cloudsTarget.textures[1].generateMipmaps = false;
            cloudsTarget.textures[1].premultiplyAlpha = false;
            //cloudsTarget.textures[1].type = THREE.FloatType;
            
        }

        



        if (!cloudsBlurTarget1) {
            cloudsBlurTarget1 = new THREE.WebGLRenderTarget(Math.floor(map.domContainer.offsetWidth), Math.floor(map.domContainer.offsetHeight));
            cloudsBlurTarget1.texture.format = THREE.RGBAFormat;
            cloudsBlurTarget1.texture.colorSpace = THREE.SRGBColorSpace;
            cloudsBlurTarget1.texture.minFilter = THREE.LinearFilter;
            cloudsBlurTarget1.texture.magFilter = THREE.LinearFilter;
            cloudsBlurTarget1.texture.generateMipmaps = false;
            cloudsBlurTarget1.stencilBuffer = false;
            cloudsBlurTarget1.depthBuffer = false;
            cloudsBlurTarget1.texture.premultiplyAlpha = false;
            cloudsBlurTarget1.texture.type = THREE.HalfFloatType;
        }



        if (!cloudsBlurTarget2) {
            cloudsBlurTarget2 = new THREE.WebGLRenderTarget(Math.floor(map.domContainer.offsetWidth), Math.floor(map.domContainer.offsetHeight));
            cloudsBlurTarget2.texture.format = THREE.RGBAFormat;
            cloudsBlurTarget2.texture.colorSpace = THREE.SRGBColorSpace;
            cloudsBlurTarget2.texture.minFilter = THREE.LinearFilter;
            cloudsBlurTarget2.texture.magFilter = THREE.LinearFilter;
            cloudsBlurTarget2.texture.generateMipmaps = false;
            cloudsBlurTarget2.stencilBuffer = false;
            cloudsBlurTarget2.depthBuffer = false;
            cloudsBlurTarget2.texture.premultiplyAlpha = false;
            cloudsBlurTarget2.texture.type = THREE.HalfFloatType;
        }




        if (cloudsMaterial) cloudsMaterial.dispose();

        cloudsMaterial = new THREE.ShaderMaterial({
            vertexShader: CloudsShader.vertexShader(),
            fragmentShader: map.shadows ? CloudsShader.fragmentShaderShadows(!!map.ocean, map.atmosphere, map.sunColor, self.sampleDensityFunction, self.extraUniforms) : CloudsShader.fragmentShader(!!map.ocean, map.atmosphere, map.sunColor, self.sampleDensityFunction, self.extraUniforms),
            uniforms: {
                cameraNear: { value: map.camera.near },
                cameraFar: { value: map.camera.far },
                perlinWorley: { value: null },
                noise2D: { value: null },
                tDepth: { value: null },
                radius: { value: 0 },
                xfov: { value: 0 },
                yfov: { value: 0 },
                resolution: {value: cloudsTarget.height},
                planetPosition: { value: new THREE.Vector3(0, 0, 0) },
                nonPostCameraPosition: { value: new THREE.Vector3(0, 0, 0) },
                viewCenterFar: { value: new THREE.Vector3(0, 0, 0) },
                viewCenterNear: { value: new THREE.Vector3(0, 0, 0) },
                up: { value: new THREE.Vector3(0, 0, 0) },
                right: { value: new THREE.Vector3(0, 0, 0) },
                ldf: { value: 0 },
                time: { value: 0.0 },
                proportionSamples: { value: self.proportionSamples },
                densityMultiplier: { value: 20.0 },
                sunlight: { value: 10.0 },
                sunLocation: { value: new THREE.Vector3(0, 0, 0) },
                color: { value: new THREE.Vector3(1.0, 1.0, 1.0) },
                startRadius: { value: self.startRadius },
                endRadius: { value: self.endRadius },
                windSpeed: { value: self.windspeed },
                windDirection: { value: new THREE.Vector3(1.0, 0.0) },
                quality: {value: self.quality}
            },
            depthTest: false,
            depthWrite: false
        });

        if (this.extraUniforms) {
            Object.entries(this.extraUniforms).forEach(([key, value]) => {
                cloudsMaterial.uniforms[key] = { value: value };
            });
        }

        if (blurMaterial) blurMaterial.dispose();
        blurMaterial = new THREE.ShaderMaterial({
            vertexShader: CloudsBlurShader.vertexShader(),
            fragmentShader: CloudsBlurShader.fragmentShader(),
            uniforms: {
                offset: { value: new THREE.Vector2() },
                image: { value: null },
                tDepth: { value: null },
                cloudsDepth: { value: null },
                noise2D: { value: null },
                preserveMaxOpacity: { value: 0.0 },
                cameraNear: { value: map.camera.near },
                cameraFar: { value: map.camera.far },
                ldf: { value: 0 },
            },
            premultipliedAlpha: false,
            depthTest: false,
            depthWrite: false
        });



        if (!perlinWorley3DDataTexture) {
            if (!textureCallback) {
                Promise.all([
                    CloudsShader.loadPerlinWorley(perlinWorley3D).then(texture => {
                        perlinWorley3DDataTexture = texture;
                    }),
                    new THREE.TextureLoader().load(
                        blueNoise,
                        function (texture) {
                            texture.wrapS = THREE.RepeatWrapping;
                            texture.wrapT = THREE.RepeatWrapping;
                            texture.magFilter = THREE.LinearFilter;
                            texture.minFilter = THREE.LinearFilter;
                            blueNoiseTexture = texture;
                            blueNoiseTexture = texture;
                        },
                        undefined,
                        function (err) {
                            console.error('An error happened: ' + err);
                        }
                    )
                    ]).then(() => {
                    cloudsMaterial.uniforms.perlinWorley.value = perlinWorley3DDataTexture;
                    cloudsMaterial.uniforms.noise2D.value = blueNoiseTexture;
                    blurMaterial.uniforms.noise2D.value = blueNoiseTexture;
                });
            }
        } else {
            cloudsMaterial.uniforms.perlinWorley.value = perlinWorley3DDataTexture;
            cloudsMaterial.uniforms.noise2D.value = blueNoiseTexture;
            blurMaterial.uniforms.noise2D.value = blueNoiseTexture;
        }
    }

    getUniforms(){
        return cloudsMaterial.uniforms;
    }
    _createCloudsDebugPanel() {
        const self = this;
        // Create panel element
        const panel = document.createElement('div');
        panel.style.position = 'fixed';
        panel.style.top = '0';
        panel.style.right = '0';
        panel.style.backgroundColor = '#f0f0f0';
        panel.style.padding = '10px';
        panel.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
        panel.style.maxWidth = '300px';

        // Define labels and ranges
        const elements = [
            { label: 'density', min: 0, max: 2, value: self.density, step: 0.001, action: (val) => { self.density = val; } },
            { label: 'sun strength', min: 0, max: 10, value: self.luminance, step: 0.01, action: (val) => { self.luminance = val; } },
            { label: 'r', min: 0, max: 1, value: self.color.x, step: 0.01, action: (val) => { self.color.x = val; } },
            { label: 'g', min: 0, max: 1, value: self.color.y, step: 0.01, action: (val) => { self.color.y = val; } },
            { label: 'b', min: 0, max: 1, value: self.color.z, step: 0.01, action: (val) => { self.color.z = val; } },
            { label: 'wind speed', min: 0, max: 1, value: self.windSpeed, step: 0.01, action: (val) => { self.windSpeed = val; } },

        ];


        elements.forEach(element => {
            const container = document.createElement('div');
            container.style.display = 'flex';
            container.style.alignItems = 'center';
            container.style.justifyContent = 'space-between';
            container.style.marginBottom = '10px';

            const label = document.createElement('label');
            label.textContent = element.label;
            label.style.marginRight = '10px';

            const slider = document.createElement('input');
            slider.type = 'range';
            slider.min = element.min;
            slider.max = element.max;
            slider.step = element.step;
            slider.value = element.value;

            const valueDisplay = document.createElement('span');
            valueDisplay.style.minWidth = '50px';
            valueDisplay.style.textAlign = 'right';
            valueDisplay.textContent = slider.value.toString();

            slider.oninput = () => {
                valueDisplay.textContent = slider.value.toString();
                element.action(slider.value);
            };

            container.appendChild(label);
            container.appendChild(slider);
            container.appendChild(valueDisplay);
            panel.appendChild(container);
        });

        //// Clouds min max height
        const lowClouds = document.createElement('div');
        lowClouds.style.display = 'flex';
        lowClouds.style.alignItems = 'center';
        lowClouds.style.justifyContent = 'space-between';
        lowClouds.style.marginBottom = '10px';

        const lowCloudsLabel = document.createElement('label');
        lowCloudsLabel.textContent = "clouds Radius Start";
        lowCloudsLabel.style.marginRight = '10px';

        const lowCloudsSlider = document.createElement('input');
        lowCloudsSlider.type = 'range';
        lowCloudsSlider.min = 0.0;
        lowCloudsSlider.max = 50000;
        lowCloudsSlider.step = 1;
        lowCloudsSlider.value = self.startRadius;

        const lowCloudsValueDisplay = document.createElement('span');
        lowCloudsValueDisplay.style.minWidth = '50px';
        lowCloudsValueDisplay.style.textAlign = 'right';
        lowCloudsValueDisplay.textContent = lowCloudsSlider.value.toString();

        lowClouds.appendChild(lowCloudsLabel);
        lowClouds.appendChild(lowCloudsSlider);
        lowClouds.appendChild(lowCloudsValueDisplay);
        panel.appendChild(lowClouds);

        const highClouds = document.createElement('div');
        highClouds.style.display = 'flex';
        highClouds.style.alignItems = 'center';
        highClouds.style.justifyContent = 'space-between';
        highClouds.style.marginBottom = '10px';

        const highCloudsLabel = document.createElement('label');
        highCloudsLabel.textContent = "clouds Radius End";
        highCloudsLabel.style.marginRight = '10px';

        const highCloudsSlider = document.createElement('input');
        highCloudsSlider.type = 'range';
        highCloudsSlider.min = 1.0;
        highCloudsSlider.max = 50000;
        highCloudsSlider.step = 1.0;
        highCloudsSlider.value = self.endRadius;

        const highCloudsValueDisplay = document.createElement('span');
        highCloudsValueDisplay.style.minWidth = '50px';
        highCloudsValueDisplay.style.textAlign = 'right';
        highCloudsValueDisplay.textContent = highCloudsSlider.value.toString();

        highClouds.appendChild(highCloudsLabel);
        highClouds.appendChild(highCloudsSlider);
        highClouds.appendChild(highCloudsValueDisplay);
        panel.appendChild(highClouds);

        lowCloudsSlider.oninput = () => {
            lowCloudsValueDisplay.textContent = lowCloudsSlider.value.toString();
            this.startRadius = lowCloudsSlider.value;
            this.endRadius = Math.max(this.endRadius, this.startRadius);
            highCloudsSlider.value = this.endRadius;
            highCloudsValueDisplay.textContent = this.endRadius.toString();
        };
        highCloudsSlider.oninput = () => {
            highCloudsValueDisplay.textContent = highCloudsSlider.value.toString();
            this.endRadius = highCloudsSlider.value;
            this.startRadius = Math.min(this.startRadius, highCloudsSlider.value);
            lowCloudsSlider.value = this.startRadius;
            lowCloudsValueDisplay.textContent = lowCloudsSlider.value.toString();
        };



        document.body.appendChild(panel);

    }

}
export { CloudsLayer }



function _isMobileDevice() {
    return (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1);
};

function frobeniusNormDifference(matrixA, matrixB) {
    // Create a new matrix for the difference
    let difference = new THREE.Matrix4();

    // Extract the elements of the matrices
    let elementsA = matrixA.elements;
    let elementsB = matrixB.elements;

    // Compute the element-wise differences and store in the difference matrix
    let elementsDifference = [];
    for (let i = 0; i < elementsA.length; i++) {
        elementsDifference[i] = elementsA[i] - elementsB[i];
    }

    // Assign the computed differences to the difference matrix's elements
    difference.elements = elementsDifference;

    // Calculate the Frobenius norm of the difference
    let sumOfSquares = 0;
    for (let element of elementsDifference) {
        sumOfSquares += element * element;
    }

    return Math.sqrt(sumOfSquares);
}