import * as THREE from 'three';
import { ElevationLayer } from './ElevationLayer.js'
import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js';
import { PerlinElevationWorker } from './workers/PerlinElevation.worker.js';
let id = 0;
let nextWorker = 0;
function getConcurency() {
return 1;
if ('hardwareConcurrency' in navigator) {
return navigator.hardwareConcurrency;
} else {
return 4;
}
}
const meshGeneratorWorkers = [];
const workerCallbacks = new Map();
const workerOnErrors = new Map();
const blob = new Blob([PerlinElevationWorker.getScript()], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(blob);
for (let i = 0; i < getConcurency(); i++) {
const elevationMesherWorker = new Worker(workerUrl);
elevationMesherWorker.onmessage = handleWorkerResponse;
elevationMesherWorker.onerror = handleWorkerError;
meshGeneratorWorkers.push(elevationMesherWorker);
}
function sendWorkerTask(data, callback, onerror) {
const messageID = id++;
workerCallbacks.set(messageID, callback);
workerOnErrors.set(messageID, onerror);
nextWorker = (nextWorker + 1) % meshGeneratorWorkers.length;
meshGeneratorWorkers[nextWorker].postMessage({ id: messageID, input: data });
}
function handleWorkerResponse(e) {
if (e.data.error) {
workerOnErrors.get(e.data.id)(e.data.error);
} else {
workerCallbacks.get(e.data.id)(e.data.result);
}
workerCallbacks.delete(e.data.id);
workerOnErrors.delete(e.data.id);
}
function handleWorkerError(error) {
console.error("uncaught elevation mesher worker error : " + error)
}
const perlin = new ImprovedNoise();
const rand = Math.random();
const halfPI = Math.PI * 0.5;
function noise(x, y, z) {
return perlin.noise(x + rand, y + rand, z + rand) * 2;
}
/**
* An elevation layer that generates on the fly elevation using a mixture of noise techniques
* @class
* @extends ElevationLayer
*/
class PerlinElevationLayer extends ElevationLayer {
/**
* Base constructor for elevation layers.
* @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 {Number} properties.minHeight min terrain height relative to sea level
* @param {Number} properties.maxHeight max terrain height relative to sea level
* @param {Number[]} [properties.bounds=[-180, -90, 180, 90]] min longitude, min latitude, max longitude, max latitude in degrees
* @param {Boolean} [properties.visible = true] layer will be rendered if true (true by default)
*/
constructor(properties) {
super(properties);
this.min = properties.minHeight ? this.minHeight : -100000;
this.max = properties.maxHeight ? this.maxHeight : 100000;
this.maxOctaveSimplex = 3 + Math.random() * 3;
this.gainSimplex = 0.2 + Math.random() * 0.3;//0.5 + Math.random() * 0.2;
this.maxOctaveTurbulence = 3 + Math.random() * 2;
this.gainTurbulence = 0.2 + Math.random() * 0.23;//0.7;//0.5 + Math.random() * 0.2;
this.warpFactorMultiplier = Math.random() * 0.3 + 0.1;
this.continentFrequency = 0.2 + Math.random() * 2;
this.turbulenceUp = 0.25 + Math.random() * 0.5;
this.freqSup = 0.5 + Math.random() * 1;
this.gains = Array.from({ length: 20 }, () => 0.55+Math.random()*0.1);
this.shift = [Math.random()*360, Math.random()*360, Math.random()*360]
this.lacunarities = [1+Math.random(), 20+Math.random()*70]/* [1+Math.random(), 1+Math.random()*20] */;//Array.from({ length: 20 }, () => 1.6+Math.random()*0.2);
let f = 1;
for(let i = 0; i<this.lacunarities.length; i++){
f*=this.lacunarities[i];
}
let l = Math.pow(200000/f, 1/(15-this.lacunarities.length));
this.lacunarities.push(l);
this.noiseTypes = Array.from({ length: 20 }, () => Math.floor(Math.random()*3)); //0 normal, 1 ridged, 2 turbulent
this.biomRepLatitude = Math.random()+1;
this.biomRepLongitude = Math.random()+1;
}
getElevation(bounds, width, height, geometry, skirtGeometry, maxOctaves = 12){
const trim = super._trimEdges;
return new Promise((resolve, reject) => {
sendWorkerTask({
bounds: bounds,
resolution: width,
min: this.min,
max:this.max,
maxOctaves:Math.min(12,maxOctaves),
lacunarities: this.lacunarities,
biomRepLongitude: this.biomRepLongitude,
biomRepLatitude: this.biomRepLatitude,
shift: this.shift,
gains:this.gains,
noiseTypes:this.noiseTypes},
(response) => {
geometry.setIndex(new THREE.Uint32BufferAttribute(new Int32Array(response.indices),1));
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(response.vertices), 3));
geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(response.normals), 3));
geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(response.uvs), 2));
skirtGeometry.setIndex(new THREE.Uint32BufferAttribute(new Int32Array(response.skirtIndices),1));
skirtGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(response.skirts), 3));
skirtGeometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(response.skirtNormals), 3));
skirtGeometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(response.skirtUVs), 2));
geometry.computeBoundingSphere();
geometry.computeBoundingBox();
//geometry.computeVertexNormals();
skirtGeometry.computeBoundingSphere();
resolve({
elevationArray: trim(new Float32Array(response.extendedElevationBuffer), width+2, height+2),
shift: new THREE.Vector3(response.shift.x, response.shift.y, response.shift.z),
});
}, (error) => {
reject(error);
});
});
}
getElevation2(bounds, width, height, geometry, skirtGeometry, maxOctaves = 13) {
const meshGeneration = super._simpleMeshFromElevation;
const trim = super._trimEdges;
return new Promise((resolve, reject) => {
const extendedBounds = bounds.clone();
extendedBounds.min.x -= (bounds.max.x - bounds.min.x) / (width - 1);
extendedBounds.max.x += (bounds.max.x - bounds.min.x) / (width - 1);
extendedBounds.min.y -= (bounds.max.y - bounds.min.y) / (height - 1);
extendedBounds.max.y += (bounds.max.y - bounds.min.y) / (height - 1);
const extendedWidth = width + 2;
const extendedHeight = height + 2;
const latStep = (extendedBounds.max.y - extendedBounds.min.y) / (extendedHeight - 1);
const lonStep = (extendedBounds.max.x - extendedBounds.min.x) / (extendedWidth - 1);
let baseLat = extendedBounds.min.y;
let baseLon = extendedBounds.min.x;
var extendedElevationArray = new Array(extendedWidth * extendedHeight).fill(0);
let ampTotal = 0;
let lat = baseLat;
for (let y = 0; y < extendedHeight; y++, lat += latStep) {
let lon = baseLon;
for (let x = 0; x < extendedWidth; x++, lon += lonStep) {
let adjustedLon = lon;
let adjustedLat = lat;
if (adjustedLat > halfPI) {
adjustedLon -= Math.PI;
adjustedLat = halfPI - (adjustedLat - halfPI)
} else if (adjustedLat < -halfPI) {
adjustedLon -= Math.PI;
adjustedLat = -halfPI - (adjustedLat + halfPI)
}
if (adjustedLon > Math.PI) {
adjustedLon = -Math.PI + (adjustedLon % Math.PI);
}
else if (adjustedLon < -Math.PI) {
adjustedLon = Math.PI + (adjustedLon % Math.PI);
}
let a = Math.cos(adjustedLat) * Math.cos(adjustedLon);
let b = Math.cos(adjustedLat) * Math.sin(adjustedLon);
let c = Math.sin(adjustedLat);
const warpFactor = this.warpFactorMultiplier * noise(a, b, c);
const dx = warpFactor * noise(a + 0.57, b + 0.1248, c + 0.845);
const dy = warpFactor * noise(a + 0.1111, b + 0.744, c + 0.154);
const dz = warpFactor * noise(a + 0.287, b + 0.2678, c + 0.36698);
let p2 = 3 * (noise((a + 0.214) * this.continentFrequency, (b + 0.569) * this.continentFrequency, (c + 0.648) * this.continentFrequency));
let p1 = 3 * (noise((a + 0.878) * this.continentFrequency, (b + 0.2456) * this.continentFrequency, (c + 0.211) * this.continentFrequency));
//p1 = Math.sign(p1)*Math.sqrt(Math.abs(p1));
//p2 = Math.sign(p2)*Math.sqrt(Math.abs(p2));
const teracingMax = (1 + (noise((a + 0.456) * 10.0, (b + 0.678) * 10.0, (c + 0.125) * 10.0)));
const teracingMin = -(1 + (noise((a + 0.168) * 10.0, (b + 0.895) * 10.0, (c + 0.174) * 10.0)));
let previousTurbulence = 1;
for (let octave = 0; octave < maxOctaves; octave++) {
const freq = Math.pow(5, octave + 1 + this.freqSup);
const freqSimplex = freq * 0.02;
if (octave < this.maxOctaveSimplex) {
const ampSimplex = Math.pow(this.gainSimplex, octave + 1) * p2;
extendedElevationArray[extendedWidth * y + x] += Math.max(teracingMin, Math.min(teracingMax, noise((a + 0.187 + dx) * freqSimplex, (b + 0.289 + dy) * freqSimplex, (c + 0.247 + dz) * freqSimplex))) * ampSimplex;
}
if (octave < this.maxOctaveTurbulence) {
const ampTurbulence = Math.pow(this.gainTurbulence, octave + 1) * (p1) * 2;
//previousTurbulence = -(2.0 * (Math.max(teracingMin, Math.min(teracingMax, Math.abs(noise((a+0.966 + dx) * freq, (b+0.871 + dy) * freq, (c+0.498 + dz) * freq))))) - 1.0) * ampTurbulence * previousTurbulence;
previousTurbulence = Math.max(teracingMin, Math.min(teracingMax, Math.abs(noise((a + 0.966 + dx) * freq, (b + 0.871 + dy) * freq, (c + 0.498 + dz) * freq)) - this.turbulenceUp)) * ampTurbulence * previousTurbulence;
extendedElevationArray[extendedWidth * y + x] += previousTurbulence;
}
}
}
}
for (let octave = 0; octave < 13; octave++) {
if (octave < this.maxOctaveSimplex) {
ampTotal += Math.pow(this.gainSimplex, octave + 1);
}
if (octave < this.maxOctaveTurbulence) {
ampTotal += Math.pow(this.gainTurbulence, octave + 1);
}
}
for (let x = 0; x < extendedWidth; x++) {
for (let y = 0; y < extendedHeight; y++) {
extendedElevationArray[extendedWidth * y + x] = (((extendedElevationArray[extendedWidth * y + x] / ampTotal) + 1) * 0.5) * (this.max - this.min) + this.min /* + elevationMultiplierArray[width * y + x]*8000 */;
}
}
let shift;
if(geometry && skirtGeometry){
shift = meshGeneration(bounds, width, height, extendedElevationArray, geometry, skirtGeometry);
}
resolve({
elevationArray: trim(extendedElevationArray, width, height),
shift: shift,
});
});
}
}
function generateRandomRotation() {
// Generate a random unit quaternion for a uniformly distributed rotation
const u1 = Math.random();
const u2 = Math.random();
const u3 = Math.random();
return [u1,u2,u3];
}
function generateRandomOrthogonalMatrix() {
// Generate a random unit quaternion for a uniformly distributed rotation
const u1 = Math.random();
const u2 = Math.random();
const u3 = Math.random();
const q0 = Math.sqrt(1 - u1) * Math.sin(2 * Math.PI * u2);
const q1 = Math.sqrt(1 - u1) * Math.cos(2 * Math.PI * u2);
const q2 = Math.sqrt(u1) * Math.sin(2 * Math.PI * u3);
const q3 = Math.sqrt(u1) * Math.cos(2 * Math.PI * u3);
// Convert the quaternion to a 3x3 rotation matrix
const m = [
1 - 2 * (q2 * q2 + q3 * q3),
2 * (q1 * q2 - q0 * q3),
2 * (q1 * q3 + q0 * q2),
2 * (q1 * q2 + q0 * q3),
1 - 2 * (q1 * q1 + q3 * q3),
2 * (q2 * q3 - q0 * q1),
2 * (q1 * q3 - q0 * q2),
2 * (q2 * q3 + q0 * q1),
1 - 2 * (q1 * q1 + q2 * q2)
];
return m;
}
function generateRandomMatrixAndScale() {
// Generate a random 3x3 matrix
let m = Array.from({ length: 9 }, () => Math.random() * 10 - 5); // Random values in [-1, 1]
// Calculate the magnitude of each row to determine the scale
let scales = [
Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]),
Math.sqrt(m[3] * m[3] + m[4] * m[4] + m[5] * m[5]),
Math.sqrt(m[6] * m[6] + m[7] * m[7] + m[8] * m[8]),
];
// Adjust each row to normalize the scale to 1
for (let i = 0; i < 3; i++) {
let scale = scales[i];
for (let j = 0; j < 3; j++) {
m[i * 3 + j] /= scale;
}
}
return m;
}
export { PerlinElevationLayer };