import "regenerator-runtime/runtime.js";
import * as THREE from 'three';
import { Planet } from './planet/Planet.js';
import Stats from 'three/examples/jsm/libs/stats.module.js';
import { PanController } from './controls/PanController.js';
import { RotateController } from './controls/RotateController.js';
import { ZoomController } from './controls/ZoomController.js';
import { LayerManager } from './layers/LayerManager.js';
import { PostShader } from './PostShader.js';
import { VideoPostShader } from './VideoPostShader.js';
import { MapNavigator } from "./MapNavigator.js";
import { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer.js';
import opticalDepth from './images/optical_depth.png';
import water1 from './images/Water_1_M_Normal.jpg';
import water2 from './images/Water_2_M_Normal.jpg';
import perlin from './images/noise2.png';
import blueNoise from './images/blueNoise.png';
import { Controller } from "./controls/Controller.js";
import { getSunPosition } from "./Sun";
import { CSM } from 'three/addons/csm/CSM.js';
import { CSMHelper } from 'three/addons/csm/CSMHelper.js';
import ringsPalette from './images/ringsPalette.png';
import stars from './images/stars.png';
import nebula from './images/nebula2.png';
import nebulaPalette from './images/paletteNebula.png';
import perlinWorley3D from "./images/perlinWorley3D_128.bin"
import { ultraClock } from './controls/clock';
import { PositionBufferShaderMaterial } from "./materials/PositionBufferShaderMaterial.js";
// reused variables
const frustum = new THREE.Frustum();
const mat = new THREE.Matrix4();
const depths = new Uint8Array(4);
const depth24 = new THREE.Vector3();
const unpacker = new THREE.Vector3(1, 1 / 256, 1 / (256 * 256));
const A = new THREE.Vector3();
const B = new THREE.Vector3();
const loader = new THREE.TextureLoader();
const degreeToRadians = Math.PI / 180;
const cycle = 0.04; // a cycle of a flow map phase
const halfCycle = cycle * 0.5;
const waterScale = 1000;
const clock = new THREE.Clock();
const flowSpeed = 0.01;
class Map {
/**
* @param {Object} properties
* @param {String} properties.divID A div ID.
* @param {Boolean} [properties.debug=false] Display debug information.
* @param {Boolean} [properties.shadows=false] Display sunlight and shadows.
* @param {THREE.Vector3} [properties.atmosphere=new THREE.Vector3(0.1, 0.4, 1.0)] An atmosphere color. By thefault a blueish atmosphere is displayed.
* @param {Number} [properties.atmosphereDensity=1.0] An atmosphere density.
* @param {Boolean|Object} [properties.clock = false] add a clock.
* @param {THREE.Vector3|Boolean} [properties.sun=true] A sun color, defaults to a yelowish sun. Only taken into account when shadows is true. An explicitely "false" value switches the sun for a black hole (queue theremin music).
* @param {Boolean|THREE.Vector3} [properties.ocean=false] if true displays a blue ocean but a specific ocean color can be specified.
* @param {THREE.DataTexture} [properties.globalElevation=false] A texture representing the global elevation (equidistant cylindrical projection) used for post processing effects.
* @param {Boolean|Object} [properties.rings = false] Rings properties, if undefined, no rings are drawn
* @param {Number} [properties.detailMultiplier = 1.0] multiplier for loading terrain and 2D maps, a higher number loads higher detail data
* @param {Number} [properties.tileSize = 32] mesh resolution per tile.
* @param {Number} [properties.targetFPS] target FPS: defaults to 60 for desktop and 30 for mobile (FPS may not be precisely respected depending on monitor refresh rate)
* @param {Number} [properties.tileImagerySize = 256] Resolution of imagery per tile.
* @param {Boolean} [properties.loadOutsideView = false] loads higher LOD tiles outside view so that they are already loaded when the camera pans and turns
* @param {Boolean|Object|THREE.Color} [properties.space = true] if undefined, a default space backgound is drawn. Space can also be a single opaque color as a THREE.Vector3
* @param {Boolean} [properties.clock.timezone = false] add time-zone select widget.
* @param {Boolean} [properties.clock.dateTimePicker = false] add date picker widget.
* @param {THREE.Vector3} [properties.rings.origin=new THREE.Vector3()] the center point of the rings
* @param {THREE.Vector3} [properties.rings.normal=new THREE.Vector3(Math.random()-0.5, Math.random()-0.5, Math.random()-0.5).normalize()] the orientation of the rings plane
* @param {Number} [properties.rings.innerRadius=6378137.0 * (1.1+Math.random())] the rings inner radius
* @param {Number} [properties.rings.outerRadius=this.rings.innerRadius+(0.1+Math.random())*6378137.0] the rings outer radius
* @param {Number} [properties.rings.colorMap=Math.random()] a modulation on the ring colors
* @param {Number} [properties.rings.colorMapDisplace=Math.random()] rings displacement in a loop
* @param {Number} [properties.space.starsIntensity=0.75] The intensity of stars
* @param {Number} [properties.space.gasCloudsIntensity=0.25] the intensity of nebula like gasClouds
* @param {Number} [properties.space.colorMap=Math.random()] a modulation on gas cloud colors
* @param {Number} [properties.space.texRotation1= Math.random()*Math.PI] a texture rotation to avoid obvious repetition.
* @param {Number} [properties.space.texRotation2 = Math.random()*Math.PI] a texture rotation to avoid obvious repetition.
*
*/
constructor(properties) {
const self = this;
self.isMobile = _isMobileDevice();
self.positionBufferMaterial = PositionBufferShaderMaterial();
this.previousCameraPosition = new THREE.Vector3();
this.previousCameraRotation = new THREE.Euler();
this.loadOutsideView = properties.loadOutsideView ? properties.loadOutsideView : false;
if (properties.targetFPS) {
self.targetFPS = properties.targetFPS;
} else {
self.targetFPS = self.isMobile ? 31 : 61;
}
self.tileSize = properties.tileSize ? properties.tileSize : 32;
self.tileImagerySize = properties.tileImagerySize ? properties.tileImagerySize : 256;
this.detailMultiplier = properties.detailMultiplier ? properties.detailMultiplier : 1.0;
this.layerManager = new LayerManager();
this.layerManager.addListener("mapEnvLayerListener", this._layersChangedListener());
this.debug = properties.debug;
this.shadows = properties.shadows;
this.rings = properties.rings;
this.postCamera = new THREE.OrthographicCamera(- 1, 1, 1, - 1, 0, 1);
if (this.rings) {
if (!(typeof this.rings === 'object' && Array.isArray(this.rings))) this.rings = {};
if (!this.rings.origin) this.rings.origin = new THREE.Vector3();
if (!this.rings.normal) this.rings.normal = new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize();
if (!this.rings.innerRadius) this.rings.innerRadius = 6378137.0 * (1.1 + Math.random());
if (!this.rings.outerRadius) this.rings.outerRadius = this.rings.innerRadius + (0.1 + Math.random()) * 6378137.0;
if (!this.rings.colorMap) this.rings.colorMap = Math.random();
if (!this.rings.colorMapDisplace) this.rings.colorMapDisplace = Math.random();
}
this.globalElevation = properties.globalElevation;
if (!!properties.domContainer) {
this.domContainer = properties.domContainer;
} else if (!!properties.divID) {
this.domContainer = document.getElementById(properties.divID);
} else {
throw "cannot create Map without a domContainer or divID"
}
this.camera = !!properties.camera ? properties.camera : this._initCamera();
this.camera.layers.enable(31);
this.renderCamera = this.camera.clone();
this.scene = !!properties.scene ? properties.scene : this._initScene(properties.shadows);
if (properties.space && properties.space.isColor) {
this.space = false;
this.scene.background = properties.space;
} else {
this.space = properties.space;
if (this.space) {
if (!(typeof this.space === 'object' && Array.isArray(this.space))) this.space = {};
if (!this.space.starsIntensity) this.space.starsIntensity = 0.75;
if (!this.space.gasCloudsIntensity) this.space.gasCloudsIntensity = 0.5;//Math.random();
if (!this.space.colorMap) this.space.colorMap = Math.random();
if (!this.space.texRotation1) this.space.texRotation1 = Math.random() * Math.PI;
if (!this.space.texRotation2) this.space.texRotation2 = Math.random() * Math.PI;
}
}
this._resetLogDepthBuffer();
if (properties.debug) {
this._initStats();
const axesHelper = new THREE.AxesHelper(50000000);
this.scene.add(axesHelper);
}
this.ultraClock = ultraClock(properties.clock);
this.ultraClock.addListener(date => self._setDate(date));
this.ocean = properties.ocean;
this.atmosphere = properties.atmosphere;
if (!!this.atmosphere && !this.atmosphere.isVector3) this.atmosphere = new THREE.Vector3(0.1, 0.4, 1.0);
this.atmosphereDensity = properties.atmosphereDensity ? properties.atmosphereDensity : 1.0;
this.sunColor = properties.sun;
this._initRenderer(properties.shadows);
this._initLabelRenderer();
this._initPlanet(properties.shadows);
this._initController();
this.scene.add(this.planet);
this._setupRenderTarget();
this._setupPostScene();
this._setupPostMaterial();
this._setupDepthPassMaterial();
this._setupVideoPassMaterial();
this._startAnimation();
this.mapNavigator = new MapNavigator(this);
this.raycaster = new THREE.Raycaster();
this.selection = {};
}
_updateFlow() {
const delta = clock.getDelta();
const config = this.postMaterial.uniforms['waterConfig'];
config.value.x += flowSpeed * delta; // flowMapOffset0
config.value.y = config.value.x + halfCycle; // flowMapOffset1
// Important: The distance between offsets should be always the value of "halfCycle".
// Moreover, both offsets should be in the range of [ 0, cycle ].
// This approach ensures a smooth water flow and avoids "reset" effects.
if (config.value.x >= cycle) {
config.value.x = 0;
config.value.y = halfCycle;
} else if (config.value.y >= cycle) {
config.value.y = config.value.y - cycle;
}
}
/**
* Set the date (sun position)
* @param {Date} date
*/
_setDate(date) {
if (this.shadows) {
this.sunPosition = getSunPosition(date);
if (this.csm) {
this.csm.lightDirection.copy(this.sunPosition).negate();
}
}
}
/**
* Sets the given layer at the given index disposing of any layer previously at that index.
* @param {Layer} layer
* @param {Number} index
*/
setLayer(layer, index) {
this._prepareLayer(layer)
this.layerManager.setLayer(layer, index);
}
_prepareLayer(layer) {
if (layer.isOGC3DTilesLayer) {
layer._setMap(this);
}
if (layer.isOGC3DTilesLayer || layer.isObjectLayer) {
layer._setPlanet(this.planet);
layer._addToScene(this.scene);
}
if (layer.isI3SLayer) {
layer.addToScene(this.scene, this.camera);
}
if (layer.isTracksLayer) {
layer.addToScene(this.scene, this.camera);
}
}
/**
* appends the layer to the end of the list of layers, replacing any layer already at that position.
* @param {Layer} layer
* @param {Number} index
*/
addLayer(layer) {
this._prepareLayer(layer)
return this.layerManager.addLayer(layer);
}
/**
* removes the layer at the specific index optionally "disposing" of any resources the layer is using.
* @param {Number} index
* @param {Boolean} dispose
*/
removeLayer(index, dispose = true) {
this.layerManager.removeLayer(index, dispose);
}
/**
* Returns an array of layers currently loaded on the map
* @returns {Layer[]} the list of layers
*/
getLayers() {
return this.layerManager.getLayers();
}
/**
* Fetches a specific layer by ID.
* @param {Number|String} id
* @returns {Layer} the layer with given ID if any
*/
getLayerByID(id) {
return this.layerManager.getLayerByID(id);
}
_layersChangedListener() {
const self = this;
return () => {
let cloudsLayerEncountered = false;
self.layerManager.getLayers().forEach(layer => {
if (layer.isCloudsLayer && !cloudsLayerEncountered) {
if (layer.visible) {
cloudsLayerEncountered = true;
if (!self.cloudsLayer || self.cloudsLayer != layer) {
self.cloudsLayer = layer;
if (!layer.isInitialized) self.cloudsLayer._init(self);
console.log("new clouds layer loaded");
}
}
}
if (layer.isProjectedLayer && !layer.map) {
layer._init(self);
}
})
}
}
_initScene() {
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
if (this.shadows) {
this.sunPosition = getSunPosition(new Date())
const csmSplits = [];
for (let i = 0; i < 8; i++) {
if (i == 0) csmSplits.push(1);
else {
csmSplits.push(csmSplits[i - 1] * 3.2);
}
}
for (let i = 0; i < csmSplits.length; i++) {
csmSplits[i] /= csmSplits[csmSplits.length - 1];
}
this.csm = new CSM({
maxFar: 500000,
cascades: csmSplits.length,
mode: "custom",
customSplitsCallback: (cascadeCount, nearDistance, farDistance, target) => {
target.push(...csmSplits);
},
fade: true,
parent: scene,
shadowMapSize: this.isMobile ? 1024 : 2048,
lightIntensity: 3.5,
lightDirection: this.sunPosition.clone().negate(),
lightMargin: 500000,
shadowBias: -0.000001,
//noLastCascadeCutOff: true,
//shadowNormalBias : -5000,
camera: this.camera
});
this.csm.csmSplits = csmSplits;
for (let i = 0; i < this.csm.lights.length; i++) {
this.csm.lights[i].shadow.bias = 0.0001 * csmSplits[i];
this.csm.lights[i].shadow.normalBias = 0.1;
this.csm.lights[i].shadow.camera.near = 1;
this.csm.lights[i].shadow.camera.updateProjectionMatrix();
this.csm.lights[i].shadow.camera.far = this.csm.lightMargin + this.csm.maxFar * 2 * csmSplits[i];
this.csm.lights[i].shadow.needsUpdate = true;
}
//this.sun.shadow.bias = -0.005;
scene.add(new THREE.AmbientLight(0xFFFFFF, 0.8));
if (this.debug) {
this.csmHelper = new CSMHelper(this.csm);
this.csmHelper.visible = true;
scene.add(this.csmHelper);
const self = this;
document.addEventListener('keyup', (e) => {
if (e.key === 'u') {
console.log("csmHelper update");
self.csmHelper.update();
}
});
document.addEventListener('keyup', (e) => {
if (e.key === 'a') {
for (let i = 0; i < this.csm.lights.length; i++) {
this.csm.lights[i].shadow.normalBias *= 2;
this.csm.lights[i].shadow.needsUpdate = true;
}
}
if (e.key === 'q') {
for (let i = 0; i < this.csm.lights.length; i++) {
this.csm.lights[i].shadow.normalBias *= 0.5;
this.csm.lights[i].shadow.needsUpdate = true;
}
console.log("normalBiasDown " + this.csm.lights[0].shadow.normalBias);
}
if (e.key === 'z') {
for (let i = 0; i < this.csm.lights.length; i++) {
this.csm.lights[i].shadow.bias *= 2;
this.csm.lights[i].shadow.needsUpdate = true;
}
}
if (e.key === 's') {
for (let i = 0; i < this.csm.lights.length; i++) {
this.csm.lights[i].shadow.bias *= 0.5;
this.csm.lights[i].shadow.needsUpdate = true;
}
console.log("BiasDown " + this.csm.lights[0].shadow.bias);
}
});
}
} else {
scene.add(new THREE.AmbientLight(0xFFFFFF, 3.0));
}
return scene;
}
_setupRenderTarget() {
if (this.target) this.target.dispose();
this.target = new THREE.WebGLRenderTarget(this.domContainer.offsetWidth, this.domContainer.offsetHeight);
this.target.texture.format = THREE.RGBAFormat;
this.target.texture.colorSpace = THREE.SRGBColorSpace;
this.target.texture.minFilter = THREE.NearestFilter;
this.target.texture.magFilter = THREE.NearestFilter;
this.target.texture.premultiplyAlpha = true;
this.target.texture.generateMipmaps = false;
this.target.stencilBuffer = false;
this.target.depthBuffer = true;
this.target.depthTexture = new THREE.DepthTexture();
this.target.depthTexture.format = THREE.DepthFormat;
this.target.depthTexture.type = THREE.FloatType;
this.target.samples = 8;
// the depth render target is used to render depth to the main texture so that it can read retrieved on the CPU
if (this.depthTarget) this.depthTarget.dispose();
this.depthTarget = new THREE.WebGLRenderTarget(this.domContainer.offsetWidth, this.domContainer.offsetHeight);
this.depthTarget.texture.format = THREE.RGBAFormat;
this.depthTarget.texture.colorSpace = THREE.LinearSRGBColorSpace;
this.depthTarget.texture.minFilter = THREE.NearestFilter;
this.depthTarget.texture.magFilter = THREE.NearestFilter;
this.depthTarget.texture.generateMipmaps = false;
this.depthTarget.stencilBuffer = false;
this.depthTarget.depthBuffer = false;
if (this.target2) this.target2.dispose();
this.target2 = new THREE.WebGLRenderTarget(this.domContainer.offsetWidth, this.domContainer.offsetHeight);
this.target2.texture.format = THREE.RGBAFormat;
this.target2.texture.colorSpace = THREE.SRGBColorSpace;
this.target2.texture.minFilter = THREE.NearestFilter;
this.target2.texture.magFilter = THREE.NearestFilter;
this.target2.texture.premultiplyAlpha = true;
this.target2.texture.generateMipmaps = false;
this.target2.stencilBuffer = false;
this.target2.depthBuffer = true;
this.target2.depthTexture = new THREE.DepthTexture();
this.target2.depthTexture.format = THREE.DepthFormat;
this.target2.depthTexture.type = THREE.FloatType;
if (this.targetWorld) this.targetWorld.dispose();
this.targetWorld = new THREE.WebGLRenderTarget(this.domContainer.offsetWidth, this.domContainer.offsetHeight);
this.targetWorld.texture.format = THREE.RGBAFormat;
this.targetWorld.texture.type = THREE.FloatType;
this.targetWorld.texture.colorSpace = THREE.NoColorSpace;
this.targetWorld.texture.minFilter = THREE.NearestFilter;
this.targetWorld.texture.magFilter = THREE.NearestFilter;
this.targetWorld.texture.premultiplyAlpha = false;
this.targetWorld.texture.generateMipmaps = false;
this.targetWorld.stencilBuffer = false;
this.targetWorld.depthBuffer = true;
this.targetWorld.depthTexture = new THREE.DepthTexture();
this.targetWorld.depthTexture.format = THREE.DepthFormat;
this.targetWorld.depthTexture.type = THREE.FloatType;
}
_setupBlurMaterials() {
}
_setupPostScene() {
const postPlane = new THREE.PlaneGeometry(2, 2);
this.postQuad = new THREE.Mesh(postPlane);
this.postScene = new THREE.Scene();
this.postScene.add(this.postQuad);
this.postScene.matrixAutoUpdate = false;
this.postQuad.matrixAutoUpdate = false;
}
_setupPostMaterial() {
// Setup post processing stage
const self = this;
this.postMaterial = new THREE.ShaderMaterial({
vertexShader: PostShader.vertexShader(),
fragmentShader: self.shadows ? PostShader.fragmentShaderShadows(self.atmosphere, self.ocean, self.sunColor, !!self.globalElevation, self.rings, self.space, true) : PostShader.fragmentShader(self.atmosphere, self.ocean, self.rings, self.space, true),
uniforms: {
cameraNear: { value: this.camera.near },
cameraFar: { value: this.camera.far },
tDiffuse: { value: null },
tDepth: { value: null },
radius: { value: 0 },
mobile: { value: this.isMobile },
xfov: { value: 0 },
yfov: { value: 0 },
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) },
heightAboveSeaLevel: { value: 0 },
opticalDepth: { value: null },
perlin: { value: null },
water1: { value: null },
water2: { value: null },
waterConfig: { value: new THREE.Vector4(0, halfCycle, halfCycle, waterScale) },
ldf: { value: 0 },
sunLocation: { value: new THREE.Vector3(0, 0, 0) },
projMatrixInv: { value: new THREE.Matrix4() },
viewMatrixInv: { value: new THREE.Matrix4() },
ringsPalette: { value: null },
starsTexture: { value: null },
nebulaTexture: { value: null },
nebulaPalette: { value: null },
tClouds: { value: null },
tCloudsDepth: { value: null },
time: { value: 0.0 },
atmosphereDensity: { value: this.atmosphereDensity }
},
depthTest: false,
depthWrite: false
});
if (self.globalElevation) {
self.postMaterial.uniforms.globalElevation = { type: "t", value: self.globalElevation };
};
loader.load(
// resource URL
opticalDepth,
// onLoad callback
function (texture) {
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearFilter;
self.postMaterial.uniforms.opticalDepth.value = texture;
},
undefined,
function (err) {
console.error('An error happened: ' + err);
}
);
loader.load(
// resource URL
ringsPalette,
// onLoad callback
function (texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearFilter;
self.postMaterial.uniforms.ringsPalette.value = texture;
},
undefined,
function (err) {
console.error('An error happened: ' + err);
}
);
if (this.shadows && this.ocean) {
loader.load(
// resource URL
water1,
// onLoad callback
function (texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearFilter;
self.postMaterial.uniforms.water1.value = texture;
},
undefined,
function (err) {
console.error('An error happened: ' + err);
}
);
loader.load(
water2,
function (texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearFilter;
self.postMaterial.uniforms.water2.value = texture;
},
undefined,
function (err) {
console.error('An error happened: ' + err);
}
);
}
if (self.space) {
loader.load(
perlin,
function (texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearFilter;
self.postMaterial.uniforms.perlin.value = texture;
},
undefined,
function (err) {
console.error('An error happened: ' + err);
}
);
}
if (this.space) {
loader.load(
stars,
function (texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearFilter;
self.postMaterial.uniforms.starsTexture.value = texture;
},
undefined,
function (err) {
console.error('An error happened: ' + err);
}
);
loader.load(
nebula,
function (texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearFilter;
self.postMaterial.uniforms.nebulaTexture.value = texture;
},
undefined,
function (err) {
console.error('An error happened: ' + err);
}
);
loader.load(
nebulaPalette,
function (texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearFilter;
self.postMaterial.uniforms.nebulaPalette.value = texture;
},
undefined,
function (err) {
console.error('An error happened: ' + err);
}
);
}
}
_setupDepthPassMaterial() {
this.depthPassMaterial = new THREE.ShaderMaterial({
vertexShader: PostShader.vertexShader(),
fragmentShader: PostShader.depthPassFragmentShader(),
uniforms: {
cameraNear: { value: this.camera.near },
cameraFar: { value: this.camera.far },
tDepth: { value: null },
ldf: { value: 0 },
}
});
}
_setupVideoPassMaterial() {
this.videoPassMaterial = new THREE.ShaderMaterial({
vertexShader: VideoPostShader.vertexShader(),
fragmentShader: VideoPostShader.fragmentShader(),
uniforms: {
tColor: { value: null },
tWorld: { value: null },
tVideoColor: { value: null },
tVideoWorld: { value: null },
videoProjectionMatrix: { value: new THREE.Matrix4() },
videoViewMatrix: { value: new THREE.Matrix4() },
depthTest: {value: true},
chromaKeying: {value: false},
chromaKey: {value: null},
chromaKeyTolerance: {value: 0.2}
},
depthTest: false,
depthWrite: false,
precision: "highp"
});
}
_initRenderer(shadows) {
let self = this;
self.renderer = new THREE.WebGLRenderer({ antialias: true, maxSamples: 4, logarithmicDepthBuffer: true, stencil: false, preserveDrawingBuffer: false, powerPreference: "high-performance" });
//self.renderer.getContext().getProgramInfoLog= function() { return '' }
//self.renderer.debug.checkShaderErrors = false;
if (shadows) {
self.renderer.shadowMap.enabled = true;
if (self.isMobile) {
self.renderer.shadowMap.type = THREE.PCFShadowMap;
} else {
self.renderer.shadowMap.type = THREE.PCFShadowMap;
}
}
self.renderer.setPixelRatio(1)
self.renderer.setSize(this.domContainer.offsetWidth, this.domContainer.offsetHeight);
self.renderer.outputColorSpace = THREE.SRGBColorSpace;
self.renderer.autoClear = false;
THREE.ShaderChunk.tonemapping_pars_fragment = THREE.ShaderChunk.tonemapping_pars_fragment.replace(
'vec3 CustomToneMapping( vec3 color ) { return color; }',
`#define Uncharted2Helper( x ) max( ( ( x * ( 0.15 * x + 0.10 * 0.50 ) + 0.20 * 0.02 ) / ( x * ( 0.15 * x + 0.50 ) + 0.20 * 0.30 ) ) - 0.02 / 0.30, vec3( 0.0 ) )
float toneMappingWhitePoint = 1.0;
vec3 CustomToneMapping( vec3 color ) {
color *= toneMappingExposure;
return saturate( Uncharted2Helper( color ) / Uncharted2Helper( vec3( toneMappingWhitePoint ) ) );
}`
);
self.renderer.toneMapping = THREE.CustomToneMapping;
self.renderer.toneMappingExposure = 0.2;
self.renderer.domElement.style.overflow = "hidden";
self.domContainer.appendChild(self.renderer.domElement);
window.addEventListener('resize', onWindowResize);
function onWindowResize() {
const aspect = self.domContainer.offsetWidth / self.domContainer.offsetHeight;
self.camera.aspect = aspect;
self.camera.updateProjectionMatrix();
self.target.setSize(self.domContainer.offsetWidth, self.domContainer.offsetHeight);
self.depthTarget.setSize(self.domContainer.offsetWidth, self.domContainer.offsetHeight);
self.target2.setSize(self.domContainer.offsetWidth, self.domContainer.offsetHeight);
if (self.cloudsLayer) {
self.cloudsLayer.changeSize(self.domContainer);
}
self.renderer.setSize(self.domContainer.offsetWidth, self.domContainer.offsetHeight);
self.labelRenderer.setSize(self.domContainer.offsetWidth, self.domContainer.offsetHeight);
}
setTimeout(onWindowResize, 1000);
if (self.debug) {
const gl = self.renderer.getContext(); // Get the underlying WebGL context
// WebGL provides a few parameters that can help us infer depth precision
// Although not directly specifying the depth buffer precision, these can offer some insights
const depthBits = gl.getParameter(gl.DEPTH_BITS);
console.log(`Depth Bits: ${depthBits}`);
// Additionally, you can check for the presence of certain WebGL extensions
const depthTextureExtension = gl.getExtension('WEBGL_depth_texture');
if (depthTextureExtension) {
console.log('Depth texture extension supported.');
} else {
console.log('Depth texture extension not supported.');
}
}
}
_initLabelRenderer() {
this.labelRenderer = new CSS3DRenderer();
this.labelRenderer.setSize(this.domContainer.offsetWidth, this.domContainer.offsetHeight);
this.labelRenderer.domElement.style.position = 'absolute';
this.labelRenderer.domElement.style.top = '0px';
this.labelRenderer.domElement.style.pointerEvents = 'none';
document.body.appendChild(this.labelRenderer.domElement);
}
_initStats() {
this.stats = new Stats();
this.domContainer.appendChild(this.stats.dom);
}
_initCamera() {
const camera = new THREE.PerspectiveCamera(40, this.domContainer.offsetWidth / this.domContainer.offsetHeight, 0.01, 50000000);
camera.position.set(40000000, 0, 0);
camera.up.set(0, 0, 1)
camera.lookAt(new THREE.Vector3(-0, 0, 10000));
camera.updateProjectionMatrix();
return camera;
}
_initPlanet() {
this.planet = new Planet({
camera: this.renderCamera,
center: new THREE.Vector3(0, 0, 0),
shadows: this.csm,
layerManager: this.layerManager,
renderer: this.renderer,
detailMultiplier: this.detailMultiplier,
tileSize: this.tileSize,
tileImagerySize: this.tileImagerySize,
loadOutsideView: this.loadOutsideView
});
this.resetCameraNearFar();
}
_initController() {
const self = this;
self.controller = new Controller(self.camera, self.domContainer, self);
//self.controller.append(new SelectController(self.camera, self.domContainer, self));
self.controller.append(new PanController(self.camera, self.domContainer, self));
self.controller.append(new RotateController(self.camera, self.domContainer, self));
self.controller.append(new ZoomController(self.camera, self.domContainer, self));
self.domContainer.addEventListener('mousedown', (e) => {
if (!!self.controller && !self.pause) self.controller.event('mousedown', e);
}, false);
self.domContainer.addEventListener('mouseup', (e) => {
if (!!self.controller && !self.pause) self.controller.event('mouseup', e);
}, false);
self.domContainer.addEventListener('mousemove', (e) => {
if (!!self.controller && !self.pause) self.controller.event('mousemove', e);
}, false);
self.domContainer.addEventListener('wheel', (e) => {
if (!!self.controller && !self.pause) self.controller.event('mousewheel', e);
}, false);
self.domContainer.addEventListener('touchstart', (e) => {
if (!!self.controller && !self.pause) self.controller.event('touchstart', e);
}, false);
self.domContainer.addEventListener('touchmove', (e) => {
if (!!self.controller && !self.pause) self.controller.event('touchmove', e);
}, false);
self.domContainer.addEventListener('touchcancel', (e) => {
if (!!self.controller && !self.pause) self.controller.event('touchcancel', e);
}, false);
self.domContainer.addEventListener('touchend', (e) => {
if (!!self.controller && !self.pause) self.controller.event('touchend', e);
}, false);
self.domContainer.addEventListener('keydown', (e) => {
if (!!self.controller && !self.pause) self.controller.event('keydown', e);
}, false);
self.domContainer.addEventListener('keyup', (e) => {
if (!!self.controller && !self.pause) self.controller.event('keyup', e);
}, false);
document.addEventListener("mouseleave", function (event) {
if (event.clientY <= 0 || event.clientX <= 0 || (event.clientX >= self.domContainer.offsetWidth || event.clientY >= self.domContainer.offsetHeight)) {
self.controller.event('mouseup', { which: "all" });
}
});
}
/**
* Pauses the rendering of all the layers.
*/
pauseRendering() {
this.pause = true;
this.planet._pauseRendering();
this.layerManager._pauseRendering();
}
/**
* Resumes the rendering of all the layers
*/
resumeRendering() {
this.pause = false;
this.planet._resumeRendering();
this.layerManager._resumeRendering();
}
_startAnimation() {
var self = this;
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const delta = performance.now() - lastTime;
//console.log(delta);
self.planet.update();
if (delta < 1000 / self.targetFPS) {
return;
}
lastTime = performance.now();
if (!self.pause) {
self.controller.update();
const cameraOffset = self.renderCamera.position.length();
if (cameraOffset > 1000) {
self.scene.position.set(-self.camera.position.x, -self.camera.position.y, -self.camera.position.z);
self.scene.updateMatrix();
self.scene.updateMatrixWorld(true);
self.layerManager.layers.forEach((layer) => {
if (layer.isOGC3DTilesLayer) {
layer._updateMatrices();
}
if (layer.isObjectLayer) {
layer._updateMatrices();
}
})
}
self.renderCamera.copy(self.camera, true);
self.renderCamera.position.add(self.scene.position)
self.renderCamera.updateMatrix();
self.renderCamera.updateMatrixWorld(true);
self.renderCamera.updateProjectionMatrix();
if (self.shadows) {
self.csm.update(self.renderCamera.matrix);
}
let hasVideo = false;
self.layerManager.layers.forEach(layer => {
if (layer.isOGC3DTilesLayer) {
layer.update(self.renderCamera);
}
if (layer.isTracksLayer) {
layer.update(clock);
}
if (layer.isProjectedLayer && layer.visible && layer.isReady()) {
hasVideo = true;
}
})
//self.controller.update();
//frustum.setFromProjectionMatrix(mat.multiplyMatrices(self.camera.projectionMatrix, self.camera.matrixWorldInverse));
//self.camera.updateMatrixWorld();
self.renderer.setRenderTarget(self.target);
self.renderer.render(self.scene, self.renderCamera);
/// depth
self.depthPassMaterial.uniforms.tDepth.value = self.target.depthTexture;
self.depthPassMaterial.uniforms.cameraNear.value = self.camera.near;
self.depthPassMaterial.uniforms.cameraFar.value = self.camera.far;
self.depthPassMaterial.uniforms.ldf.value = self.logDepthBufFC;
self.renderer.setRenderTarget(self.depthTarget);
self.postQuad.material = self.depthPassMaterial;
self.renderer.render(self.postScene, self.postCamera);
// compute values for post
self.postMaterial.uniforms.cameraNear.value = self.camera.near;
self.postMaterial.uniforms.cameraFar.value = self.camera.far;
self.postMaterial.uniforms.xfov.value = 2 * Math.atan(Math.tan(self.camera.fov * Math.PI / 180 / 2) * self.camera.aspect) * 180 / Math.PI;
self.postMaterial.uniforms.yfov.value = self.camera.fov;
self.postMaterial.uniforms.nonPostCameraPosition.value = self.camera.position;
self.postMaterial.uniforms.ldf.value = self.logDepthBufFC;
self.camera.getWorldDirection(self.postMaterial.uniforms.viewCenterFar.value).normalize();
self.postMaterial.uniforms.viewCenterNear.value.copy(self.postMaterial.uniforms.viewCenterFar.value);
self.postMaterial.uniforms.up.value = self.camera.up.normalize();
self.postMaterial.uniforms.right.value.crossVectors(self.camera.up, self.postMaterial.uniforms.viewCenterFar.value);
self.postMaterial.uniforms.viewCenterFar.value.multiplyScalar(self.camera.far).add(self.camera.position);
self.postMaterial.uniforms.viewCenterNear.value.multiplyScalar(self.camera.near).add(self.camera.position);
/// video
let t1 = self.target;
let t2 = self.target2;
//self.camera.updateProjectionMatrix()
if (hasVideo) {
self.renderer.setRenderTarget(self.targetWorld);
//self.renderer.clearDepth();
self.scene.overrideMaterial = self.positionBufferMaterial;
self.renderer.render(self.scene, self.renderCamera);
// copy main camera params to video post shader
self.layerManager.layers.forEach(layer => {
if (layer.isProjectedLayer && layer.visible && layer.isReady()) {
if (true) {
layer.projectionRenderCamera.copy(layer.projectionCamera, true);
layer.projectionRenderCamera.position.add(self.scene.position);
layer.projectionRenderCamera.updateMatrix();
layer.projectionRenderCamera.updateMatrixWorld(true);
layer.projectionRenderCamera.updateProjectionMatrix();
self.renderer.setRenderTarget(layer.renderTarget);
//self.renderer.setClearColor(0xff0000, 1); // Set a clear color
//self.renderer.clear(true, true, true);
self.renderer.render(self.scene, layer.projectionRenderCamera);
self.videoPassMaterial.uniforms.tColor.value = t1.texture;
self.videoPassMaterial.uniforms.tWorld.value = self.targetWorld.texture;
self.videoPassMaterial.uniforms.tVideoColor.value = layer.texture;
self.videoPassMaterial.uniforms.tVideoWorld.value = layer.renderTarget.texture;
self.videoPassMaterial.uniforms.depthTest.value = layer.depthTest;
self.videoPassMaterial.uniforms.chromaKeying.value = layer.chromaKeying;
self.videoPassMaterial.uniforms.chromaKey.value = layer.chromaKey;
self.videoPassMaterial.uniforms.chromaKeyTolerance.value = layer.chromaKeyTolerance;
self.videoPassMaterial.uniforms.videoProjectionMatrix.value.copy(layer.projectionRenderCamera.projectionMatrix);
self.videoPassMaterial.uniforms.videoViewMatrix.value.copy(layer.projectionRenderCamera.matrixWorldInverse);
self.postQuad.material = self.videoPassMaterial;
self.renderer.setRenderTarget(t2);
//self.renderer.clear(true, true, true);
self.renderer.render(self.postScene, self.postCamera);
let temp = t2;
t2 = t1;
t1 = temp;
}
}
})
self.scene.overrideMaterial = null;
}
/// clouds
if (self.cloudsLayer) {
self.cloudsLayer.updateUniforms(self);
self.cloudsLayer.render(self);
self.postMaterial.uniforms.tClouds.value = self.cloudsLayer.getOutputTexture();
self.postMaterial.uniforms.tCloudsDepth.value = self.cloudsLayer.getOutputDepthTexture();
}
/// post final
self.postMaterial.uniforms.tDiffuse.value = t1.texture;
self.postMaterial.uniforms.tDepth.value = self.target.depthTexture;
self.postMaterial.uniforms.radius.value = self.planet.radius;
self.postMaterial.uniforms.planetPosition.value = self.planet.position;
self.postMaterial.uniforms.projMatrixInv.value.copy(self.camera.projectionMatrixInverse);
self.postMaterial.uniforms.viewMatrixInv.value.copy(self.camera.matrixWorld);
if (self.shadows) {
self.postMaterial.uniforms.sunLocation.value.copy(self.sunPosition);
}
self.postMaterial.uniforms.heightAboveSeaLevel.value = self.camera.position.length() - self.planet.radius;
//water
self._updateFlow();
self.postMaterial.uniforms.time.value = clock.elapsedTime;
self.renderer.setRenderTarget(null);
self.postQuad.material = self.postMaterial;
self.renderer.render(self.postScene, self.postCamera);
self.labelRenderer.render(self.scene, self.camera);
}
if (self.stats) {
self.stats.update();
}
}
animate();
}
/**
* When moving the map.camera manually, you may want to call this method to correctly set the camera near and far to limit z-fighting artefacts.
*/
resetCameraNearFar() {
const heightAboveEllipsoid = Math.max(this.distToGround, this.camera.position.length() - this.planet.radius);
this.camera.near = 0.0001;
const distanceToHorizon = Math.sqrt(2 * this.planet.radius * Math.abs(heightAboveEllipsoid) + heightAboveEllipsoid * heightAboveEllipsoid); // estimation
this.camera.far = Math.max(200000, distanceToHorizon * 2.0);
//console.log(distanceToHorizon)
this.camera.updateProjectionMatrix();
this._resetLogDepthBuffer();
if(this.csm){
this.csm.maxFar = Math.min(500000, this.camera.far);
for (let i = 0; i < this.csm.lights.length; i++) {
this.csm.lights[i].shadow.bias = 0.0001 * this.csm.csmSplits[i];
this.csm.lights[i].shadow.normalBias = 0.1;
this.csm.lights[i].shadow.camera.near = 1;
this.csm.lights[i].shadow.camera.updateProjectionMatrix();
this.csm.lights[i].shadow.camera.far = this.csm.lightMargin + this.csm.maxFar * 2 * this.csm.csmSplits[i];
this.csm.lights[i].shadow.needsUpdate = true;
}
this.csm.updateFrustums()
}
}
_resetLogDepthBuffer() {
this.logDepthBufFC = 2.0 / (Math.log(this.camera.far + 1.0) / Math.LN2);
}
/**
* Moves the camera 1 meter above the ground.
*/
moveCameraAboveSurface() {
try {
let geodeticCameraPosition = this.planet.llhToCartesian.inverse(this.camera.position);
B.set(geodeticCameraPosition.x * degreeToRadians, geodeticCameraPosition.y * degreeToRadians);
this.distToGround = geodeticCameraPosition.z - this.planet.getTerrainElevation(B);
if (this.distToGround < 5) {
geodeticCameraPosition.z += (5 - this.distToGround);
geodeticCameraPosition = this.planet.llhToCartesian.forward(geodeticCameraPosition);
this.camera.position.set(geodeticCameraPosition.x, geodeticCameraPosition.y, geodeticCameraPosition.z);
}
} catch (e) { }
}
/**
* reset the camera up so that the camera roll alligns with the horizon
*/
setCameraUp() {
this.camera.getWorldDirection(A).normalize();
B.crossVectors(this.camera.position, A);
this.camera.up.crossVectors(A, B).normalize();
}
/**
* Moves the camera to a location in lon lat height and looks at another location in lon lat height.
*
* @param {Object} cameraPosition an object representing the camera desired location in lon lat height (according to WGS84 coordinates)
* @param {Number} cameraPosition.x longitude
* @param {Number} cameraPosition.y latitude
* @param {Number} cameraPosition.z height
* @param {Object} cameraAim an object representing the camera desired target in lon lat height (according to WGS84 coordinates)
* @param {Number} cameraAim.x longitude
* @param {Number} cameraAim.y latitude
* @param {Number} cameraAim.z height
*/
moveAndLookAt(cameraPosition, cameraAim) {
this.camera.position.copy(this.planet.llhToCartesian.forward(cameraPosition));
const target = this.planet.llhToCartesian.forward(cameraAim);
this.camera.up.copy(this.camera.position).normalize()
this.camera.lookAt(target.x, target.y, target.z);
this.moveCameraAboveSurface();
this.resetCameraNearFar();
this.setCameraUp();
}
/**
* Get the hit location of a ray going from the camera through a pixel on screen or undefined if the ray does not hit anything.
* @param {Number} x a screen pixel x coordinate
* @param {Number} y a screen pixel y coordinate
* @param {THREE.Vector3} sideEffect a THREE.Vector3 that will be moved to the ray hit location
* @returns {THREE.Vector3} the sideEffect object.
*/
screenPixelRayCast(x, y, sideEffect) {
this.renderer.readRenderTargetPixels(this.depthTarget, x - this.domContainer.offsetLeft, (this.domContainer.offsetHeight - (y - this.domContainer.offsetTop)), 1, 1, depths);
depth24.set(depths[0], depths[1], depths[2]);
let z = depth24.dot(unpacker);
z = (z * 0.00390630960555428397039749752041);
if (z <= 0 || z >= 1) {
sideEffect.copy(this.camera.position);
return;
}
z = -(Math.pow(2, z * Math.log2(this.camera.far + 1.0)) - 1.0);
z = this._viewZToPerspectiveDepth(z, this.camera.near, this.camera.far);
z = z * 2 - 1;
x = ((x - this.domContainer.offsetLeft) / this.domContainer.offsetWidth) * 2 - 1;
y = (1 - ((y - this.domContainer.offsetTop) / this.domContainer.offsetHeight)) * 2 - 1;
sideEffect.set(x, y, z).unproject(this.camera);
return sideEffect;
}
_viewZToPerspectiveDepth(viewZ, near, far) {
return ((near + viewZ) * far) / ((far - near) * viewZ);
}
/**
* Transforms a lon lat height point (degrees) to cartesian coordinates (EPSG:4978).
* The transform is slightly inaccurate compared to proj4 but it's 3 times faster
* @param {THREE.Vector3} llh
*/
llhToCartesianFastSFCT(llh) {
this.planet.llhToCartesianFastSFCT(llh);
}
/**
* Transforms a xyz point (degrees) to llh coordinates (EPSG:4326).
* The transform is slightly inaccurate compared to proj4 but it's 2.5 times faster
* @param {THREE.Vector3} llh
*/
cartesianToLlhFastSFCT(xyz) {
this.planet.cartesianToLlhFastSFCT(xyz);
}
/**
* Set an elevation exageration factor
* @param {Number} elevationExageration
*/
setElevationExageration(elevationExageration) {
this.elevationExageration = elevationExageration;
this.planet.setElevationExageration(this.elevationExageration);
}
addSelectionListener(calback) {
if (!this.selectionListeners) this.selectionListeners = [];
this.selectionListeners.push(calback);
}
removeSelectionListener(callback) {
if (this.selectionListeners) this.selectionListeners.filter(f => f !== callback);
}
/**
* select action at a particular location on this map (normalized between -1 and 1)
* @param {THREE.Vector2} screenLocation
* @param {Number} type 0(Add), 1(Remove) or 2(Replace)
*/
select(screenLocation, type) {
this.raycaster.setFromCamera(screenLocation, this.camera);
const selectableObjects = [];
const layers = this.layerManager.getLayers();
for (let i = layers.length - 1; i >= 0; i--) {
const l = layers[i];
if (l) {
const selectable = l.getSelectableObjects();
while (selectable.length) selectableObjects.push(selectable.shift());
}
}
const select = this.raycaster.intersectObjects(selectableObjects, false);
const selected = [];
const unselected = [];
if (type == 0) {
select.forEach(object => {
if (!this.selection[object.object.layer.id]) {
this.selection[object.object.layer.id] = [];
}
if (!this.selection[object.object.layer.id].includes(object)) {
this.selection[object.object.layer.id].push(object);
selected.push(object.object);
}
});
for (const layerID in this.selection) {
const selectLayer = this.layerManager.getLayerByID(layerID);
selectLayer.select(this.selection[layerID]);
}
} else if (type == 1) {
select.forEach(object => {
if (this.selection[object.object.layer.id] && this.selection[object.object.layer.id].includes(object.object)) {
this.selection[object.object.layer.id].filter(o => o !== object.object);
if (!this.selection[object.object.layer.id].length) delete this.selection[object.object.layer.id];
unselected.push(object.object);
object.object.layer.unselect([object.object])
}
});
} else if (type == 2) {
// unselect everything
for (const key in this.selection) {
const unselectLayer = this.layerManager.getLayerByID(key);
unselectLayer.unselect(this.selection[key])
while (this.selection[key].length) unselected.push(this.selection[key].shift());
}
this.selection = {};
// select first object
if (select.length > 0) {
const object = select[0].object;
if (!this.selection[object.layer.id]) {
this.selection[object.layer.id] = [];
}
if (!unselected.includes(object)) {
this.selection[object.layer.id].push(object);
object.layer.select([object]);
selected.push(object);
unselected.filter(o => o !== object);
}
}
}
const selections = {
selection: this.selection,
selected: selected,
unselected: unselected,
}
if (this.selectionListeners) this.selectionListeners.forEach(callback => callback(selections))
return selections;
}
_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: 10, value: self.clouds.density, step: 0.1, action: (val) => { self.clouds.density = val; } },
{ label: 'sun strength', min: 0, max: 10, value: self.clouds.luminance, step: 0.01, action: (val) => { self.clouds.luminance = val; } },
{ label: 'coverage', min: 0, max: 1, value: self.clouds.coverage, step: 0.01, action: (val) => { self.clouds.coverage = val; } },
{ label: 'r', min: 0, max: 1, value: self.clouds.color.x, step: 0.01, action: (val) => { self.clouds.color.x = val; } },
{ label: 'g', min: 0, max: 1, value: self.clouds.color.y, step: 0.01, action: (val) => { self.clouds.color.y = val; } },
{ label: 'b', min: 0, max: 1, value: self.clouds.color.z, step: 0.01, action: (val) => { self.clouds.color.z = val; } },
{ label: 'wind speed', min: 0, max: 1, value: self.clouds.windSpeed, step: 0.01, action: (val) => { self.clouds.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 = 1.0;
lowCloudsSlider.max = 1.1;
lowCloudsSlider.step = 0.001;
lowCloudsSlider.value = self.clouds.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 = 1.1;
highCloudsSlider.step = 0.001;
highCloudsSlider.value = self.clouds.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.clouds.startRadius = lowCloudsSlider.value;
this.clouds.endRadius = Math.max(this.clouds.endRadius, lowCloudsSlider.value);
highCloudsSlider.value = this.clouds.endRadius;
highCloudsValueDisplay.textContent = highCloudsSlider.value.toString();
};
highCloudsSlider.oninput = () => {
highCloudsValueDisplay.textContent = highCloudsSlider.value.toString();
this.clouds.endRadius = highCloudsSlider.value;
this.clouds.startRadius = Math.min(this.clouds.startRadius, highCloudsSlider.value);
lowCloudsSlider.value = this.clouds.startRadius;
lowCloudsValueDisplay.textContent = lowCloudsSlider.value.toString();
};
document.body.appendChild(panel);
}
}
function _perspectiveDepthToViewZ(invClipZ, near, far) {
return (near * far) / ((far - near) * invClipZ - far);
}
function _isMobileDevice() {
return (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1);
};
export { Map };